From 7e16f500cb7bc0cfd8bafbf6bb1555704f771231 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Fri, 29 Apr 2022 12:13:34 +0200
Subject: chore: remove old pages, components, helpers and types

Since I'm using new components, I will also rewrite the GraphQL queries
so it is easier to start from scratch.
---
 src/pages/_app.tsx | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

(limited to 'src/pages/_app.tsx')

diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 84c2469..939b337 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -1,17 +1,16 @@
-import { AppPropsWithLayout } from '@ts/types/app';
 import { settings } from '@utils/config';
 import { AckeeProvider } from '@utils/providers/ackee';
 import { PrismThemeProvider } from '@utils/providers/prism-theme';
 import { ThemeProvider } from 'next-themes';
+import { AppProps } from 'next/app';
 import { useRouter } from 'next/router';
 import { IntlProvider } from 'react-intl';
 import '../styles/globals.scss';
 
-const MyApp = ({ Component, pageProps }: AppPropsWithLayout) => {
+const App = ({ Component, pageProps }: AppProps) => {
   const { locale, defaultLocale } = useRouter();
   const appLocale: string = locale || settings.locales.defaultLocale;
 
-  const getLayout = Component.getLayout ?? ((page) => page);
   return (
     <AckeeProvider domain={settings.ackee.url} siteId={settings.ackee.siteId}>
       <IntlProvider
@@ -25,7 +24,7 @@ const MyApp = ({ Component, pageProps }: AppPropsWithLayout) => {
           enableSystem={true}
         >
           <PrismThemeProvider>
-            {getLayout(<Component {...pageProps} />)}
+            <Component {...pageProps} />
           </PrismThemeProvider>
         </ThemeProvider>
       </IntlProvider>
@@ -33,4 +32,4 @@ const MyApp = ({ Component, pageProps }: AppPropsWithLayout) => {
   );
 };
 
-export default MyApp;
+export default App;
-- 
cgit v1.2.3


From 271ef6debaca7ed9a01829dcef3a37e90a2dff05 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Tue, 17 May 2022 22:48:41 +0200
Subject: chore: use persistent layout

It prevents to rerender the common components between pages (header,
footer...).
---
 src/components/templates/layout/layout.module.scss | 14 ++++++
 src/components/templates/layout/layout.test.tsx    | 11 ++---
 src/components/templates/layout/layout.tsx         | 54 +++++++++++++---------
 .../templates/page/page-layout.module.scss         | 11 -----
 .../templates/page/page-layout.stories.tsx         | 24 ++++------
 src/components/templates/page/page-layout.tsx      | 35 +++++++-------
 .../sectioned/sectioned-layout.stories.tsx         |  8 ++++
 .../templates/sectioned/sectioned-layout.tsx       | 25 ++++++++--
 src/pages/404.tsx                                  |  9 +++-
 src/pages/_app.tsx                                 |  7 +--
 src/pages/article/[slug].tsx                       | 16 +++++--
 src/pages/blog/index.tsx                           | 14 ++++--
 src/pages/contact.tsx                              | 11 +++--
 src/pages/cv.tsx                                   |  9 +++-
 src/pages/index.tsx                                | 20 +++++---
 src/pages/mentions-legales.tsx                     |  9 +++-
 src/pages/projets/[slug].tsx                       | 14 ++++--
 src/pages/projets/index.tsx                        | 10 ++--
 src/pages/recherche/index.tsx                      | 14 ++++--
 src/pages/sujet/[slug].tsx                         | 18 ++++++--
 src/pages/thematique/[slug].tsx                    | 15 ++++--
 src/ts/types/app.ts                                | 21 +++++++++
 22 files changed, 253 insertions(+), 116 deletions(-)

(limited to 'src/pages/_app.tsx')

diff --git a/src/components/templates/layout/layout.module.scss b/src/components/templates/layout/layout.module.scss
index 09904dc..1080732 100644
--- a/src/components/templates/layout/layout.module.scss
+++ b/src/components/templates/layout/layout.module.scss
@@ -1,5 +1,6 @@
 @use "@styles/abstracts/functions" as fun;
 @use "@styles/abstracts/mixins" as mix;
+@use "@styles/abstracts/placeholders";
 
 .header {
   border-bottom: fun.convert-px(3) solid var(--color-border-light);
@@ -9,6 +10,19 @@
   flex: 1;
 }
 
+.article {
+  &--grid {
+    @extend %grid;
+
+    grid-auto-flow: column dense;
+    align-items: baseline;
+  }
+
+  &--padding {
+    padding-bottom: var(--spacing-lg);
+  }
+}
+
 .footer {
   border-top: fun.convert-px(3) solid var(--color-border-light);
 }
diff --git a/src/components/templates/layout/layout.test.tsx b/src/components/templates/layout/layout.test.tsx
index 94145ec..78547d4 100644
--- a/src/components/templates/layout/layout.test.tsx
+++ b/src/components/templates/layout/layout.test.tsx
@@ -4,33 +4,32 @@ import Layout from './layout';
 
 const body =
   'Sit dolorem eveniet. Sit sit odio nemo vitae corrupti modi sint est rerum. Pariatur quidem maiores distinctio. Quia et illum aspernatur est cum.';
-const breadcrumbSchema: BreadcrumbList['itemListElement'][] = [];
 
 describe('Layout', () => {
   it('renders the website header', () => {
-    render(<Layout breadcrumbSchema={breadcrumbSchema}>{body}</Layout>);
+    render(<Layout>{body}</Layout>);
     expect(screen.getByRole('banner')).toBeInTheDocument();
   });
 
   it('renders the website main content', () => {
-    render(<Layout breadcrumbSchema={breadcrumbSchema}>{body}</Layout>);
+    render(<Layout>{body}</Layout>);
     expect(screen.getByRole('main')).toBeInTheDocument();
   });
 
   it('renders the website footer', () => {
-    render(<Layout breadcrumbSchema={breadcrumbSchema}>{body}</Layout>);
+    render(<Layout>{body}</Layout>);
     expect(screen.getByRole('contentinfo')).toBeInTheDocument();
   });
 
   it('renders a skip to content link', () => {
-    render(<Layout breadcrumbSchema={breadcrumbSchema}>{body}</Layout>);
+    render(<Layout>{body}</Layout>);
     expect(
       screen.getByRole('link', { name: 'Skip to content' })
     ).toBeInTheDocument();
   });
 
   it('renders an article', () => {
-    render(<Layout breadcrumbSchema={breadcrumbSchema}>{body}</Layout>);
+    render(<Layout>{body}</Layout>);
     expect(screen.getByRole('article')).toHaveTextContent(body);
   });
 });
diff --git a/src/components/templates/layout/layout.tsx b/src/components/templates/layout/layout.tsx
index 3eb02ee..559eaed 100644
--- a/src/components/templates/layout/layout.tsx
+++ b/src/components/templates/layout/layout.tsx
@@ -9,18 +9,13 @@ import Main from '@components/atoms/layout/main';
 import NoScript from '@components/atoms/layout/no-script';
 import Footer, { type FooterProps } from '@components/organisms/layout/footer';
 import Header, { type HeaderProps } from '@components/organisms/layout/header';
+import { type NextPageWithLayoutOptions } from '@ts/types/app';
 import useScrollPosition from '@utils/hooks/use-scroll-position';
 import useSettings from '@utils/hooks/use-settings';
 import Script from 'next/script';
-import { FC, ReactNode, useState } from 'react';
+import { FC, ReactElement, ReactNode, useState } from 'react';
 import { useIntl } from 'react-intl';
-import {
-  BreadcrumbList,
-  Person,
-  SearchAction,
-  WebSite,
-  WithContext,
-} from 'schema-dts';
+import { Person, SearchAction, WebSite, WithContext } from 'schema-dts';
 import styles from './layout.module.scss';
 
 export type QueryAction = SearchAction & {
@@ -28,18 +23,18 @@ export type QueryAction = SearchAction & {
 };
 
 export type LayoutProps = Pick<HeaderProps, 'isHome'> & {
-  /**
-   * The breadcrumb JSON schema.
-   */
-  breadcrumbSchema: BreadcrumbList['itemListElement'][];
   /**
    * The layout main content.
    */
   children: ReactNode;
   /**
-   * Set additional classnames to the article element.
+   * Determine if article has a comments section.
+   */
+  withExtraPadding?: boolean;
+  /**
+   * Determine if article should use grid. Default: false.
    */
-  className?: string;
+  useGrid?: boolean;
 };
 
 /**
@@ -48,14 +43,16 @@ export type LayoutProps = Pick<HeaderProps, 'isHome'> & {
  * Render the base layout used by all pages.
  */
 const Layout: FC<LayoutProps> = ({
-  breadcrumbSchema,
   children,
+  withExtraPadding = false,
   isHome,
-  ...props
+  useGrid = false,
 }) => {
   const intl = useIntl();
   const { website } = useSettings();
   const { baseline, copyright, locales, name, picture, url } = website;
+  const articleGridClass = useGrid ? 'article--grid' : '';
+  const articleCommentsClass = withExtraPadding ? 'article--padding' : '';
 
   const skipToContent = intl.formatMessage({
     defaultMessage: 'Skip to content',
@@ -188,11 +185,6 @@ const Layout: FC<LayoutProps> = ({
         type="application/ld+json"
         dangerouslySetInnerHTML={{ __html: JSON.stringify(brandingSchema) }}
       />
-      <Script
-        id="schema-breadcrumb"
-        type="application/ld+json"
-        dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
-      />
       <noscript>
         <div className={styles['noscript-spacing']}></div>
       </noscript>
@@ -211,7 +203,11 @@ const Layout: FC<LayoutProps> = ({
         className={styles.header}
       />
       <Main id="main" className={styles.main}>
-        <article {...props}>{children}</article>
+        <article
+          className={`${styles[articleGridClass]} ${styles[articleCommentsClass]}`}
+        >
+          {children}
+        </article>
       </Main>
       <Footer
         copyright={copyrightData}
@@ -227,4 +223,18 @@ const Layout: FC<LayoutProps> = ({
   );
 };
 
+/**
+ * Get the global layout.
+ *
+ * @param {ReactElement} page - A page.
+ * @param {boolean} [isHome] - Determine if it is the homepage.
+ * @returns A page wrapped with the global layout.
+ */
+export const getLayout = (
+  page: ReactElement,
+  props: NextPageWithLayoutOptions
+) => {
+  return <Layout {...props}>{page}</Layout>;
+};
+
 export default Layout;
diff --git a/src/components/templates/page/page-layout.module.scss b/src/components/templates/page/page-layout.module.scss
index c6b4e8d..c7674ae 100644
--- a/src/components/templates/page/page-layout.module.scss
+++ b/src/components/templates/page/page-layout.module.scss
@@ -2,17 +2,6 @@
 @use "@styles/abstracts/mixins" as mix;
 @use "@styles/abstracts/placeholders";
 
-.article {
-  @extend %grid;
-
-  grid-auto-flow: column dense;
-  align-items: baseline;
-
-  &--no-comments {
-    padding-bottom: var(--spacing-lg);
-  }
-}
-
 .breadcrumb {
   @extend %grid;
 
diff --git a/src/components/templates/page/page-layout.stories.tsx b/src/components/templates/page/page-layout.stories.tsx
index 8af5f98..bec1066 100644
--- a/src/components/templates/page/page-layout.stories.tsx
+++ b/src/components/templates/page/page-layout.stories.tsx
@@ -1,11 +1,11 @@
 import ButtonLink from '@components/atoms/buttons/button-link';
 import Heading from '@components/atoms/headings/heading';
 import Link from '@components/atoms/links/link';
-import ProgressBar from '@components/atoms/loaders/progress-bar';
 import PostsList from '@components/organisms/layout/posts-list';
 import LinksListWidget from '@components/organisms/widgets/links-list-widget';
 import Sharing from '@components/organisms/widgets/sharing';
 import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { LayoutBase } from '../layout/layout.stories';
 import PageLayoutComponent from './page-layout';
 
 /**
@@ -118,20 +118,6 @@ export default {
         required: false,
       },
     },
-    isHome: {
-      control: {
-        type: 'boolean',
-      },
-      description: 'Determine if the current page is the homepage.',
-      table: {
-        category: 'Options',
-        defaultValue: { summary: false },
-      },
-      type: {
-        name: 'boolean',
-        required: false,
-      },
-    },
     title: {
       control: {
         type: 'text',
@@ -168,6 +154,13 @@ export default {
       },
     },
   },
+  decorators: [
+    (Story) => (
+      <LayoutBase {...LayoutBase.args}>
+        <Story />
+      </LayoutBase>
+    ),
+  ],
   parameters: {
     layout: 'fullscreen',
   },
@@ -477,7 +470,6 @@ Blog.args = {
   children: (
     <>
       <PostsList posts={posts} byYear={true} total={posts.length} />
-      <ProgressBar min={1} max={1} current={1} info="1/1 page loaded." />
     </>
   ),
   widgets: [
diff --git a/src/components/templates/page/page-layout.tsx b/src/components/templates/page/page-layout.tsx
index 2ff2084..d171944 100644
--- a/src/components/templates/page/page-layout.tsx
+++ b/src/components/templates/page/page-layout.tsx
@@ -21,19 +21,20 @@ import TableOfContents from '@components/organisms/widgets/table-of-contents';
 import { type SendCommentVars } from '@services/graphql/api';
 import { sendComment } from '@services/graphql/comments';
 import useIsMounted from '@utils/hooks/use-is-mounted';
+import Script from 'next/script';
 import { FC, HTMLAttributes, ReactNode, useRef, useState } from 'react';
 import { useIntl } from 'react-intl';
-import Layout, { type LayoutProps } from '../layout/layout';
+import { BreadcrumbList } from 'schema-dts';
 import styles from './page-layout.module.scss';
 
-export type PageLayoutProps = Pick<
-  LayoutProps,
-  'breadcrumbSchema' | 'isHome'
-> & {
+export type PageLayoutProps = {
   /**
    * True if the page accepts new comments. Default: false.
    */
   allowComments?: boolean;
+  /**
+   * Set attributes to the page body.
+   */
   bodyAttributes?: HTMLAttributes<HTMLDivElement>;
   /**
    * Set additional classnames to the body wrapper.
@@ -43,6 +44,10 @@ export type PageLayoutProps = Pick<
    * The breadcrumb items.
    */
   breadcrumb: BreadcrumbItem[];
+  /**
+   * The breadcrumb JSON schema.
+   */
+  breadcrumbSchema: BreadcrumbList['itemListElement'][];
   /**
    * The main content of the page.
    */
@@ -98,7 +103,6 @@ const PageLayout: FC<PageLayoutProps> = ({
   headerMeta,
   id,
   intro,
-  isHome = false,
   title,
   widgets,
   withToC = false,
@@ -117,13 +121,7 @@ const PageLayout: FC<PageLayoutProps> = ({
 
   const bodyRef = useRef<HTMLDivElement>(null);
   const isMounted = useIsMounted(bodyRef);
-
   const hasComments = Array.isArray(comments) && comments.length > 0;
-  const articleModifier =
-    hasComments || allowComments
-      ? 'article--has-comments'
-      : 'article--no-comments';
-
   const [status, setStatus] = useState<NoticeKind>('info');
   const [statusMessage, setStatusMessage] = useState<string>('');
   const isReplyRef = useRef<boolean>(false);
@@ -186,11 +184,12 @@ const PageLayout: FC<PageLayoutProps> = ({
   };
 
   return (
-    <Layout
-      breadcrumbSchema={breadcrumbSchema}
-      isHome={isHome}
-      className={`${styles.article} ${styles[articleModifier]}`}
-    >
+    <>
+      <Script
+        id="schema-breadcrumb"
+        type="application/ld+json"
+        dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
+      />
       <Breadcrumb
         items={breadcrumb}
         className={styles.breadcrumb}
@@ -291,7 +290,7 @@ const PageLayout: FC<PageLayoutProps> = ({
           </section>
         </div>
       )}
-    </Layout>
+    </>
   );
 };
 
diff --git a/src/components/templates/sectioned/sectioned-layout.stories.tsx b/src/components/templates/sectioned/sectioned-layout.stories.tsx
index ce31a83..689f9a7 100644
--- a/src/components/templates/sectioned/sectioned-layout.stories.tsx
+++ b/src/components/templates/sectioned/sectioned-layout.stories.tsx
@@ -1,4 +1,5 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { LayoutBase } from '../layout/layout.stories';
 import SectionedLayoutComponent from './sectioned-layout';
 
 /**
@@ -31,6 +32,13 @@ export default {
       },
     },
   },
+  decorators: [
+    (Story) => (
+      <LayoutBase {...LayoutBase.args}>
+        <Story />
+      </LayoutBase>
+    ),
+  ],
   parameters: {
     layout: 'fullscreen',
   },
diff --git a/src/components/templates/sectioned/sectioned-layout.tsx b/src/components/templates/sectioned/sectioned-layout.tsx
index 58d5ad0..f91c354 100644
--- a/src/components/templates/sectioned/sectioned-layout.tsx
+++ b/src/components/templates/sectioned/sectioned-layout.tsx
@@ -2,12 +2,17 @@ import Section, {
   type SectionProps,
   type SectionVariant,
 } from '@components/atoms/layout/section';
+import Script from 'next/script';
 import { FC } from 'react';
-import Layout, { type LayoutProps } from '../layout/layout';
+import { BreadcrumbList } from 'schema-dts';
 
 export type Section = Pick<SectionProps, 'content' | 'title'>;
 
-export type SectionedLayoutProps = Pick<LayoutProps, 'breadcrumbSchema'> & {
+export type SectionedLayoutProps = {
+  /**
+   * The breadcrumb JSON schema.
+   */
+  breadcrumbSchema: BreadcrumbList['itemListElement'][];
   /**
    * An array of objects describing each section.
    */
@@ -19,7 +24,10 @@ export type SectionedLayoutProps = Pick<LayoutProps, 'breadcrumbSchema'> & {
  *
  * Render a sectioned layout.
  */
-const SectionedLayout: FC<SectionedLayoutProps> = ({ sections, ...props }) => {
+const SectionedLayout: FC<SectionedLayoutProps> = ({
+  breadcrumbSchema,
+  sections,
+}) => {
   const getSections = (items: SectionProps[]) => {
     return items.map((section, index) => {
       const variant: SectionVariant = index % 2 ? 'light' : 'dark';
@@ -37,7 +45,16 @@ const SectionedLayout: FC<SectionedLayoutProps> = ({ sections, ...props }) => {
     });
   };
 
-  return <Layout {...props}>{getSections(sections)}</Layout>;
+  return (
+    <>
+      <Script
+        id="schema-breadcrumb"
+        type="application/ld+json"
+        dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
+      />
+      {getSections(sections)}
+    </>
+  );
 };
 
 export default SectionedLayout;
diff --git a/src/pages/404.tsx b/src/pages/404.tsx
index 4f6e22d..4ab7784 100644
--- a/src/pages/404.tsx
+++ b/src/pages/404.tsx
@@ -1,11 +1,13 @@
 import Link from '@components/atoms/links/link';
 import LinksListWidget from '@components/organisms/widgets/links-list-widget';
+import { getLayout } from '@components/templates/layout/layout';
 import PageLayout from '@components/templates/page/page-layout';
 import {
   getThematicsPreview,
   getTotalThematics,
 } from '@services/graphql/thematics';
 import { getTopicsPreview, getTotalTopics } from '@services/graphql/topics';
+import { type NextPageWithLayout } from '@ts/types/app';
 import {
   type RawThematicPreview,
   type RawTopicPreview,
@@ -17,7 +19,7 @@ import {
 } from '@utils/helpers/pages';
 import useBreadcrumb from '@utils/hooks/use-breadcrumb';
 import useSettings from '@utils/hooks/use-settings';
-import { GetStaticProps, NextPage } from 'next';
+import { GetStaticProps } from 'next';
 import Head from 'next/head';
 import { ReactNode } from 'react';
 import { useIntl } from 'react-intl';
@@ -31,7 +33,7 @@ type Error404PageProps = {
 /**
  * Error 404 page.
  */
-const Error404Page: NextPage<Error404PageProps> = ({
+const Error404Page: NextPageWithLayout<Error404PageProps> = ({
   thematicsList,
   topicsList,
 }) => {
@@ -119,6 +121,9 @@ const Error404Page: NextPage<Error404PageProps> = ({
   );
 };
 
+Error404Page.getLayout = (page) =>
+  getLayout(page, { useGrid: true, withExtraPadding: true });
+
 export const getStaticProps: GetStaticProps<Error404PageProps> = async ({
   locale,
 }) => {
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 939b337..5bc9f85 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -1,15 +1,16 @@
+import { type AppPropsWithLayout } from '@ts/types/app';
 import { settings } from '@utils/config';
 import { AckeeProvider } from '@utils/providers/ackee';
 import { PrismThemeProvider } from '@utils/providers/prism-theme';
 import { ThemeProvider } from 'next-themes';
-import { AppProps } from 'next/app';
 import { useRouter } from 'next/router';
 import { IntlProvider } from 'react-intl';
 import '../styles/globals.scss';
 
-const App = ({ Component, pageProps }: AppProps) => {
+const App = ({ Component, pageProps }: AppPropsWithLayout) => {
   const { locale, defaultLocale } = useRouter();
   const appLocale: string = locale || settings.locales.defaultLocale;
+  const getLayout = Component.getLayout ?? ((page) => page);
 
   return (
     <AckeeProvider domain={settings.ackee.url} siteId={settings.ackee.siteId}>
@@ -24,7 +25,7 @@ const App = ({ Component, pageProps }: AppProps) => {
           enableSystem={true}
         >
           <PrismThemeProvider>
-            <Component {...pageProps} />
+            {getLayout(<Component {...pageProps} />, {})}
           </PrismThemeProvider>
         </ThemeProvider>
       </IntlProvider>
diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx
index a0fb7fc..a3df43b 100644
--- a/src/pages/article/[slug].tsx
+++ b/src/pages/article/[slug].tsx
@@ -2,6 +2,7 @@ import ButtonLink from '@components/atoms/buttons/button-link';
 import Link from '@components/atoms/links/link';
 import ResponsiveImage from '@components/molecules/images/responsive-image';
 import Sharing from '@components/organisms/widgets/sharing';
+import { getLayout } from '@components/templates/layout/layout';
 import PageLayout, {
   type PageLayoutProps,
 } from '@components/templates/page/page-layout';
@@ -11,7 +12,11 @@ import {
 } from '@services/graphql/articles';
 import { getPostComments } from '@services/graphql/comments';
 import styles from '@styles/pages/article.module.scss';
-import { type Article, type Comment } from '@ts/types/app';
+import {
+  type Article,
+  type Comment,
+  type NextPageWithLayout,
+} from '@ts/types/app';
 import { loadTranslation, type Messages } from '@utils/helpers/i18n';
 import useAddPrismClassAttr from '@utils/hooks/use-add-prism-class-attr';
 import useBreadcrumb from '@utils/hooks/use-breadcrumb';
@@ -20,7 +25,7 @@ import usePrismPlugins, {
 } from '@utils/hooks/use-prism-plugins';
 import useReadingTime from '@utils/hooks/use-reading-time';
 import useSettings from '@utils/hooks/use-settings';
-import { GetStaticPaths, GetStaticProps, NextPage } from 'next';
+import { GetStaticPaths, GetStaticProps } from 'next';
 import Head from 'next/head';
 import { useRouter } from 'next/router';
 import Script from 'next/script';
@@ -39,7 +44,10 @@ type ArticlePageProps = {
 /**
  * Article page.
  */
-const ArticlePage: NextPage<ArticlePageProps> = ({ comments, post }) => {
+const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({
+  comments,
+  post,
+}) => {
   const { content, id, intro, meta, slug, title } = post;
   const {
     author,
@@ -252,6 +260,8 @@ const ArticlePage: NextPage<ArticlePageProps> = ({ comments, post }) => {
   );
 };
 
+ArticlePage.getLayout = (page) => getLayout(page, { useGrid: true });
+
 interface PostParams extends ParsedUrlQuery {
   slug: string;
 }
diff --git a/src/pages/blog/index.tsx b/src/pages/blog/index.tsx
index b6ce221..2676305 100644
--- a/src/pages/blog/index.tsx
+++ b/src/pages/blog/index.tsx
@@ -1,6 +1,7 @@
 import Notice from '@components/atoms/layout/notice';
 import PostsList, { type Post } from '@components/organisms/layout/posts-list';
 import LinksListWidget from '@components/organisms/widgets/links-list-widget';
+import { getLayout } from '@components/templates/layout/layout';
 import PageLayout from '@components/templates/page/page-layout';
 import { type EdgesResponse } from '@services/graphql/api';
 import {
@@ -13,7 +14,11 @@ import {
   getTotalThematics,
 } from '@services/graphql/thematics';
 import { getTopicsPreview, getTotalTopics } from '@services/graphql/topics';
-import { type Article, type Meta } from '@ts/types/app';
+import {
+  type Article,
+  type Meta,
+  type NextPageWithLayout,
+} from '@ts/types/app';
 import {
   RawThematicPreview,
   RawTopicPreview,
@@ -28,7 +33,7 @@ import {
 import useBreadcrumb from '@utils/hooks/use-breadcrumb';
 import usePagination from '@utils/hooks/use-pagination';
 import useSettings from '@utils/hooks/use-settings';
-import { GetStaticProps, NextPage } from 'next';
+import { GetStaticProps } from 'next';
 import Head from 'next/head';
 import { useRouter } from 'next/router';
 import Script from 'next/script';
@@ -46,7 +51,7 @@ type BlogPageProps = {
 /**
  * Blog index page.
  */
-const BlogPage: NextPage<BlogPageProps> = ({
+const BlogPage: NextPageWithLayout<BlogPageProps> = ({
   articles,
   thematicsList,
   topicsList,
@@ -268,6 +273,9 @@ const BlogPage: NextPage<BlogPageProps> = ({
   );
 };
 
+BlogPage.getLayout = (page) =>
+  getLayout(page, { useGrid: true, withExtraPadding: true });
+
 export const getStaticProps: GetStaticProps<BlogPageProps> = async ({
   locale,
 }) => {
diff --git a/src/pages/contact.tsx b/src/pages/contact.tsx
index 617117b..c0d6c79 100644
--- a/src/pages/contact.tsx
+++ b/src/pages/contact.tsx
@@ -3,22 +3,24 @@ import ContactForm, {
   type ContactFormProps,
 } from '@components/organisms/forms/contact-form';
 import SocialMedia from '@components/organisms/widgets/social-media';
+import { getLayout } from '@components/templates/layout/layout';
 import PageLayout from '@components/templates/page/page-layout';
 import { meta } from '@content/pages/contact.mdx';
 import styles from '@styles/pages/contact.module.scss';
 import { sendMail } from '@services/graphql/contact';
+import { type NextPageWithLayout } from '@ts/types/app';
 import { loadTranslation } from '@utils/helpers/i18n';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
 import useSettings from '@utils/hooks/use-settings';
-import { GetStaticProps, NextPage } from 'next';
+import { GetStaticProps } from 'next';
 import Head from 'next/head';
 import { useRouter } from 'next/router';
 import Script from 'next/script';
 import { useState } from 'react';
 import { useIntl } from 'react-intl';
 import { ContactPage as ContactPageSchema, Graph, WebPage } from 'schema-dts';
-import useBreadcrumb from '@utils/hooks/use-breadcrumb';
 
-const ContactPage: NextPage = () => {
+const ContactPage: NextPageWithLayout = () => {
   const { dates, intro, seo, title } = meta;
   const intl = useIntl();
   const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
@@ -164,6 +166,9 @@ const ContactPage: NextPage = () => {
   );
 };
 
+ContactPage.getLayout = (page) =>
+  getLayout(page, { useGrid: true, withExtraPadding: true });
+
 export const getStaticProps: GetStaticProps = async ({ locale }) => {
   const translation = await loadTranslation(locale);
 
diff --git a/src/pages/cv.tsx b/src/pages/cv.tsx
index 7936c84..3f035d8 100644
--- a/src/pages/cv.tsx
+++ b/src/pages/cv.tsx
@@ -3,16 +3,18 @@ import Link from '@components/atoms/links/link';
 import List from '@components/atoms/lists/list';
 import ImageWidget from '@components/organisms/widgets/image-widget';
 import SocialMedia from '@components/organisms/widgets/social-media';
+import { getLayout } from '@components/templates/layout/layout';
 import PageLayout, {
   type PageLayoutProps,
 } from '@components/templates/page/page-layout';
 import CVContent, { data, meta } from '@content/pages/cv.mdx';
 import styles from '@styles/pages/cv.module.scss';
+import { type NextPageWithLayout } from '@ts/types/app';
 import { loadTranslation } from '@utils/helpers/i18n';
 import useBreadcrumb from '@utils/hooks/use-breadcrumb';
 import useSettings from '@utils/hooks/use-settings';
 import { NestedMDXComponents } from 'mdx/types';
-import { GetStaticProps, NextPage } from 'next';
+import { GetStaticProps } from 'next';
 import Head from 'next/head';
 import { useRouter } from 'next/router';
 import Script from 'next/script';
@@ -23,7 +25,7 @@ import { AboutPage, Graph, WebPage } from 'schema-dts';
 /**
  * CV page.
  */
-const CVPage: NextPage = () => {
+const CVPage: NextPageWithLayout = () => {
   const intl = useIntl();
   const { file, image } = data;
   const { dates, intro, seo, title } = meta;
@@ -186,6 +188,9 @@ const CVPage: NextPage = () => {
   );
 };
 
+CVPage.getLayout = (page) =>
+  getLayout(page, { useGrid: true, withExtraPadding: true });
+
 export const getStaticProps: GetStaticProps = async ({ locale }) => {
   const translation = await loadTranslation(locale);
 
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index 1143a33..a831ea3 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -13,16 +13,16 @@ import Columns, {
 import CardsList, {
   type CardsListItem,
 } from '@components/organisms/layout/cards-list';
-import Layout from '@components/templates/layout/layout';
+import { getLayout } from '@components/templates/layout/layout';
 import HomePageContent from '@content/pages/homepage.mdx';
 import { getArticlesCard } from '@services/graphql/articles';
 import styles from '@styles/pages/home.module.scss';
-import { ArticleCard } from '@ts/types/app';
+import { type ArticleCard, type NextPageWithLayout } from '@ts/types/app';
 import { loadTranslation, type Messages } from '@utils/helpers/i18n';
 import useBreadcrumb from '@utils/hooks/use-breadcrumb';
 import useSettings from '@utils/hooks/use-settings';
 import { NestedMDXComponents } from 'mdx/types';
-import { GetStaticProps, NextPage } from 'next';
+import { GetStaticProps } from 'next';
 import Head from 'next/head';
 import Script from 'next/script';
 import { ReactElement } from 'react';
@@ -37,7 +37,7 @@ type HomeProps = {
 /**
  * Home page.
  */
-const HomePage: NextPage<HomeProps> = ({ recentPosts }) => {
+const HomePage: NextPageWithLayout<HomeProps> = ({ recentPosts }) => {
   const intl = useIntl();
   const { schema: breadcrumbSchema } = useBreadcrumb({
     title: '',
@@ -327,7 +327,7 @@ const HomePage: NextPage<HomeProps> = ({ recentPosts }) => {
   };
 
   return (
-    <Layout breadcrumbSchema={breadcrumbSchema} isHome={true}>
+    <>
       <Head>
         <title>{pageTitle}</title>
         <meta name="description" content={pageDescription} />
@@ -340,11 +340,19 @@ const HomePage: NextPage<HomeProps> = ({ recentPosts }) => {
         type="application/ld+json"
         dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
       />
+      <Script
+        id="schema-breadcrumb"
+        type="application/ld+json"
+        dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
+      />
       <HomePageContent components={components} />
-    </Layout>
+    </>
   );
 };
 
+HomePage.getLayout = (page) =>
+  getLayout(page, { isHome: true, withExtraPadding: false });
+
 export const getStaticProps: GetStaticProps<HomeProps> = async ({ locale }) => {
   const translation = await loadTranslation(locale);
   const recentPosts = await getArticlesCard({ first: 3 });
diff --git a/src/pages/mentions-legales.tsx b/src/pages/mentions-legales.tsx
index 41bc218..c8d1772 100644
--- a/src/pages/mentions-legales.tsx
+++ b/src/pages/mentions-legales.tsx
@@ -1,14 +1,16 @@
 import Link from '@components/atoms/links/link';
 import ResponsiveImage from '@components/molecules/images/responsive-image';
+import { getLayout } from '@components/templates/layout/layout';
 import PageLayout, {
   type PageLayoutProps,
 } from '@components/templates/page/page-layout';
 import LegalNoticeContent, { meta } from '@content/pages/legal-notice.mdx';
+import { type NextPageWithLayout } from '@ts/types/app';
 import { loadTranslation } from '@utils/helpers/i18n';
 import useBreadcrumb from '@utils/hooks/use-breadcrumb';
 import useSettings from '@utils/hooks/use-settings';
 import { NestedMDXComponents } from 'mdx/types';
-import { GetStaticProps, NextPage } from 'next';
+import { GetStaticProps } from 'next';
 import Head from 'next/head';
 import { useRouter } from 'next/router';
 import Script from 'next/script';
@@ -17,7 +19,7 @@ import { Article, Graph, WebPage } from 'schema-dts';
 /**
  * Legal Notice page.
  */
-const LegalNoticePage: NextPage = () => {
+const LegalNoticePage: NextPageWithLayout = () => {
   const { dates, intro, seo, title } = meta;
   const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
     title,
@@ -111,6 +113,9 @@ const LegalNoticePage: NextPage = () => {
   );
 };
 
+LegalNoticePage.getLayout = (page) =>
+  getLayout(page, { useGrid: true, withExtraPadding: true });
+
 export const getStaticProps: GetStaticProps = async ({ locale }) => {
   const translation = await loadTranslation(locale);
 
diff --git a/src/pages/projets/[slug].tsx b/src/pages/projets/[slug].tsx
index 1a90e0f..cf7d0dc 100644
--- a/src/pages/projets/[slug].tsx
+++ b/src/pages/projets/[slug].tsx
@@ -6,10 +6,15 @@ import Code from '@components/molecules/layout/code';
 import Gallery from '@components/organisms/images/gallery';
 import Overview, { OverviewMeta } from '@components/organisms/layout/overview';
 import Sharing from '@components/organisms/widgets/sharing';
+import { getLayout } from '@components/templates/layout/layout';
 import PageLayout, {
   PageLayoutProps,
 } from '@components/templates/page/page-layout';
-import { ProjectPreview, Repos } from '@ts/types/app';
+import {
+  type NextPageWithLayout,
+  type ProjectPreview,
+  type Repos,
+} from '@ts/types/app';
 import { loadTranslation, Messages } from '@utils/helpers/i18n';
 import { getProjectData, getProjectFilenames } from '@utils/helpers/projects';
 import { capitalize } from '@utils/helpers/strings';
@@ -17,7 +22,7 @@ import useBreadcrumb from '@utils/hooks/use-breadcrumb';
 import useGithubApi, { RepoData } from '@utils/hooks/use-github-api';
 import useSettings from '@utils/hooks/use-settings';
 import { MDXComponents, NestedMDXComponents } from 'mdx/types';
-import { GetStaticPaths, GetStaticProps, NextPage } from 'next';
+import { GetStaticPaths, GetStaticProps } from 'next';
 import Head from 'next/head';
 import { useRouter } from 'next/router';
 import Script from 'next/script';
@@ -33,7 +38,7 @@ type ProjectPageProps = {
 /**
  * Project page.
  */
-const ProjectPage: NextPage<ProjectPageProps> = ({ project }) => {
+const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => {
   const { id, intro, meta, title } = project;
   const { cover, dates, license, repos, seo, technologies } = meta;
   const intl = useIntl();
@@ -207,6 +212,9 @@ const ProjectPage: NextPage<ProjectPageProps> = ({ project }) => {
   );
 };
 
+ProjectPage.getLayout = (page) =>
+  getLayout(page, { useGrid: true, withExtraPadding: true });
+
 export const getStaticProps: GetStaticProps<ProjectPageProps> = async ({
   locale,
   params,
diff --git a/src/pages/projets/index.tsx b/src/pages/projets/index.tsx
index 4a58269..d500b6b 100644
--- a/src/pages/projets/index.tsx
+++ b/src/pages/projets/index.tsx
@@ -2,16 +2,17 @@ import Link from '@components/atoms/links/link';
 import CardsList, {
   CardsListItem,
 } from '@components/organisms/layout/cards-list';
+import { getLayout } from '@components/templates/layout/layout';
 import PageLayout from '@components/templates/page/page-layout';
 import PageContent, { meta } from '@content/pages/projects.mdx';
 import styles from '@styles/pages/projects.module.scss';
-import { ProjectCard } from '@ts/types/app';
+import { type NextPageWithLayout, type ProjectCard } from '@ts/types/app';
 import { loadTranslation, Messages } from '@utils/helpers/i18n';
 import { getProjectsCard } from '@utils/helpers/projects';
 import useBreadcrumb from '@utils/hooks/use-breadcrumb';
 import useSettings from '@utils/hooks/use-settings';
 import { NestedMDXComponents } from 'mdx/types';
-import { GetStaticProps, NextPage } from 'next';
+import { GetStaticProps } from 'next';
 import Head from 'next/head';
 import { useRouter } from 'next/router';
 import Script from 'next/script';
@@ -25,7 +26,7 @@ type ProjectsPageProps = {
 /**
  * Projects page.
  */
-const ProjectsPage: NextPage<ProjectsPageProps> = ({ projects }) => {
+const ProjectsPage: NextPageWithLayout<ProjectsPageProps> = ({ projects }) => {
   const { dates, seo, title } = meta;
   const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
     title,
@@ -122,6 +123,9 @@ const ProjectsPage: NextPage<ProjectsPageProps> = ({ projects }) => {
   );
 };
 
+ProjectsPage.getLayout = (page) =>
+  getLayout(page, { useGrid: true, withExtraPadding: true });
+
 export const getStaticProps: GetStaticProps<ProjectsPageProps> = async ({
   locale,
 }) => {
diff --git a/src/pages/recherche/index.tsx b/src/pages/recherche/index.tsx
index d88a293..09091c8 100644
--- a/src/pages/recherche/index.tsx
+++ b/src/pages/recherche/index.tsx
@@ -2,6 +2,7 @@ import Notice from '@components/atoms/layout/notice';
 import Spinner from '@components/atoms/loaders/spinner';
 import PostsList, { type Post } from '@components/organisms/layout/posts-list';
 import LinksListWidget from '@components/organisms/widgets/links-list-widget';
+import { getLayout } from '@components/templates/layout/layout';
 import PageLayout from '@components/templates/page/page-layout';
 import { type EdgesResponse } from '@services/graphql/api';
 import {
@@ -14,7 +15,11 @@ import {
   getTotalThematics,
 } from '@services/graphql/thematics';
 import { getTopicsPreview, getTotalTopics } from '@services/graphql/topics';
-import { type Article, type Meta } from '@ts/types/app';
+import {
+  type Article,
+  type Meta,
+  type NextPageWithLayout,
+} from '@ts/types/app';
 import {
   RawThematicPreview,
   RawTopicPreview,
@@ -29,7 +34,7 @@ import useBreadcrumb from '@utils/hooks/use-breadcrumb';
 import useDataFromAPI from '@utils/hooks/use-data-from-api';
 import usePagination from '@utils/hooks/use-pagination';
 import useSettings from '@utils/hooks/use-settings';
-import { GetStaticProps, NextPage } from 'next';
+import { GetStaticProps } from 'next';
 import Head from 'next/head';
 import { useRouter } from 'next/router';
 import Script from 'next/script';
@@ -45,7 +50,7 @@ type SearchPageProps = {
 /**
  * Search page.
  */
-const SearchPage: NextPage<SearchPageProps> = ({
+const SearchPage: NextPageWithLayout<SearchPageProps> = ({
   thematicsList,
   topicsList,
 }) => {
@@ -283,6 +288,9 @@ const SearchPage: NextPage<SearchPageProps> = ({
   );
 };
 
+SearchPage.getLayout = (page) =>
+  getLayout(page, { useGrid: true, withExtraPadding: true });
+
 export const getStaticProps: GetStaticProps<SearchPageProps> = async ({
   locale,
 }) => {
diff --git a/src/pages/sujet/[slug].tsx b/src/pages/sujet/[slug].tsx
index 348fe05..6277293 100644
--- a/src/pages/sujet/[slug].tsx
+++ b/src/pages/sujet/[slug].tsx
@@ -2,6 +2,7 @@ import Heading from '@components/atoms/headings/heading';
 import ResponsiveImage from '@components/molecules/images/responsive-image';
 import PostsList, { type Post } from '@components/organisms/layout/posts-list';
 import LinksListWidget from '@components/organisms/widgets/links-list-widget';
+import { getLayout } from '@components/templates/layout/layout';
 import PageLayout, {
   type PageLayoutProps,
 } from '@components/templates/page/page-layout';
@@ -12,7 +13,12 @@ import {
   getTotalTopics,
 } from '@services/graphql/topics';
 import styles from '@styles/pages/topic.module.scss';
-import { type Article, type PageLink, type Topic } from '@ts/types/app';
+import {
+  type Article,
+  type NextPageWithLayout,
+  type PageLink,
+  type Topic,
+} from '@ts/types/app';
 import { loadTranslation, type Messages } from '@utils/helpers/i18n';
 import {
   getLinksListItems,
@@ -21,7 +27,7 @@ import {
 } from '@utils/helpers/pages';
 import useBreadcrumb from '@utils/hooks/use-breadcrumb';
 import useSettings from '@utils/hooks/use-settings';
-import { GetStaticPaths, GetStaticProps, NextPage } from 'next';
+import { GetStaticPaths, GetStaticProps } from 'next';
 import Head from 'next/head';
 import { useRouter } from 'next/router';
 import Script from 'next/script';
@@ -35,7 +41,10 @@ export type TopicPageProps = {
   translation: Messages;
 };
 
-const TopicPage: NextPage<TopicPageProps> = ({ currentTopic, topics }) => {
+const TopicPage: NextPageWithLayout<TopicPageProps> = ({
+  currentTopic,
+  topics,
+}) => {
   const { content, intro, meta, slug, title } = currentTopic;
   const {
     articles,
@@ -208,6 +217,9 @@ const TopicPage: NextPage<TopicPageProps> = ({ currentTopic, topics }) => {
   );
 };
 
+TopicPage.getLayout = (page) =>
+  getLayout(page, { useGrid: true, withExtraPadding: true });
+
 interface TopicParams extends ParsedUrlQuery {
   slug: string;
 }
diff --git a/src/pages/thematique/[slug].tsx b/src/pages/thematique/[slug].tsx
index 13ef0da..23e6a8b 100644
--- a/src/pages/thematique/[slug].tsx
+++ b/src/pages/thematique/[slug].tsx
@@ -1,6 +1,7 @@
 import Heading from '@components/atoms/headings/heading';
 import PostsList, { type Post } from '@components/organisms/layout/posts-list';
 import LinksListWidget from '@components/organisms/widgets/links-list-widget';
+import { getLayout } from '@components/templates/layout/layout';
 import PageLayout, {
   type PageLayoutProps,
 } from '@components/templates/page/page-layout';
@@ -10,7 +11,12 @@ import {
   getThematicsPreview,
   getTotalThematics,
 } from '@services/graphql/thematics';
-import { type Article, type PageLink, type Thematic } from '@ts/types/app';
+import {
+  type Article,
+  type NextPageWithLayout,
+  type PageLink,
+  type Thematic,
+} from '@ts/types/app';
 import { loadTranslation, type Messages } from '@utils/helpers/i18n';
 import {
   getLinksListItems,
@@ -19,7 +25,7 @@ import {
 } from '@utils/helpers/pages';
 import useBreadcrumb from '@utils/hooks/use-breadcrumb';
 import useSettings from '@utils/hooks/use-settings';
-import { GetStaticPaths, GetStaticProps, NextPage } from 'next';
+import { GetStaticPaths, GetStaticProps } from 'next';
 import Head from 'next/head';
 import { useRouter } from 'next/router';
 import Script from 'next/script';
@@ -33,7 +39,7 @@ export type ThematicPageProps = {
   translation: Messages;
 };
 
-const ThematicPage: NextPage<ThematicPageProps> = ({
+const ThematicPage: NextPageWithLayout<ThematicPageProps> = ({
   currentThematic,
   thematics,
 }) => {
@@ -191,6 +197,9 @@ const ThematicPage: NextPage<ThematicPageProps> = ({
   );
 };
 
+ThematicPage.getLayout = (page) =>
+  getLayout(page, { useGrid: true, withExtraPadding: true });
+
 interface ThematicParams extends ParsedUrlQuery {
   slug: string;
 }
diff --git a/src/ts/types/app.ts b/src/ts/types/app.ts
index a3b9889..feff5a5 100644
--- a/src/ts/types/app.ts
+++ b/src/ts/types/app.ts
@@ -1,3 +1,24 @@
+import { NextPage } from 'next';
+import { AppProps } from 'next/app';
+import { ReactElement, ReactNode } from 'react';
+
+export type NextPageWithLayoutOptions = {
+  withExtraPadding?: boolean;
+  isHome?: boolean;
+  useGrid?: boolean;
+};
+
+export type NextPageWithLayout<T = {}> = NextPage<T> & {
+  getLayout?: (
+    page: ReactElement,
+    options: NextPageWithLayoutOptions
+  ) => ReactNode;
+};
+
+export type AppPropsWithLayout = AppProps & {
+  Component: NextPageWithLayout;
+};
+
 export type ContentKind =
   | 'article'
   | 'comment'
-- 
cgit v1.2.3

