From 5a6584777e42e6e3e55294d357cb0adafe2853e7 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Fri, 22 Apr 2022 18:46:48 +0200
Subject: chore: add a Layout component

It defines the different components used by all other layouts.
---
 src/components/templates/layout/layout.stories.tsx | 57 ++++++++++++++++++++++
 1 file changed, 57 insertions(+)
 create mode 100644 src/components/templates/layout/layout.stories.tsx

(limited to 'src/components/templates/layout/layout.stories.tsx')

diff --git a/src/components/templates/layout/layout.stories.tsx b/src/components/templates/layout/layout.stories.tsx
new file mode 100644
index 0000000..f3579e3
--- /dev/null
+++ b/src/components/templates/layout/layout.stories.tsx
@@ -0,0 +1,57 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { IntlProvider } from 'react-intl';
+import LayoutComponent from './layout';
+
+/**
+ * Layout - Storybook Meta
+ */
+export default {
+  title: 'Templates/LayoutBase',
+  component: LayoutComponent,
+  argTypes: {
+    children: {
+      control: {
+        type: 'text',
+      },
+      description: 'The article content.',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the article element.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
+  },
+  decorators: [
+    (Story) => (
+      <IntlProvider locale="en">
+        <div id="__next">
+          <Story />
+        </div>
+      </IntlProvider>
+    ),
+  ],
+  parameters: {
+    layout: 'fullscreen',
+  },
+} as ComponentMeta<typeof LayoutComponent>;
+
+const Template: ComponentStory<typeof LayoutComponent> = (args) => (
+  <LayoutComponent {...args} />
+);
+
+/**
+ * Layout Stories - Default
+ */
+export const LayoutBase = Template.bind({});
-- 
cgit v1.2.3


From 732d0943f8041d76262222a092b014f2557085ef Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Mon, 2 May 2022 18:57:29 +0200
Subject: chore: add homepage

---
 src/components/atoms/lists/list.module.scss        |  16 +
 src/components/atoms/lists/list.stories.tsx        |  13 +-
 src/components/atoms/lists/list.tsx                |   4 +-
 src/components/molecules/layout/card.module.scss   |   3 +-
 src/components/molecules/layout/card.tsx           |   2 +-
 src/components/molecules/layout/page-header.tsx    |   2 +-
 src/components/molecules/nav/nav.stories.tsx       |  26 ++
 src/components/molecules/nav/nav.tsx               |   7 +-
 .../organisms/layout/cards-list.module.scss        |   4 +-
 .../organisms/layout/cards-list.stories.tsx        |  13 +
 src/components/organisms/layout/cards-list.tsx     |   8 +-
 src/components/templates/layout/layout.module.scss |   9 -
 src/components/templates/layout/layout.stories.tsx |  10 +-
 src/components/templates/layout/layout.tsx         | 107 ++++--
 src/pages/index.tsx                                | 365 +++++++++++++++++++++
 src/services/graphql/articles.ts                   |  52 ++-
 src/styles/pages/Home.module.scss                  |  49 ---
 src/styles/pages/home.module.scss                  |  36 ++
 src/utils/hooks/use-settings.tsx                   | 112 +++++++
 19 files changed, 745 insertions(+), 93 deletions(-)
 create mode 100644 src/pages/index.tsx
 delete mode 100644 src/styles/pages/Home.module.scss
 create mode 100644 src/styles/pages/home.module.scss
 create mode 100644 src/utils/hooks/use-settings.tsx

(limited to 'src/components/templates/layout/layout.stories.tsx')

diff --git a/src/components/atoms/lists/list.module.scss b/src/components/atoms/lists/list.module.scss
index df3b49c..f647072 100644
--- a/src/components/atoms/lists/list.module.scss
+++ b/src/components/atoms/lists/list.module.scss
@@ -1,3 +1,5 @@
+@use "@styles/abstracts/placeholders";
+
 .list {
   margin: 0;
 
@@ -36,4 +38,18 @@
       margin-bottom: var(--spacing-2xs);
     }
   }
+
+  &--flex {
+    @extend %reset-list;
+
+    display: flex;
+    flex-flow: row wrap;
+    gap: var(--spacing-sm);
+  }
+
+  &--flex &--flex {
+    display: initial;
+    position: relative;
+    top: var(--spacing-2xs);
+  }
 }
diff --git a/src/components/atoms/lists/list.stories.tsx b/src/components/atoms/lists/list.stories.tsx
index 3a80962..54fdd3a 100644
--- a/src/components/atoms/lists/list.stories.tsx
+++ b/src/components/atoms/lists/list.stories.tsx
@@ -39,8 +39,8 @@ export default {
       control: {
         type: 'select',
       },
-      description: 'The list kind: ordered or unordered.',
-      options: ['ordered', 'unordered'],
+      description: 'The list kind: flex, ordered or unordered.',
+      options: ['flex', 'ordered', 'unordered'],
       table: {
         category: 'Options',
         defaultValue: { summary: 'unordered' },
@@ -71,6 +71,15 @@ const items: ListItem[] = [
   { id: 'item-4', value: 'Item 4' },
 ];
 
+/**
+ * List Stories - Flex list
+ */
+export const Flex = Template.bind({});
+Flex.args = {
+  items,
+  kind: 'flex',
+};
+
 /**
  * List Stories - Ordered list
  */
diff --git a/src/components/atoms/lists/list.tsx b/src/components/atoms/lists/list.tsx
index 6726802..711ade1 100644
--- a/src/components/atoms/lists/list.tsx
+++ b/src/components/atoms/lists/list.tsx
@@ -30,9 +30,9 @@ export type ListProps = {
    */
   itemsClassName?: string;
   /**
-   * The list kind (ordered or unordered).
+   * The list kind.
    */
-  kind?: 'ordered' | 'unordered';
+  kind?: 'ordered' | 'unordered' | 'flex';
   /**
    * Set margin between list items. Default: true.
    */
diff --git a/src/components/molecules/layout/card.module.scss b/src/components/molecules/layout/card.module.scss
index 85c319a..d5b9836 100644
--- a/src/components/molecules/layout/card.module.scss
+++ b/src/components/molecules/layout/card.module.scss
@@ -19,7 +19,8 @@
 
   .cover {
     align-self: flex-start;
-    max-height: fun.convert-px(150);
+    place-content: center;
+    height: fun.convert-px(150);
     margin: auto;
     border-bottom: fun.convert-px(1) solid var(--color-border);
   }
diff --git a/src/components/molecules/layout/card.tsx b/src/components/molecules/layout/card.tsx
index 15927e9..89f100e 100644
--- a/src/components/molecules/layout/card.tsx
+++ b/src/components/molecules/layout/card.tsx
@@ -93,7 +93,7 @@ const Card: FC<CardProps> = ({
             {title}
           </Heading>
         </header>
-        {tagline && <div className={styles.tagline}>{tagline}</div>}
+        <div className={styles.tagline}>{tagline}</div>
         {meta && (
           <footer className={styles.footer}>
             <DescriptionList
diff --git a/src/components/molecules/layout/page-header.tsx b/src/components/molecules/layout/page-header.tsx
index 174e246..1663085 100644
--- a/src/components/molecules/layout/page-header.tsx
+++ b/src/components/molecules/layout/page-header.tsx
@@ -11,7 +11,7 @@ export type PageHeaderProps = {
   /**
    * The page introduction.
    */
-  intro?: string;
+  intro?: string | JSX.Element;
   /**
    * The page metadata.
    */
diff --git a/src/components/molecules/nav/nav.stories.tsx b/src/components/molecules/nav/nav.stories.tsx
index 25455fd..5cef5f0 100644
--- a/src/components/molecules/nav/nav.stories.tsx
+++ b/src/components/molecules/nav/nav.stories.tsx
@@ -11,6 +11,19 @@ export default {
   title: 'Molecules/Navigation/Nav',
   component: NavComponent,
   argTypes: {
+    'aria-label': {
+      control: {
+        type: 'text',
+      },
+      description: 'An accessible name for the navigation.',
+      table: {
+        category: 'Accessibility',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     className: {
       control: {
         type: 'text',
@@ -46,6 +59,19 @@ export default {
         required: true,
       },
     },
+    listClassName: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the navigation list.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
   },
   decorators: [
     (Story) => (
diff --git a/src/components/molecules/nav/nav.tsx b/src/components/molecules/nav/nav.tsx
index 2666ea2..581f813 100644
--- a/src/components/molecules/nav/nav.tsx
+++ b/src/components/molecules/nav/nav.tsx
@@ -23,6 +23,10 @@ export type NavItem = {
 };
 
 export type NavProps = {
+  /**
+   * An accessible name.
+   */
+  'aria-label'?: string;
   /**
    * Set additional classnames to the navigation wrapper.
    */
@@ -51,6 +55,7 @@ const Nav: FC<NavProps> = ({
   items,
   kind,
   listClassName = '',
+  ...props
 }) => {
   const kindClass = `nav--${kind}`;
 
@@ -71,7 +76,7 @@ const Nav: FC<NavProps> = ({
   };
 
   return (
-    <nav className={`${styles[kindClass]} ${className}`}>
+    <nav className={`${styles[kindClass]} ${className}`} {...props}>
       <ul className={`${styles.nav__list} ${listClassName}`}>{getItems()}</ul>
     </nav>
   );
diff --git a/src/components/organisms/layout/cards-list.module.scss b/src/components/organisms/layout/cards-list.module.scss
index 9fe428c..2763585 100644
--- a/src/components/organisms/layout/cards-list.module.scss
+++ b/src/components/organisms/layout/cards-list.module.scss
@@ -1,12 +1,10 @@
 @use "@styles/abstracts/placeholders";
 
 .wrapper {
-  --card-width: 30ch;
-
   display: grid;
   grid-template-columns: repeat(
     auto-fit,
-    min(calc(100vw - (var(--spacing-md) * 2)), var(--card-width))
+    min(calc(100vw - (var(--spacing-md) * 2)), var(--card-width, 30ch))
   );
   gap: var(--spacing-sm);
   place-content: center;
diff --git a/src/components/organisms/layout/cards-list.stories.tsx b/src/components/organisms/layout/cards-list.stories.tsx
index 7ff4365..fe0ebfd 100644
--- a/src/components/organisms/layout/cards-list.stories.tsx
+++ b/src/components/organisms/layout/cards-list.stories.tsx
@@ -12,6 +12,19 @@ export default {
     kind: 'unordered',
   },
   argTypes: {
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the list wrapper.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     coverFit: {
       control: {
         type: 'select',
diff --git a/src/components/organisms/layout/cards-list.tsx b/src/components/organisms/layout/cards-list.tsx
index 33ffe23..1558d7c 100644
--- a/src/components/organisms/layout/cards-list.tsx
+++ b/src/components/organisms/layout/cards-list.tsx
@@ -14,6 +14,10 @@ export type CardsListItem = Omit<
 };
 
 export type CardsListProps = {
+  /**
+   * Set additional classnames to the list wrapper.
+   */
+  className?: string;
   /**
    * The cover fit.
    */
@@ -38,6 +42,7 @@ export type CardsListProps = {
  * Return a list of Card components.
  */
 const CardsList: FC<CardsListProps> = ({
+  className = '',
   coverFit,
   items,
   kind = 'unordered',
@@ -70,9 +75,10 @@ const CardsList: FC<CardsListProps> = ({
 
   return (
     <List
+      kind="flex"
       items={getCards(items)}
       withMargin={false}
-      className={`${styles.wrapper} ${styles[kindModifier]}`}
+      className={`${styles.wrapper} ${styles[kindModifier]} ${className}`}
     />
   );
 };
diff --git a/src/components/templates/layout/layout.module.scss b/src/components/templates/layout/layout.module.scss
index 3533257..806d2d7 100644
--- a/src/components/templates/layout/layout.module.scss
+++ b/src/components/templates/layout/layout.module.scss
@@ -1,15 +1,6 @@
 @use "@styles/abstracts/functions" as fun;
 @use "@styles/abstracts/mixins" as mix;
 
-:global {
-  #__next {
-    flex: 1;
-    display: flex;
-    flex-flow: column nowrap;
-    min-height: 100vh;
-  }
-}
-
 .header {
   border-bottom: fun.convert-px(3) solid var(--color-border-light);
 }
diff --git a/src/components/templates/layout/layout.stories.tsx b/src/components/templates/layout/layout.stories.tsx
index f3579e3..2415412 100644
--- a/src/components/templates/layout/layout.stories.tsx
+++ b/src/components/templates/layout/layout.stories.tsx
@@ -36,7 +36,15 @@ export default {
   decorators: [
     (Story) => (
       <IntlProvider locale="en">
-        <div id="__next">
+        <div
+          id="__next"
+          style={{
+            flex: 1,
+            display: 'flex',
+            flexFlow: 'column nowrap',
+            minHeight: '100vh',
+          }}
+        >
           <Story />
         </div>
       </IntlProvider>
diff --git a/src/components/templates/layout/layout.tsx b/src/components/templates/layout/layout.tsx
index 601ced4..e1be1af 100644
--- a/src/components/templates/layout/layout.tsx
+++ b/src/components/templates/layout/layout.tsx
@@ -1,4 +1,3 @@
-import photo from '@assets/images/armand-philippot.jpg';
 import ButtonLink from '@components/atoms/buttons/button-link';
 import Career from '@components/atoms/icons/career';
 import CCBySA from '@components/atoms/icons/cc-by-sa';
@@ -8,13 +7,19 @@ import Home from '@components/atoms/icons/home';
 import PostsStack from '@components/atoms/icons/posts-stack';
 import Main from '@components/atoms/layout/main';
 import NoScript from '@components/atoms/layout/no-script';
-import Footer from '@components/organisms/layout/footer';
-import Header, { HeaderProps } from '@components/organisms/layout/header';
-import { settings } from '@utils/config';
+import Footer, { FooterProps } from '@components/organisms/layout/footer';
+import Header, { type HeaderProps } from '@components/organisms/layout/header';
+import useSettings from '@utils/hooks/use-settings';
+import Script from 'next/script';
 import { FC, ReactNode } from 'react';
 import { useIntl } from 'react-intl';
+import { Person, SearchAction, WebSite, WithContext } from 'schema-dts';
 import styles from './layout.module.scss';
 
+export type QueryAction = SearchAction & {
+  'query-input': string;
+};
+
 export type LayoutProps = Pick<HeaderProps, 'isHome'> & {
   /**
    * The layout main content.
@@ -33,6 +38,9 @@ export type LayoutProps = Pick<HeaderProps, 'isHome'> & {
  */
 const Layout: FC<LayoutProps> = ({ children, isHome, ...props }) => {
   const intl = useIntl();
+  const { website } = useSettings();
+  const { baseline, copyright, locales, name, picture, url } = website;
+
   const skipToContent = intl.formatMessage({
     defaultMessage: 'Skip to content',
     description: 'Layout: Skip to content link',
@@ -45,12 +53,12 @@ const Layout: FC<LayoutProps> = ({ children, isHome, ...props }) => {
     id: '7jVUT6',
   });
 
-  const copyright = {
+  const copyrightData = {
     dates: {
-      start: settings.copyright.startYear,
-      end: settings.copyright.endYear,
+      start: copyright.start,
+      end: copyright.end,
     },
-    owner: settings.name,
+    owner: name,
     icon: <CCBySA />,
   };
 
@@ -80,21 +88,77 @@ const Layout: FC<LayoutProps> = ({ children, isHome, ...props }) => {
     id: 'AE4kCD',
   });
 
-  const nav: HeaderProps['nav'] = [
-    { id: 'home', label: homeLabel, href: '#', logo: <Home /> },
-    { id: 'blog', label: blogLabel, href: '#', logo: <PostsStack /> },
+  const mainNav: HeaderProps['nav'] = [
+    { id: 'home', label: homeLabel, href: '/', logo: <Home /> },
+    { id: 'blog', label: blogLabel, href: '/blog', logo: <PostsStack /> },
     {
       id: 'projects',
       label: projectsLabel,
-      href: '#',
+      href: '/projets',
       logo: <ComputerScreen />,
     },
-    { id: 'cv', label: cvLabel, href: '#', logo: <Career /> },
-    { id: 'contact', label: contactLabel, href: '#', logo: <Envelop /> },
+    { id: 'cv', label: cvLabel, href: '/cv', logo: <Career /> },
+    { id: 'contact', label: contactLabel, href: '/contact', logo: <Envelop /> },
+  ];
+
+  const legalNoticeLabel = intl.formatMessage({
+    defaultMessage: 'Legal notice',
+    description: 'Layout: Legal notice label',
+    id: 'nwbzKm',
+  });
+
+  const footerNav: FooterProps['navItems'] = [
+    { id: 'legal-notice', label: legalNoticeLabel, href: '/mentions-legales' },
   ];
 
+  const searchActionSchema: QueryAction = {
+    '@type': 'SearchAction',
+    target: {
+      '@type': 'EntryPoint',
+      urlTemplate: `${url}/recherche?s={search_term_string}`,
+    },
+    query: 'required',
+    'query-input': 'required name=search_term_string',
+  };
+
+  const schemaJsonLd: WithContext<WebSite> = {
+    '@context': 'https://schema.org',
+    '@id': `${url}`,
+    '@type': 'WebSite',
+    name: name,
+    description: baseline,
+    url: url,
+    author: { '@id': `${url}/#branding` },
+    copyrightYear: Number(copyright.start),
+    creator: { '@id': `${url}/#branding` },
+    editor: { '@id': `${url}/#branding` },
+    inLanguage: locales.default,
+    potentialAction: searchActionSchema,
+  };
+
+  const brandingSchema: WithContext<Person> = {
+    '@context': 'https://schema.org',
+    '@type': 'Person',
+    '@id': `${url}/#branding`,
+    name: name,
+    url: url,
+    jobTitle: baseline,
+    image: picture.src,
+    subjectOf: { '@id': `${url}` },
+  };
+
   return (
     <>
+      <Script
+        id="schema-layout"
+        type="application/ld+json"
+        dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+      ></Script>
+      <Script
+        id="schema-branding"
+        type="application/ld+json"
+        dangerouslySetInnerHTML={{ __html: JSON.stringify(brandingSchema) }}
+      />
       <noscript>
         <div className={styles['noscript-spacing']}></div>
       </noscript>
@@ -103,10 +167,10 @@ const Layout: FC<LayoutProps> = ({ children, isHome, ...props }) => {
         {skipToContent}
       </ButtonLink>
       <Header
-        title={settings.name}
-        baseline={settings.baseline.fr}
-        photo={photo.src}
-        nav={nav}
+        title={name}
+        baseline={baseline}
+        photo={picture}
+        nav={mainNav}
         isHome={isHome}
         className={styles.header}
         withLink={true}
@@ -114,7 +178,12 @@ const Layout: FC<LayoutProps> = ({ children, isHome, ...props }) => {
       <Main id="main" className={styles.main}>
         <article {...props}>{children}</article>
       </Main>
-      <Footer copyright={copyright} topId="top" className={styles.footer} />
+      <Footer
+        copyright={copyrightData}
+        navItems={footerNav}
+        topId="top"
+        className={styles.footer}
+      />
       <noscript>
         <NoScript message={noScript} position="top" />
       </noscript>
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
new file mode 100644
index 0000000..c965320
--- /dev/null
+++ b/src/pages/index.tsx
@@ -0,0 +1,365 @@
+import FeedIcon from '@assets/images/icon-feed.svg';
+import ButtonLink from '@components/atoms/buttons/button-link';
+import Envelop from '@components/atoms/icons/envelop';
+import Column, { type ColumnProps } from '@components/atoms/layout/column';
+import Section, { type SectionProps } from '@components/atoms/layout/section';
+import List, { type ListItem } from '@components/atoms/lists/list';
+import ResponsiveImage, {
+  type ResponsiveImageProps,
+} from '@components/molecules/images/responsive-image';
+import Columns, {
+  type ColumnsProps,
+} from '@components/molecules/layout/columns';
+import CardsList, {
+  type CardsListItem,
+} from '@components/organisms/layout/cards-list';
+import Layout 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 { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import useSettings from '@utils/hooks/use-settings';
+import { NestedMDXComponents } from 'mdx/types';
+import { GetStaticProps, NextPage } from 'next';
+import Head from 'next/head';
+import Script from 'next/script';
+import { ReactElement } from 'react';
+import { useIntl } from 'react-intl';
+import { Graph, WebPage } from 'schema-dts';
+
+type HomeProps = {
+  recentPosts: ArticleCard[];
+  translation?: Messages;
+};
+
+/**
+ * Home page.
+ */
+const HomePage: NextPage<HomeProps> = ({ recentPosts }) => {
+  const intl = useIntl();
+
+  /**
+   * Retrieve a list of coding links.
+   *
+   * @returns {JSX.Element} - A list of links.
+   */
+  const CodingLinks = (): JSX.Element => {
+    const links: ListItem[] = [
+      {
+        id: 'web-development',
+        value: (
+          <ButtonLink target="/thematique/developpement-web">
+            {intl.formatMessage({
+              defaultMessage: 'Web development',
+              description: 'HomePage: link to web development thematic',
+              id: 'vkF/RP',
+            })}
+          </ButtonLink>
+        ),
+      },
+      {
+        id: 'projects',
+        value: (
+          <ButtonLink target="/projets">
+            {intl.formatMessage({
+              defaultMessage: 'Projects',
+              description: 'HomePage: link to projects',
+              id: 'N44SOc',
+            })}
+          </ButtonLink>
+        ),
+      },
+    ];
+
+    return <List kind="flex" items={links} className={styles.list} />;
+  };
+
+  /**
+   * Retrieve a list of Coldark repositories.
+   *
+   * @returns {JSX.Element} - A list of links.
+   */
+  const ColdarkRepos = (): JSX.Element => {
+    const links: ListItem[] = [
+      {
+        id: 'coldark-github',
+        value: (
+          <ButtonLink
+            target="https://github.com/ArmandPhilippot/coldark"
+            external={true}
+          >
+            {intl.formatMessage({
+              defaultMessage: 'Github',
+              description: 'HomePage: Github link',
+              id: '3f3PzH',
+            })}
+          </ButtonLink>
+        ),
+      },
+      {
+        id: 'coldark-gitlab',
+        value: (
+          <ButtonLink
+            target="https://gitlab.com/ArmandPhilippot/coldark"
+            external={true}
+          >
+            {intl.formatMessage({
+              defaultMessage: 'Gitlab',
+              description: 'HomePage: Gitlab link',
+              id: '7AnwZ7',
+            })}
+          </ButtonLink>
+        ),
+      },
+    ];
+
+    return <List kind="flex" items={links} className={styles.list} />;
+  };
+
+  /**
+   * Retrieve a list of links related to Free thematic.
+   *
+   * @returns {JSX.Element} - A list of links.
+   */
+  const LibreLinks = (): JSX.Element => {
+    const links: ListItem[] = [
+      {
+        id: 'free',
+        value: (
+          <ButtonLink target="/thematique/libre">
+            {intl.formatMessage({
+              defaultMessage: 'Free',
+              description: 'HomePage: link to free thematic',
+              id: 'w8GrOf',
+            })}
+          </ButtonLink>
+        ),
+      },
+      {
+        id: 'linux',
+        value: (
+          <ButtonLink target="/thematique/linux">
+            {intl.formatMessage({
+              defaultMessage: 'Linux',
+              description: 'HomePage: link to Linux thematic',
+              id: 'jASD7k',
+            })}
+          </ButtonLink>
+        ),
+      },
+    ];
+
+    return <List kind="flex" items={links} className={styles.list} />;
+  };
+
+  /**
+   * Retrieve the Shaarli link.
+   *
+   * @returns {JSX.Element} - A list of links
+   */
+  const ShaarliLink = (): JSX.Element => {
+    const links: ListItem[] = [
+      {
+        id: 'shaarli',
+        value: (
+          <ButtonLink target="https://shaarli.armandphilippot.com/">
+            {intl.formatMessage({
+              defaultMessage: 'Shaarli',
+              description: 'HomePage: link to Shaarli',
+              id: 'i5L19t',
+            })}
+          </ButtonLink>
+        ),
+      },
+    ];
+
+    return <List kind="flex" items={links} className={styles.list} />;
+  };
+
+  /**
+   * Retrieve the additional links.
+   *
+   * @returns {JSX.Element} - A list of links.
+   */
+  const MoreLinks = (): JSX.Element => {
+    const links: ListItem[] = [
+      {
+        id: 'contact-me',
+        value: (
+          <ButtonLink target="/contact">
+            <Envelop className={styles.icon} />
+            {intl.formatMessage({
+              defaultMessage: 'Contact me',
+              description: 'HomePage: contact button text',
+              id: 'sO/Iwj',
+            })}
+          </ButtonLink>
+        ),
+      },
+      {
+        id: 'rss-feed',
+        value: (
+          <ButtonLink target="/feed">
+            <FeedIcon className={`${styles.icon} ${styles['icon--feed']}`} />
+            {intl.formatMessage({
+              defaultMessage: 'Subscribe',
+              description: 'HomePage: RSS feed subscription text',
+              id: 'T4YA64',
+            })}
+          </ButtonLink>
+        ),
+      },
+    ];
+
+    return <List kind="flex" items={links} className={styles.list} />;
+  };
+
+  /**
+   * Get a cards list of recent posts.
+   *
+   * @returns {JSX.Element} - The cards list.
+   */
+  const getRecentPosts = (): JSX.Element => {
+    const posts: CardsListItem[] = recentPosts.map((post) => {
+      return {
+        cover: post.cover,
+        id: post.slug,
+        meta: [
+          {
+            id: 'publication',
+            term: intl.formatMessage({
+              defaultMessage: 'Published on:',
+              description: 'HomePage: publication date label',
+              id: 'pT5nHk',
+            }),
+            value: [post.dates.publication],
+          },
+        ],
+        title: post.title,
+        url: `/article/${post.slug}`,
+      };
+    });
+
+    return (
+      <CardsList
+        items={posts}
+        titleLevel={3}
+        className={`${styles.list} ${styles['list--cards']}`}
+      />
+    );
+  };
+
+  /**
+   * Create the page sections.
+   *
+   * @param {object} obj - An object containing the section body.
+   * @param {ReactElement[]} obj.children - The section body.
+   * @returns {JSX.Element} A section element.
+   */
+  const getSection = ({
+    children,
+    variant,
+  }: {
+    children: ReactElement[];
+    variant: SectionProps['variant'];
+  }): JSX.Element => {
+    const [headingEl, ...content] = children;
+    const title = headingEl.props.children;
+
+    return (
+      <Section
+        title={title}
+        content={content}
+        variant={variant}
+        className={styles.section}
+      />
+    );
+  };
+
+  const components: NestedMDXComponents = {
+    CodingLinks: CodingLinks,
+    ColdarkRepos: ColdarkRepos,
+    Column: (props: ColumnProps) => <Column {...props} />,
+    Columns: (props: ColumnsProps) => (
+      <Columns className={styles.columns} {...props} />
+    ),
+    Image: (props: ResponsiveImageProps) => <ResponsiveImage {...props} />,
+    LibreLinks: LibreLinks,
+    MoreLinks: MoreLinks,
+    RecentPosts: getRecentPosts,
+    Section: getSection,
+    ShaarliLink: ShaarliLink,
+  };
+
+  const { website } = useSettings();
+
+  const pageTitle = intl.formatMessage(
+    {
+      defaultMessage: '{websiteName} | Front-end developer: WordPress/React',
+      description: 'HomePage: SEO - Page title',
+      id: 'PXp2hv',
+    },
+    { websiteName: website.name }
+  );
+  const pageDescription = intl.formatMessage(
+    {
+      defaultMessage:
+        '{websiteName} is a front-end developer located in France. He codes and he writes mostly about web development and open-source.',
+      description: 'HomePage: SEO - Meta description',
+      id: 'tMuNTy',
+    },
+    { websiteName: website.name }
+  );
+
+  const webpageSchema: WebPage = {
+    '@id': `${website.url}/#home`,
+    '@type': 'WebPage',
+    name: pageTitle,
+    description: pageDescription,
+    author: { '@id': `${website.url}/#branding` },
+    creator: { '@id': `${website.url}/#branding` },
+    editor: { '@id': `${website.url}/#branding` },
+    inLanguage: website.locales.default,
+    license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+    reviewedBy: { '@id': `${website.url}/#branding` },
+    url: `${website.url}`,
+  };
+
+  const schemaJsonLd: Graph = {
+    '@context': 'https://schema.org',
+    '@graph': [webpageSchema],
+  };
+
+  return (
+    <Layout>
+      <Head>
+        <title>{pageTitle}</title>
+        <meta name="description" content={pageDescription} />
+        <meta property="og:url" content={website.url} />
+        <meta property="og:title" content={pageTitle} />
+        <meta property="og:description" content={pageDescription} />
+      </Head>
+      <Script
+        id="schema-homepage"
+        type="application/ld+json"
+        dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+      />
+      <HomePageContent components={components} />
+    </Layout>
+  );
+};
+
+export const getStaticProps: GetStaticProps = async ({ locale }) => {
+  const translation = await loadTranslation(locale);
+  const recentPosts = await getArticlesCard({ first: 3 });
+
+  return {
+    props: {
+      recentPosts,
+      translation,
+    },
+  };
+};
+
+export default HomePage;
diff --git a/src/services/graphql/articles.ts b/src/services/graphql/articles.ts
index e5ce7a5..7aff3e0 100644
--- a/src/services/graphql/articles.ts
+++ b/src/services/graphql/articles.ts
@@ -1,10 +1,19 @@
-import { Article } from '@ts/types/app';
-import { RawArticle, TotalItems } from '@ts/types/raw-data';
+import { type Article, type ArticleCard } from '@ts/types/app';
+import {
+  type RawArticle,
+  type RawArticlePreview,
+  type TotalItems,
+} from '@ts/types/raw-data';
 import { getAuthorFromRawData } from '@utils/helpers/author';
+import { getDates } from '@utils/helpers/dates';
 import { getImageFromRawData } from '@utils/helpers/images';
 import { getPageLinkFromRawData } from '@utils/helpers/pages';
 import { EdgesVars, fetchAPI, getAPIUrl, PageInfo } from './api';
-import { articlesQuery, totalArticlesQuery } from './articles.query';
+import {
+  articlesCardQuery,
+  articlesQuery,
+  totalArticlesQuery,
+} from './articles.query';
 
 /**
  * Retrieve the total number of articles.
@@ -102,3 +111,40 @@ export const getArticles = async ({
     pageInfo: response.posts.pageInfo,
   };
 };
+
+/**
+ * Convert a raw article preview to an article card.
+ *
+ * @param {RawArticlePreview} data - A raw article preview.
+ * @returns {ArticleCard} An article card.
+ */
+const getArticleCardFromRawData = (data: RawArticlePreview): ArticleCard => {
+  const { databaseId, date, featuredImage, slug, title } = data;
+
+  return {
+    cover: featuredImage ? getImageFromRawData(featuredImage.node) : undefined,
+    dates: getDates(date, ''),
+    id: databaseId,
+    slug,
+    title,
+  };
+};
+
+/**
+ * Retrieve the given number of article cards from API.
+ *
+ * @param {EdgesVars} obj - An object.
+ * @param {number} obj.first - The number of articles.
+ * @returns {Promise<ArticleCard[]>} - The article cards data.
+ */
+export const getArticlesCard = async ({
+  first,
+}: EdgesVars): Promise<ArticleCard[]> => {
+  const response = await fetchAPI<RawArticlePreview, typeof articlesCardQuery>({
+    api: getAPIUrl(),
+    query: articlesCardQuery,
+    variables: { first },
+  });
+
+  return response.posts.nodes.map((node) => getArticleCardFromRawData(node));
+};
diff --git a/src/styles/pages/Home.module.scss b/src/styles/pages/Home.module.scss
deleted file mode 100644
index 8225a57..0000000
--- a/src/styles/pages/Home.module.scss
+++ /dev/null
@@ -1,49 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/placeholders";
-
-.links-list {
-  @extend %flex-list;
-
-  gap: var(--spacing-md);
-  margin: 0 0 var(--spacing-md);
-}
-
-.icon--feed {
-  width: fun.convert-px(20);
-}
-
-:global {
-  [data-theme="dark"] {
-    :local {
-      .icon--feed {
-        filter: brightness(0.8) contrast(1.1);
-      }
-    }
-  }
-}
-
-.section {
-  --icon-size: #{fun.convert-px(20)};
-
-  composes: grid from "@styles/layout/_grid.scss";
-  padding: var(--spacing-md) 0;
-  background: var(--color-bg-secondary);
-
-  &:not(:last-child) {
-    border-bottom: fun.convert-px(1) solid var(--color-border);
-  }
-
-  &:nth-child(2n) {
-    background: var(--color-bg);
-  }
-
-  > * {
-    grid-column: 2;
-  }
-
-  :global {
-    .wp-block-columns {
-      margin: 0 0 var(--spacing-md);
-    }
-  }
-}
diff --git a/src/styles/pages/home.module.scss b/src/styles/pages/home.module.scss
new file mode 100644
index 0000000..873a5a9
--- /dev/null
+++ b/src/styles/pages/home.module.scss
@@ -0,0 +1,36 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.section {
+  --card-width: 25ch;
+
+  &:last-of-type {
+    border-bottom: none;
+  }
+}
+
+.columns {
+  margin: 0 0 var(--spacing-sm);
+}
+
+.list {
+  margin: 0 0 var(--spacing-sm);
+
+  &--cards {
+    @include mix.media("screen") {
+      @include mix.dimensions("md") {
+        margin: 0 calc(var(--spacing-sm) * -1) var(--spacing-sm);
+      }
+    }
+  }
+}
+
+.icon {
+  --icon-size: #{fun.convert-px(20)};
+
+  margin-right: var(--spacing-2xs);
+
+  &--feed {
+    width: var(--icon-size);
+  }
+}
diff --git a/src/utils/hooks/use-settings.tsx b/src/utils/hooks/use-settings.tsx
new file mode 100644
index 0000000..a45e934
--- /dev/null
+++ b/src/utils/hooks/use-settings.tsx
@@ -0,0 +1,112 @@
+import photo from '@assets/images/armand-philippot.jpg';
+import { settings } from '@utils/config';
+import { useRouter } from 'next/router';
+
+export type BlogSettings = {
+  /**
+   * The number of posts per page.
+   */
+  postsPerPage: number;
+};
+
+export type CopyrightSettings = {
+  /**
+   * The copyright end year.
+   */
+  end: string;
+  /**
+   * The copyright start year.
+   */
+  start: string;
+};
+
+export type LocaleSettings = {
+  /**
+   * The default locale.
+   */
+  default: string;
+  /**
+   * The supported locales.
+   */
+  supported: string[];
+};
+
+export type PictureSettings = {
+  /**
+   * The picture height.
+   */
+  height: number;
+  /**
+   * The picture url.
+   */
+  src: string;
+  /**
+   * The picture width.
+   */
+  width: number;
+};
+
+export type WebsiteSettings = {
+  /**
+   * The website name.
+   */
+  name: string;
+  /**
+   * The website baseline.
+   */
+  baseline: string;
+  /**
+   * The website copyright dates.
+   */
+  copyright: CopyrightSettings;
+  /**
+   * The website locales.
+   */
+  locales: LocaleSettings;
+  /**
+   * A picture representing the website.
+   */
+  picture: PictureSettings;
+  /**
+   * The website url.
+   */
+  url: string;
+};
+
+export type UseSettingsReturn = {
+  blog: BlogSettings;
+  website: WebsiteSettings;
+};
+
+/**
+ * Retrieve the website and blog settings.
+ *
+ * @returns {UseSettingsReturn} - An object describing settings.
+ */
+const useSettings = (): UseSettingsReturn => {
+  const { baseline, copyright, locales, name, postsPerPage, url } = settings;
+  const router = useRouter();
+  const locale = router.locale || locales.defaultLocale;
+
+  return {
+    blog: {
+      postsPerPage,
+    },
+    website: {
+      baseline: locale.startsWith('en') ? baseline.en : baseline.fr,
+      copyright: {
+        end: copyright.endYear,
+        start: copyright.startYear,
+      },
+      locales: {
+        default: locales.defaultLocale,
+        supported: locales.supported,
+      },
+      name,
+      picture: photo,
+      url,
+    },
+  };
+};
+
+export default useSettings;
-- 
cgit v1.2.3


From 2155550fa36a3bc3c8f66e0926530123b4018cd4 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Mon, 16 May 2022 12:46:38 +0200
Subject: refactor: use custom hook for breadcrumb items and schema

---
 src/components/templates/layout/layout.stories.tsx |  54 ++++++++---
 src/components/templates/layout/layout.test.tsx    |  12 ++-
 src/components/templates/layout/layout.tsx         |  26 ++++-
 .../templates/page/page-layout.stories.tsx         |  12 +++
 src/components/templates/page/page-layout.test.tsx |  41 ++++++--
 src/components/templates/page/page-layout.tsx      |  11 ++-
 .../sectioned/sectioned-layout.stories.tsx         |  24 +++--
 .../templates/sectioned/sectioned-layout.test.tsx  |   9 +-
 .../templates/sectioned/sectioned-layout.tsx       |   8 +-
 src/pages/404.tsx                                  |  16 ++--
 src/pages/article/[slug].tsx                       |  24 ++---
 src/pages/blog/index.tsx                           |  16 ++--
 src/pages/contact.tsx                              |  22 ++---
 src/pages/cv.tsx                                   |  22 ++---
 src/pages/index.tsx                                |   9 +-
 src/pages/mentions-legales.tsx                     |  22 ++---
 src/pages/projets/[slug].tsx                       |  27 ++----
 src/pages/projets/index.tsx                        |  22 ++---
 src/pages/recherche/index.tsx                      |  22 ++---
 src/pages/sujet/[slug].tsx                         |  22 ++---
 src/pages/thematique/[slug].tsx                    |  22 ++---
 src/utils/hooks/use-breadcrumb.tsx                 | 106 +++++++++++++++++++++
 22 files changed, 343 insertions(+), 206 deletions(-)
 create mode 100644 src/utils/hooks/use-breadcrumb.tsx

(limited to 'src/components/templates/layout/layout.stories.tsx')

diff --git a/src/components/templates/layout/layout.stories.tsx b/src/components/templates/layout/layout.stories.tsx
index 2415412..105e808 100644
--- a/src/components/templates/layout/layout.stories.tsx
+++ b/src/components/templates/layout/layout.stories.tsx
@@ -1,5 +1,4 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import LayoutComponent from './layout';
 
 /**
@@ -8,6 +7,10 @@ import LayoutComponent from './layout';
 export default {
   title: 'Templates/LayoutBase',
   component: LayoutComponent,
+  args: {
+    breadcrumbSchema: [],
+    isHome: false,
+  },
   argTypes: {
     children: {
       control: {
@@ -19,6 +22,31 @@ export default {
         required: true,
       },
     },
+    breadcrumbSchema: {
+      control: {
+        type: 'null',
+      },
+      description: 'The JSON schema for breadcrumb items.',
+      type: {
+        name: 'object',
+        required: true,
+        value: {},
+      },
+    },
+    isHome: {
+      control: {
+        type: 'boolean',
+      },
+      description: 'Determine if it is the homepage.',
+      table: {
+        category: 'Options',
+        defaultValue: { summary: false },
+      },
+      type: {
+        name: 'boolean',
+        required: false,
+      },
+    },
     className: {
       control: {
         type: 'text',
@@ -35,19 +63,17 @@ export default {
   },
   decorators: [
     (Story) => (
-      <IntlProvider locale="en">
-        <div
-          id="__next"
-          style={{
-            flex: 1,
-            display: 'flex',
-            flexFlow: 'column nowrap',
-            minHeight: '100vh',
-          }}
-        >
-          <Story />
-        </div>
-      </IntlProvider>
+      <div
+        id="__next"
+        style={{
+          flex: 1,
+          display: 'flex',
+          flexFlow: 'column nowrap',
+          minHeight: '100vh',
+        }}
+      >
+        <Story />
+      </div>
     ),
   ],
   parameters: {
diff --git a/src/components/templates/layout/layout.test.tsx b/src/components/templates/layout/layout.test.tsx
index 914e1cd..94145ec 100644
--- a/src/components/templates/layout/layout.test.tsx
+++ b/src/components/templates/layout/layout.test.tsx
@@ -1,34 +1,36 @@
 import { render, screen } from '@test-utils';
+import { BreadcrumbList } from 'schema-dts';
 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>{body}</Layout>);
+    render(<Layout breadcrumbSchema={breadcrumbSchema}>{body}</Layout>);
     expect(screen.getByRole('banner')).toBeInTheDocument();
   });
 
   it('renders the website main content', () => {
-    render(<Layout>{body}</Layout>);
+    render(<Layout breadcrumbSchema={breadcrumbSchema}>{body}</Layout>);
     expect(screen.getByRole('main')).toBeInTheDocument();
   });
 
   it('renders the website footer', () => {
-    render(<Layout>{body}</Layout>);
+    render(<Layout breadcrumbSchema={breadcrumbSchema}>{body}</Layout>);
     expect(screen.getByRole('contentinfo')).toBeInTheDocument();
   });
 
   it('renders a skip to content link', () => {
-    render(<Layout>{body}</Layout>);
+    render(<Layout breadcrumbSchema={breadcrumbSchema}>{body}</Layout>);
     expect(
       screen.getByRole('link', { name: 'Skip to content' })
     ).toBeInTheDocument();
   });
 
   it('renders an article', () => {
-    render(<Layout>{body}</Layout>);
+    render(<Layout breadcrumbSchema={breadcrumbSchema}>{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 bfb918b..9e9282b 100644
--- a/src/components/templates/layout/layout.tsx
+++ b/src/components/templates/layout/layout.tsx
@@ -13,7 +13,13 @@ import useSettings from '@utils/hooks/use-settings';
 import Script from 'next/script';
 import { FC, ReactNode } from 'react';
 import { useIntl } from 'react-intl';
-import { Person, SearchAction, WebSite, WithContext } from 'schema-dts';
+import {
+  BreadcrumbList,
+  Person,
+  SearchAction,
+  WebSite,
+  WithContext,
+} from 'schema-dts';
 import styles from './layout.module.scss';
 
 export type QueryAction = SearchAction & {
@@ -21,6 +27,10 @@ export type QueryAction = SearchAction & {
 };
 
 export type LayoutProps = Pick<HeaderProps, 'isHome'> & {
+  /**
+   * The breadcrumb JSON schema.
+   */
+  breadcrumbSchema: BreadcrumbList['itemListElement'][];
   /**
    * The layout main content.
    */
@@ -36,7 +46,12 @@ export type LayoutProps = Pick<HeaderProps, 'isHome'> & {
  *
  * Render the base layout used by all pages.
  */
-const Layout: FC<LayoutProps> = ({ children, isHome, ...props }) => {
+const Layout: FC<LayoutProps> = ({
+  breadcrumbSchema,
+  children,
+  isHome,
+  ...props
+}) => {
   const intl = useIntl();
   const { website } = useSettings();
   const { baseline, copyright, locales, name, picture, url } = website;
@@ -153,12 +168,17 @@ const Layout: FC<LayoutProps> = ({ children, isHome, ...props }) => {
         id="schema-layout"
         type="application/ld+json"
         dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
-      ></Script>
+      />
       <Script
         id="schema-branding"
         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>
diff --git a/src/components/templates/page/page-layout.stories.tsx b/src/components/templates/page/page-layout.stories.tsx
index 8e518aa..002b951 100644
--- a/src/components/templates/page/page-layout.stories.tsx
+++ b/src/components/templates/page/page-layout.stories.tsx
@@ -16,6 +16,7 @@ export default {
   component: PageLayoutComponent,
   args: {
     allowComments: false,
+    breadcrumbSchema: [],
   },
   argTypes: {
     allowComments: {
@@ -40,6 +41,17 @@ export default {
         value: {},
       },
     },
+    breadcrumbSchema: {
+      control: {
+        type: null,
+      },
+      description: 'The JSON schema for breadcrumb items.',
+      type: {
+        name: 'object',
+        required: true,
+        value: {},
+      },
+    },
     children: {
       control: {
         type: 'text',
diff --git a/src/components/templates/page/page-layout.test.tsx b/src/components/templates/page/page-layout.test.tsx
index b8fff6a..b3aea8b 100644
--- a/src/components/templates/page/page-layout.test.tsx
+++ b/src/components/templates/page/page-layout.test.tsx
@@ -1,4 +1,5 @@
 import { render, screen } from '@test-utils';
+import { BreadcrumbList } from 'schema-dts';
 import PageLayout from './page-layout';
 
 const title = 'Incidunt ad earum';
@@ -6,13 +7,18 @@ const breadcrumb = [
   { id: 'home', url: '#', name: 'Home' },
   { id: 'page', url: '#', name: title },
 ];
+const breadcrumbSchema: BreadcrumbList['itemListElement'][] = [];
 const children =
   'Reprehenderit aut quis aperiam magnam quia id. Vero enim animi placeat quia. Laborum sit odio minima. Dolores et debitis eaque iste quidem. Omnis aliquam illum porro ea non. Quaerat totam iste quos ex facilis officia accusantium.';
 
 describe('PageLayout', () => {
   it('renders the page title', () => {
     render(
-      <PageLayout breadcrumb={breadcrumb} title={title}>
+      <PageLayout
+        breadcrumb={breadcrumb}
+        breadcrumbSchema={breadcrumbSchema}
+        title={title}
+      >
         {children}
       </PageLayout>
     );
@@ -23,7 +29,11 @@ describe('PageLayout', () => {
 
   it('renders the page content', () => {
     render(
-      <PageLayout breadcrumb={breadcrumb} title={title}>
+      <PageLayout
+        breadcrumb={breadcrumb}
+        breadcrumbSchema={breadcrumbSchema}
+        title={title}
+      >
         {children}
       </PageLayout>
     );
@@ -32,7 +42,11 @@ describe('PageLayout', () => {
 
   it('renders the breadcrumb', () => {
     render(
-      <PageLayout breadcrumb={breadcrumb} title={title}>
+      <PageLayout
+        breadcrumb={breadcrumb}
+        breadcrumbSchema={breadcrumbSchema}
+        title={title}
+      >
         {children}
       </PageLayout>
     );
@@ -43,7 +57,12 @@ describe('PageLayout', () => {
 
   it('renders the table of contents', () => {
     render(
-      <PageLayout breadcrumb={breadcrumb} title={title} withToC={true}>
+      <PageLayout
+        breadcrumb={breadcrumb}
+        breadcrumbSchema={breadcrumbSchema}
+        title={title}
+        withToC={true}
+      >
         {children}
       </PageLayout>
     );
@@ -54,7 +73,12 @@ describe('PageLayout', () => {
 
   it('renders the comment form', () => {
     render(
-      <PageLayout breadcrumb={breadcrumb} title={title} allowComments={true}>
+      <PageLayout
+        breadcrumb={breadcrumb}
+        breadcrumbSchema={breadcrumbSchema}
+        title={title}
+        allowComments={true}
+      >
         {children}
       </PageLayout>
     );
@@ -79,7 +103,12 @@ describe('PageLayout', () => {
       },
     ];
     render(
-      <PageLayout breadcrumb={breadcrumb} title={title} comments={comments}>
+      <PageLayout
+        breadcrumb={breadcrumb}
+        breadcrumbSchema={breadcrumbSchema}
+        title={title}
+        comments={comments}
+      >
         {children}
       </PageLayout>
     );
diff --git a/src/components/templates/page/page-layout.tsx b/src/components/templates/page/page-layout.tsx
index 4e4ff00..bc90f4c 100644
--- a/src/components/templates/page/page-layout.tsx
+++ b/src/components/templates/page/page-layout.tsx
@@ -25,7 +25,10 @@ import { useIntl } from 'react-intl';
 import Layout, { type LayoutProps } from '../layout/layout';
 import styles from './page-layout.module.scss';
 
-export type PageLayoutProps = {
+export type PageLayoutProps = Pick<
+  LayoutProps,
+  'breadcrumbSchema' | 'isHome'
+> & {
   /**
    * True if the page accepts new comments. Default: false.
    */
@@ -58,10 +61,6 @@ export type PageLayoutProps = {
    * The page introduction.
    */
   intro?: PageHeaderProps['intro'];
-  /**
-   * True if it is homepage. Default: false.
-   */
-  isHome?: LayoutProps['isHome'];
   /**
    * The page title.
    */
@@ -85,6 +84,7 @@ const PageLayout: FC<PageLayoutProps> = ({
   children,
   allowComments = false,
   breadcrumb,
+  breadcrumbSchema,
   comments,
   footerMeta,
   headerMeta,
@@ -170,6 +170,7 @@ const PageLayout: FC<PageLayoutProps> = ({
 
   return (
     <Layout
+      breadcrumbSchema={breadcrumbSchema}
       isHome={isHome}
       className={`${styles.article} ${styles[articleModifier]}`}
     >
diff --git a/src/components/templates/sectioned/sectioned-layout.stories.tsx b/src/components/templates/sectioned/sectioned-layout.stories.tsx
index 9ff3b75..ce31a83 100644
--- a/src/components/templates/sectioned/sectioned-layout.stories.tsx
+++ b/src/components/templates/sectioned/sectioned-layout.stories.tsx
@@ -1,5 +1,4 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import SectionedLayoutComponent from './sectioned-layout';
 
 /**
@@ -8,7 +7,21 @@ import SectionedLayoutComponent from './sectioned-layout';
 export default {
   title: 'Templates/Sectioned',
   component: SectionedLayoutComponent,
+  args: {
+    breadcrumbSchema: [],
+  },
   argTypes: {
+    breadcrumbSchema: {
+      control: {
+        type: null,
+      },
+      description: 'The JSON schema for breadcrumb items.',
+      type: {
+        name: 'object',
+        required: true,
+        value: {},
+      },
+    },
     sections: {
       description: 'The different sections.',
       type: {
@@ -18,15 +31,6 @@ export default {
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <div id="__next">
-          <Story />
-        </div>
-      </IntlProvider>
-    ),
-  ],
   parameters: {
     layout: 'fullscreen',
   },
diff --git a/src/components/templates/sectioned/sectioned-layout.test.tsx b/src/components/templates/sectioned/sectioned-layout.test.tsx
index 334d1cc..9b8bab5 100644
--- a/src/components/templates/sectioned/sectioned-layout.test.tsx
+++ b/src/components/templates/sectioned/sectioned-layout.test.tsx
@@ -1,6 +1,8 @@
 import { render, screen } from '@test-utils';
+import { BreadcrumbList } from 'schema-dts';
 import SectionedLayout from './sectioned-layout';
 
+const breadcrumbSchema: BreadcrumbList['itemListElement'][] = [];
 const sections = [
   {
     title: 'Section 1',
@@ -26,7 +28,12 @@ const sections = [
 
 describe('SectionedLayout', () => {
   it('renders the correct number of section', () => {
-    render(<SectionedLayout sections={sections} />);
+    render(
+      <SectionedLayout
+        breadcrumbSchema={breadcrumbSchema}
+        sections={sections}
+      />
+    );
     expect(screen.getAllByRole('heading', { name: /^Section/ })).toHaveLength(
       sections.length
     );
diff --git a/src/components/templates/sectioned/sectioned-layout.tsx b/src/components/templates/sectioned/sectioned-layout.tsx
index 36ca039..58d5ad0 100644
--- a/src/components/templates/sectioned/sectioned-layout.tsx
+++ b/src/components/templates/sectioned/sectioned-layout.tsx
@@ -3,11 +3,11 @@ import Section, {
   type SectionVariant,
 } from '@components/atoms/layout/section';
 import { FC } from 'react';
-import Layout from '../layout/layout';
+import Layout, { type LayoutProps } from '../layout/layout';
 
 export type Section = Pick<SectionProps, 'content' | 'title'>;
 
-export type SectionedLayoutProps = {
+export type SectionedLayoutProps = Pick<LayoutProps, 'breadcrumbSchema'> & {
   /**
    * An array of objects describing each section.
    */
@@ -19,7 +19,7 @@ export type SectionedLayoutProps = {
  *
  * Render a sectioned layout.
  */
-const SectionedLayout: FC<SectionedLayoutProps> = ({ sections }) => {
+const SectionedLayout: FC<SectionedLayoutProps> = ({ sections, ...props }) => {
   const getSections = (items: SectionProps[]) => {
     return items.map((section, index) => {
       const variant: SectionVariant = index % 2 ? 'light' : 'dark';
@@ -37,7 +37,7 @@ const SectionedLayout: FC<SectionedLayoutProps> = ({ sections }) => {
     });
   };
 
-  return <Layout>{getSections(sections)}</Layout>;
+  return <Layout {...props}>{getSections(sections)}</Layout>;
 };
 
 export default SectionedLayout;
diff --git a/src/pages/404.tsx b/src/pages/404.tsx
index 7459c80..4f6e22d 100644
--- a/src/pages/404.tsx
+++ b/src/pages/404.tsx
@@ -1,5 +1,4 @@
 import Link from '@components/atoms/links/link';
-import { type BreadcrumbItem } from '@components/molecules/nav/breadcrumb';
 import LinksListWidget from '@components/organisms/widgets/links-list-widget';
 import PageLayout from '@components/templates/page/page-layout';
 import {
@@ -16,6 +15,7 @@ import {
   getLinksListItems,
   getPageLinkFromRawData,
 } from '@utils/helpers/pages';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
 import useSettings from '@utils/hooks/use-settings';
 import { GetStaticProps, NextPage } from 'next';
 import Head from 'next/head';
@@ -53,15 +53,10 @@ const Error404Page: NextPage<Error404PageProps> = ({
       link: (chunks: ReactNode) => <Link href="/contact">{chunks}</Link>,
     }
   );
-  const homeLabel = intl.formatMessage({
-    defaultMessage: 'Home',
-    description: 'Breadcrumb: home label',
-    id: 'j5k9Fe',
+  const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+    title,
+    url: `/404`,
   });
-  const breadcrumb: BreadcrumbItem[] = [
-    { id: 'home', name: homeLabel, url: '/' },
-    { id: 'error-404', name: title, url: '/404' },
-  ];
   const pageTitle = intl.formatMessage(
     {
       defaultMessage: 'Error 404: Page not found - {websiteName}',
@@ -95,7 +90,8 @@ const Error404Page: NextPage<Error404PageProps> = ({
       </Head>
       <PageLayout
         title={title}
-        breadcrumb={breadcrumb}
+        breadcrumb={breadcrumbItems}
+        breadcrumbSchema={breadcrumbSchema}
         widgets={[
           <LinksListWidget
             key="thematics-list"
diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx
index 6a47c16..5eeabd9 100644
--- a/src/pages/article/[slug].tsx
+++ b/src/pages/article/[slug].tsx
@@ -1,6 +1,5 @@
 import ButtonLink from '@components/atoms/buttons/button-link';
 import Link from '@components/atoms/links/link';
-import { type BreadcrumbItem } from '@components/molecules/nav/breadcrumb';
 import Sharing from '@components/organisms/widgets/sharing';
 import PageLayout, {
   type PageLayoutProps,
@@ -12,13 +11,13 @@ import {
 import { getPostComments } from '@services/graphql/comments';
 import { type Article, type Comment } 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 { GetStaticPaths, GetStaticProps, NextPage } from 'next';
 import Head from 'next/head';
 import { useRouter } from 'next/router';
 import Script from 'next/script';
 import { ParsedUrlQuery } from 'querystring';
-import { useIntl } from 'react-intl';
 import { Blog, BlogPosting, Graph, WebPage } from 'schema-dts';
 import useSWR from 'swr';
 
@@ -37,22 +36,10 @@ const ArticlePage: NextPage<ArticlePageProps> = ({ comments, post }) => {
   const { data } = useSWR(() => id, getPostComments, {
     fallbackData: comments,
   });
-  const intl = useIntl();
-  const homeLabel = intl.formatMessage({
-    defaultMessage: 'Home',
-    description: 'Breadcrumb: home label',
-    id: 'j5k9Fe',
+  const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+    title,
+    url: `/article/${slug}`,
   });
-  const blogLabel = intl.formatMessage({
-    defaultMessage: 'Blog',
-    description: 'Breadcrumb: blog label',
-    id: 'Es52wh',
-  });
-  const breadcrumb: BreadcrumbItem[] = [
-    { id: 'home', name: homeLabel, url: '/' },
-    { id: 'blog', name: blogLabel, url: '/blog' },
-    { id: 'article', name: title, url: `/article/${slug}` },
-  ];
 
   const headerMeta: PageLayoutProps['headerMeta'] = {
     author: author?.name,
@@ -186,7 +173,8 @@ const ArticlePage: NextPage<ArticlePageProps> = ({ comments, post }) => {
       />
       <PageLayout
         allowComments={true}
-        breadcrumb={breadcrumb}
+        breadcrumb={breadcrumbItems}
+        breadcrumbSchema={breadcrumbSchema}
         comments={data && getCommentsList(data)}
         footerMeta={footerMeta}
         headerMeta={headerMeta}
diff --git a/src/pages/blog/index.tsx b/src/pages/blog/index.tsx
index 38fabd5..a5ef045 100644
--- a/src/pages/blog/index.tsx
+++ b/src/pages/blog/index.tsx
@@ -1,5 +1,4 @@
 import Notice from '@components/atoms/layout/notice';
-import { type BreadcrumbItem } from '@components/molecules/nav/breadcrumb';
 import PostsList, { type Post } from '@components/organisms/layout/posts-list';
 import LinksListWidget from '@components/organisms/widgets/links-list-widget';
 import PageLayout from '@components/templates/page/page-layout';
@@ -26,6 +25,7 @@ import {
   getLinksListItems,
   getPageLinkFromRawData,
 } from '@utils/helpers/pages';
+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';
@@ -58,15 +58,10 @@ const BlogPage: NextPage<BlogPageProps> = ({
     description: 'BlogPage: page title',
     id: '7TbbIk',
   });
-  const homeLabel = intl.formatMessage({
-    defaultMessage: 'Home',
-    description: 'Breadcrumb: home label',
-    id: 'j5k9Fe',
+  const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+    title,
+    url: '/blog',
   });
-  const breadcrumb: BreadcrumbItem[] = [
-    { id: 'home', name: homeLabel, url: '/' },
-    { id: 'blog', name: title, url: '/blog' },
-  ];
 
   const { blog, website } = useSettings();
   const { asPath } = useRouter();
@@ -234,7 +229,8 @@ const BlogPage: NextPage<BlogPageProps> = ({
       />
       <PageLayout
         title={title}
-        breadcrumb={breadcrumb}
+        breadcrumb={breadcrumbItems}
+        breadcrumbSchema={breadcrumbSchema}
         headerMeta={{ total: postsCount }}
         widgets={[
           <LinksListWidget
diff --git a/src/pages/contact.tsx b/src/pages/contact.tsx
index fcfbe1d..617117b 100644
--- a/src/pages/contact.tsx
+++ b/src/pages/contact.tsx
@@ -1,7 +1,6 @@
-import Notice, { NoticeKind } from '@components/atoms/layout/notice';
-import { BreadcrumbItem } from '@components/molecules/nav/breadcrumb';
+import Notice, { type NoticeKind } from '@components/atoms/layout/notice';
 import ContactForm, {
-  ContactFormProps,
+  type ContactFormProps,
 } from '@components/organisms/forms/contact-form';
 import SocialMedia from '@components/organisms/widgets/social-media';
 import PageLayout from '@components/templates/page/page-layout';
@@ -17,19 +16,15 @@ 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 { dates, intro, seo, title } = meta;
   const intl = useIntl();
-  const homeLabel = intl.formatMessage({
-    defaultMessage: 'Home',
-    description: 'Breadcrumb: home label',
-    id: 'j5k9Fe',
+  const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+    title,
+    url: `/contact`,
   });
-  const breadcrumb: BreadcrumbItem[] = [
-    { id: 'home', name: homeLabel, url: '/' },
-    { id: 'contact', name: title, url: '/contact' },
-  ];
 
   const socialMediaTitle = intl.formatMessage({
     defaultMessage: 'Find me elsewhere',
@@ -148,9 +143,10 @@ const ContactPage: NextPage = () => {
         dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
       />
       <PageLayout
-        title="Contact"
+        breadcrumb={breadcrumbItems}
+        breadcrumbSchema={breadcrumbSchema}
         intro={intro}
-        breadcrumb={breadcrumb}
+        title="Contact"
         widgets={widgets}
       >
         <ContactForm
diff --git a/src/pages/cv.tsx b/src/pages/cv.tsx
index 0e4765e..b3dec10 100644
--- a/src/pages/cv.tsx
+++ b/src/pages/cv.tsx
@@ -1,5 +1,4 @@
 import Link from '@components/atoms/links/link';
-import { type BreadcrumbItem } from '@components/molecules/nav/breadcrumb';
 import ImageWidget from '@components/organisms/widgets/image-widget';
 import SocialMedia from '@components/organisms/widgets/social-media';
 import PageLayout, {
@@ -8,6 +7,7 @@ import PageLayout, {
 import CVContent, { data, meta } from '@content/pages/cv.mdx';
 import styles from '@styles/pages/cv.module.scss';
 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 Head from 'next/head';
@@ -24,15 +24,10 @@ const CVPage: NextPage = () => {
   const intl = useIntl();
   const { file, image } = data;
   const { dates, intro, seo, title } = meta;
-  const homeLabel = intl.formatMessage({
-    defaultMessage: 'Home',
-    description: 'Breadcrumb: home label',
-    id: 'j5k9Fe',
+  const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+    title,
+    url: `/cv`,
   });
-  const breadcrumb: BreadcrumbItem[] = [
-    { id: 'home', name: homeLabel, url: '/' },
-    { id: 'cv', name: title, url: '/cv' },
-  ];
 
   const imageWidgetTitle = intl.formatMessage({
     defaultMessage: 'Others formats',
@@ -148,12 +143,13 @@ const CVPage: NextPage = () => {
 
   return (
     <PageLayout
-      title={title}
-      intro={intro}
+      breadcrumb={breadcrumbItems}
+      breadcrumbSchema={breadcrumbSchema}
       headerMeta={headerMeta}
-      breadcrumb={breadcrumb}
-      withToC={true}
+      intro={intro}
+      title={title}
       widgets={widgets}
+      withToC={true}
     >
       <Head>
         <title>{`${seo.title} - ${website.name}`}</title>
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index bc9b572..1143a33 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -19,6 +19,7 @@ import { getArticlesCard } from '@services/graphql/articles';
 import styles from '@styles/pages/home.module.scss';
 import { ArticleCard } 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';
@@ -38,6 +39,10 @@ type HomeProps = {
  */
 const HomePage: NextPage<HomeProps> = ({ recentPosts }) => {
   const intl = useIntl();
+  const { schema: breadcrumbSchema } = useBreadcrumb({
+    title: '',
+    url: `/`,
+  });
 
   /**
    * Retrieve a list of coding links.
@@ -322,7 +327,7 @@ const HomePage: NextPage<HomeProps> = ({ recentPosts }) => {
   };
 
   return (
-    <Layout isHome={true}>
+    <Layout breadcrumbSchema={breadcrumbSchema} isHome={true}>
       <Head>
         <title>{pageTitle}</title>
         <meta name="description" content={pageDescription} />
@@ -340,7 +345,7 @@ const HomePage: NextPage<HomeProps> = ({ recentPosts }) => {
   );
 };
 
-export const getStaticProps: GetStaticProps = async ({ locale }) => {
+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 9235e69..41bc218 100644
--- a/src/pages/mentions-legales.tsx
+++ b/src/pages/mentions-legales.tsx
@@ -1,35 +1,28 @@
 import Link from '@components/atoms/links/link';
 import ResponsiveImage from '@components/molecules/images/responsive-image';
-import { type BreadcrumbItem } from '@components/molecules/nav/breadcrumb';
 import PageLayout, {
   type PageLayoutProps,
 } from '@components/templates/page/page-layout';
 import LegalNoticeContent, { meta } from '@content/pages/legal-notice.mdx';
 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 Head from 'next/head';
 import { useRouter } from 'next/router';
 import Script from 'next/script';
-import { useIntl } from 'react-intl';
 import { Article, Graph, WebPage } from 'schema-dts';
 
 /**
  * Legal Notice page.
  */
 const LegalNoticePage: NextPage = () => {
-  const intl = useIntl();
   const { dates, intro, seo, title } = meta;
-  const homeLabel = intl.formatMessage({
-    defaultMessage: 'Home',
-    description: 'Breadcrumb: home label',
-    id: 'j5k9Fe',
+  const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+    title,
+    url: `/mentions-legales`,
   });
-  const breadcrumb: BreadcrumbItem[] = [
-    { id: 'home', name: homeLabel, url: '/' },
-    { id: 'legal-notice', name: title, url: '/mentions-legales' },
-  ];
 
   const headerMeta: PageLayoutProps['headerMeta'] = {
     publication: {
@@ -93,10 +86,11 @@ const LegalNoticePage: NextPage = () => {
 
   return (
     <PageLayout
-      title={title}
-      intro={intro}
+      breadcrumb={breadcrumbItems}
+      breadcrumbSchema={breadcrumbSchema}
       headerMeta={headerMeta}
-      breadcrumb={breadcrumb}
+      intro={intro}
+      title={title}
       withToC={true}
     >
       <Head>
diff --git a/src/pages/projets/[slug].tsx b/src/pages/projets/[slug].tsx
index 711a5cd..1a90e0f 100644
--- a/src/pages/projets/[slug].tsx
+++ b/src/pages/projets/[slug].tsx
@@ -3,7 +3,6 @@ import SocialLink, { SocialWebsite } from '@components/atoms/links/social-link';
 import Spinner from '@components/atoms/loaders/spinner';
 import ResponsiveImage from '@components/molecules/images/responsive-image';
 import Code from '@components/molecules/layout/code';
-import { BreadcrumbItem } from '@components/molecules/nav/breadcrumb';
 import Gallery from '@components/organisms/images/gallery';
 import Overview, { OverviewMeta } from '@components/organisms/layout/overview';
 import Sharing from '@components/organisms/widgets/sharing';
@@ -14,6 +13,7 @@ import { ProjectPreview, 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';
+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';
@@ -37,21 +37,10 @@ const ProjectPage: NextPage<ProjectPageProps> = ({ project }) => {
   const { id, intro, meta, title } = project;
   const { cover, dates, license, repos, seo, technologies } = meta;
   const intl = useIntl();
-  const homeLabel = intl.formatMessage({
-    defaultMessage: 'Home',
-    description: 'Breadcrumb: home label',
-    id: 'j5k9Fe',
+  const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+    title,
+    url: `/projets/${id}`,
   });
-  const projectsLabel = intl.formatMessage({
-    defaultMessage: 'Projects',
-    description: 'Breadcrumb: projects label',
-    id: '28GZdv',
-  });
-  const breadcrumb: BreadcrumbItem[] = [
-    { id: 'home', name: homeLabel, url: '/' },
-    { id: 'projects', name: projectsLabel, url: '/projets' },
-    { id: 'project', name: title, url: `/projets/${id}` },
-  ];
 
   const ProjectContent: ComponentType<MDXComponents> =
     require(`../../content/projects/${id}.mdx`).default;
@@ -192,7 +181,8 @@ const ProjectPage: NextPage<ProjectPageProps> = ({ project }) => {
       <PageLayout
         title={title}
         intro={intro}
-        breadcrumb={breadcrumb}
+        breadcrumb={breadcrumbItems}
+        breadcrumbSchema={breadcrumbSchema}
         headerMeta={headerMeta}
         withToC={true}
         widgets={[
@@ -217,7 +207,10 @@ const ProjectPage: NextPage<ProjectPageProps> = ({ project }) => {
   );
 };
 
-export const getStaticProps: GetStaticProps = async ({ locale, params }) => {
+export const getStaticProps: GetStaticProps<ProjectPageProps> = async ({
+  locale,
+  params,
+}) => {
   const translation = await loadTranslation(locale);
   const { slug } = params!;
   const project = await getProjectData(slug as string);
diff --git a/src/pages/projets/index.tsx b/src/pages/projets/index.tsx
index a6858c8..4a58269 100644
--- a/src/pages/projets/index.tsx
+++ b/src/pages/projets/index.tsx
@@ -1,5 +1,4 @@
 import Link from '@components/atoms/links/link';
-import { BreadcrumbItem } from '@components/molecules/nav/breadcrumb';
 import CardsList, {
   CardsListItem,
 } from '@components/organisms/layout/cards-list';
@@ -9,13 +8,13 @@ import styles from '@styles/pages/projects.module.scss';
 import { 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 Head from 'next/head';
 import { useRouter } from 'next/router';
 import Script from 'next/script';
-import { useIntl } from 'react-intl';
 import { Article, Graph, WebPage } from 'schema-dts';
 
 type ProjectsPageProps = {
@@ -27,17 +26,11 @@ type ProjectsPageProps = {
  * Projects page.
  */
 const ProjectsPage: NextPage<ProjectsPageProps> = ({ projects }) => {
-  const intl = useIntl();
   const { dates, seo, title } = meta;
-  const homeLabel = intl.formatMessage({
-    defaultMessage: 'Home',
-    description: 'Breadcrumb: home label',
-    id: 'j5k9Fe',
+  const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+    title,
+    url: `/projets`,
   });
-  const breadcrumb: BreadcrumbItem[] = [
-    { id: 'home', name: homeLabel, url: '/' },
-    { id: 'projects', name: title, url: '/projets' },
-  ];
 
   const items: CardsListItem[] = projects.map(
     ({ id, meta: projectMeta, slug, title: projectTitle }) => {
@@ -120,7 +113,8 @@ const ProjectsPage: NextPage<ProjectsPageProps> = ({ projects }) => {
       <PageLayout
         title={title}
         intro={<PageContent components={components} />}
-        breadcrumb={breadcrumb}
+        breadcrumb={breadcrumbItems}
+        breadcrumbSchema={breadcrumbSchema}
       >
         <CardsList items={items} titleLevel={2} className={styles.list} />
       </PageLayout>
@@ -128,7 +122,9 @@ const ProjectsPage: NextPage<ProjectsPageProps> = ({ projects }) => {
   );
 };
 
-export const getStaticProps: GetStaticProps = async ({ locale }) => {
+export const getStaticProps: GetStaticProps<ProjectsPageProps> = async ({
+  locale,
+}) => {
   const projects = await getProjectsCard();
   const translation = await loadTranslation(locale);
 
diff --git a/src/pages/recherche/index.tsx b/src/pages/recherche/index.tsx
index bf14861..0a7dc60 100644
--- a/src/pages/recherche/index.tsx
+++ b/src/pages/recherche/index.tsx
@@ -1,6 +1,5 @@
 import Notice from '@components/atoms/layout/notice';
 import Spinner from '@components/atoms/loaders/spinner';
-import { type BreadcrumbItem } from '@components/molecules/nav/breadcrumb';
 import PostsList, { type Post } from '@components/organisms/layout/posts-list';
 import LinksListWidget from '@components/organisms/widgets/links-list-widget';
 import PageLayout from '@components/templates/page/page-layout';
@@ -26,6 +25,7 @@ import {
   getLinksListItems,
   getPageLinkFromRawData,
 } from '@utils/helpers/pages';
+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';
@@ -65,21 +65,10 @@ const SearchPage: NextPage<SearchPageProps> = ({
         description: 'SearchPage: SEO - Page title',
         id: 'WDwNDl',
       });
-  const homeLabel = intl.formatMessage({
-    defaultMessage: 'Home',
-    description: 'Breadcrumb: home label',
-    id: 'j5k9Fe',
+  const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+    title,
+    url: `/recherche`,
   });
-  const blogLabel = intl.formatMessage({
-    defaultMessage: 'Blog',
-    description: 'Breadcrumb: blog label',
-    id: 'Es52wh',
-  });
-  const breadcrumb: BreadcrumbItem[] = [
-    { id: 'home', name: homeLabel, url: '/' },
-    { id: 'blog', name: blogLabel, url: '/blog' },
-    { id: 'search', name: title, url: '/recherche' },
-  ];
 
   const { blog, website } = useSettings();
   const pageTitle = `${title} - ${website.name}`;
@@ -253,7 +242,8 @@ const SearchPage: NextPage<SearchPageProps> = ({
       />
       <PageLayout
         title={title}
-        breadcrumb={breadcrumb}
+        breadcrumb={breadcrumbItems}
+        breadcrumbSchema={breadcrumbSchema}
         headerMeta={{ total: postsCount }}
         widgets={[
           <LinksListWidget
diff --git a/src/pages/sujet/[slug].tsx b/src/pages/sujet/[slug].tsx
index 22fb531..447d969 100644
--- a/src/pages/sujet/[slug].tsx
+++ b/src/pages/sujet/[slug].tsx
@@ -1,5 +1,4 @@
 import Heading from '@components/atoms/headings/heading';
-import { type BreadcrumbItem } from '@components/molecules/nav/breadcrumb';
 import PostsList, { type Post } from '@components/organisms/layout/posts-list';
 import LinksListWidget from '@components/organisms/widgets/links-list-widget';
 import PageLayout, {
@@ -18,6 +17,7 @@ import {
   getPageLinkFromRawData,
   getPostMeta,
 } 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 Head from 'next/head';
@@ -37,21 +37,10 @@ const TopicPage: NextPage<TopicPageProps> = ({ currentTopic, topics }) => {
   const { content, intro, meta, slug, title } = currentTopic;
   const { articles, dates, seo, thematics } = meta;
   const intl = useIntl();
-  const homeLabel = intl.formatMessage({
-    defaultMessage: 'Home',
-    description: 'Breadcrumb: home label',
-    id: 'j5k9Fe',
+  const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+    title,
+    url: `/sujet/${slug}`,
   });
-  const blogLabel = intl.formatMessage({
-    defaultMessage: 'Blog',
-    description: 'Breadcrumb: blog label',
-    id: 'Es52wh',
-  });
-  const breadcrumb: BreadcrumbItem[] = [
-    { id: 'home', name: homeLabel, url: '/' },
-    { id: 'blog', name: blogLabel, url: '/blog' },
-    { id: 'topic', name: title, url: `/sujet/${slug}` },
-  ];
 
   const headerMeta: PageLayoutProps['headerMeta'] = {
     publication: { date: dates.publication },
@@ -149,7 +138,8 @@ const TopicPage: NextPage<TopicPageProps> = ({ currentTopic, topics }) => {
         dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
       />
       <PageLayout
-        breadcrumb={breadcrumb}
+        breadcrumb={breadcrumbItems}
+        breadcrumbSchema={breadcrumbSchema}
         title={title}
         intro={intro}
         headerMeta={headerMeta}
diff --git a/src/pages/thematique/[slug].tsx b/src/pages/thematique/[slug].tsx
index dd7e80d..13ef0da 100644
--- a/src/pages/thematique/[slug].tsx
+++ b/src/pages/thematique/[slug].tsx
@@ -1,5 +1,4 @@
 import Heading from '@components/atoms/headings/heading';
-import { type BreadcrumbItem } from '@components/molecules/nav/breadcrumb';
 import PostsList, { type Post } from '@components/organisms/layout/posts-list';
 import LinksListWidget from '@components/organisms/widgets/links-list-widget';
 import PageLayout, {
@@ -18,6 +17,7 @@ import {
   getPageLinkFromRawData,
   getPostMeta,
 } 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 Head from 'next/head';
@@ -40,21 +40,10 @@ const ThematicPage: NextPage<ThematicPageProps> = ({
   const { content, intro, meta, slug, title } = currentThematic;
   const { articles, dates, seo, topics } = meta;
   const intl = useIntl();
-  const homeLabel = intl.formatMessage({
-    defaultMessage: 'Home',
-    description: 'Breadcrumb: home label',
-    id: 'j5k9Fe',
+  const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+    title,
+    url: `/thematique/${slug}`,
   });
-  const blogLabel = intl.formatMessage({
-    defaultMessage: 'Blog',
-    description: 'Breadcrumb: blog label',
-    id: 'Es52wh',
-  });
-  const breadcrumb: BreadcrumbItem[] = [
-    { id: 'home', name: homeLabel, url: '/' },
-    { id: 'blog', name: blogLabel, url: '/blog' },
-    { id: 'thematic', name: title, url: `/thematique/${slug}` },
-  ];
 
   const headerMeta: PageLayoutProps['headerMeta'] = {
     publication: { date: dates.publication },
@@ -152,7 +141,8 @@ const ThematicPage: NextPage<ThematicPageProps> = ({
         dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
       />
       <PageLayout
-        breadcrumb={breadcrumb}
+        breadcrumb={breadcrumbItems}
+        breadcrumbSchema={breadcrumbSchema}
         title={title}
         intro={intro}
         headerMeta={headerMeta}
diff --git a/src/utils/hooks/use-breadcrumb.tsx b/src/utils/hooks/use-breadcrumb.tsx
new file mode 100644
index 0000000..087d400
--- /dev/null
+++ b/src/utils/hooks/use-breadcrumb.tsx
@@ -0,0 +1,106 @@
+import { BreadcrumbItem } from '@components/molecules/nav/breadcrumb';
+import { slugify } from '@utils/helpers/strings';
+import { useIntl } from 'react-intl';
+import { BreadcrumbList } from 'schema-dts';
+import useSettings from './use-settings';
+
+export type useBreadcrumbProps = {
+  /**
+   * The current page title.
+   */
+  title: string;
+  /**
+   * The current page url.
+   */
+  url: string;
+};
+
+export type useBreadcrumbReturn = {
+  /**
+   * The breadcrumb items.
+   */
+  items: BreadcrumbItem[];
+  /**
+   * The breadcrumb JSON schema.
+   */
+  schema: BreadcrumbList['itemListElement'][];
+};
+
+/**
+ * Retrieve the breadcrumb items.
+ *
+ * @param {useBreadcrumbProps} props - An object (the current page title & url).
+ * @returns {useBreadcrumbReturn} The breadcrumb items and its JSON schema.
+ */
+const useBreadcrumb = ({
+  title,
+  url,
+}: useBreadcrumbProps): useBreadcrumbReturn => {
+  const intl = useIntl();
+  const { website } = useSettings();
+  const isArticle = url.startsWith('/article/');
+  const isHome = url === '/';
+  const isProject = url.startsWith('/projets/');
+  const isSearch = url.startsWith('/recherche');
+  const isThematic = url.startsWith('/thematique/');
+  const isTopic = url.startsWith('/sujet/');
+
+  const homeLabel = intl.formatMessage({
+    defaultMessage: 'Home',
+    description: 'Breadcrumb: home label',
+    id: 'j5k9Fe',
+  });
+  const items: BreadcrumbItem[] = [{ id: 'home', name: homeLabel, url: '/' }];
+  const schema: BreadcrumbList['itemListElement'][] = [
+    {
+      '@type': 'ListItem',
+      position: 1,
+      name: homeLabel,
+      item: website.url,
+    },
+  ];
+
+  if (isHome) return { items, schema };
+
+  if (isArticle || isSearch || isThematic || isTopic) {
+    const blogLabel = intl.formatMessage({
+      defaultMessage: 'Blog',
+      description: 'Breadcrumb: blog label',
+      id: 'Es52wh',
+    });
+    items.push({ id: 'blog', name: blogLabel, url: '/blog' });
+    schema.push({
+      '@type': 'ListItem',
+      position: 2,
+      name: blogLabel,
+      item: `${website.url}/blog`,
+    });
+  }
+
+  if (isProject) {
+    const projectsLabel = intl.formatMessage({
+      defaultMessage: 'Projects',
+      description: 'Breadcrumb: projects label',
+      id: '28GZdv',
+    });
+    items.push({ id: 'blog', name: projectsLabel, url: '/projets' });
+    schema.push({
+      '@type': 'ListItem',
+      position: 2,
+      name: projectsLabel,
+      item: `${website.url}/projets`,
+    });
+  }
+
+  items.push({ id: slugify(title), name: title, url });
+  schema.push({
+    '@type': 'ListItem',
+    position: schema.length + 1,
+    name: title,
+    item: `${website.url}${url}`,
+  });
+
+  return { items, schema };
+};
+
+export default useBreadcrumb;
-- 
cgit v1.2.3


From 4581c262ca06704baaa3c0a172d509207f41f5c3 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Sat, 21 May 2022 18:42:19 +0200
Subject: chore: complete Storybook stories

---
 .../atoms/buttons/button-link.stories.tsx          | 14 ++++++
 src/components/atoms/icons/cc-by-sa.stories.tsx    |  8 ----
 src/components/atoms/layout/copyright.stories.tsx  |  8 ----
 src/components/atoms/layout/sidebar.stories.tsx    |  8 ----
 .../atoms/links/sharing-link.stories.tsx           |  5 +--
 .../atoms/lists/description-list-item.stories.tsx  |  2 +-
 src/components/atoms/lists/list.stories.tsx        | 13 ++++++
 src/components/atoms/loaders/spinner.stories.tsx   |  8 ----
 .../molecules/buttons/back-to-top.stories.tsx      |  8 ----
 .../molecules/buttons/heading-button.stories.tsx   |  1 +
 .../molecules/buttons/help-button.stories.tsx      |  8 ----
 src/components/molecules/buttons/help-button.tsx   | 11 ++---
 .../molecules/forms/labelled-field.stories.tsx     |  1 +
 .../forms/select-with-tooltip.stories.tsx          |  8 ----
 .../molecules/images/responsive-image.stories.tsx  | 13 ++++++
 .../molecules/layout/branding.stories.tsx          |  9 +---
 src/components/molecules/layout/code.stories.tsx   |  8 ----
 src/components/molecules/layout/code.tsx           | 36 ++--------------
 src/components/molecules/layout/meta.stories.tsx   | 34 +++++++--------
 src/components/molecules/layout/widget.stories.tsx | 50 +++++++++++++---------
 src/components/molecules/modals/modal.stories.tsx  | 13 ++++++
 .../molecules/nav/breadcrumb.stories.tsx           |  8 ----
 src/components/molecules/nav/nav.stories.tsx       |  8 ----
 .../molecules/nav/pagination.stories.tsx           | 12 ++----
 .../organisms/forms/comment-form.stories.tsx       | 21 +++++++++
 .../organisms/forms/contact-form.stories.tsx       | 11 ++---
 .../organisms/forms/search-form.stories.tsx        | 10 +++++
 .../organisms/forms/settings-form.stories.tsx      | 20 +++++++++
 .../organisms/layout/comment.stories.tsx           |  1 +
 .../organisms/layout/comments-list.stories.tsx     |  4 +-
 src/components/organisms/layout/header.stories.tsx | 10 +++++
 .../organisms/layout/posts-list.stories.tsx        | 44 +++++++++++++++++++
 .../organisms/modals/search-modal.stories.tsx      | 10 +++++
 .../organisms/modals/settings-modal.stories.tsx    | 20 +++++++++
 .../organisms/toolbar/main-nav.stories.tsx         | 11 ++---
 .../organisms/toolbar/search.stories.tsx           | 13 ++++++
 .../organisms/toolbar/settings.stories.tsx         |  3 ++
 src/components/organisms/toolbar/settings.tsx      |  9 +---
 .../organisms/toolbar/toolbar.stories.tsx          | 10 +++++
 .../organisms/widgets/image-widget.stories.tsx     | 21 +++++----
 .../widgets/links-list-widget.stories.tsx          | 21 +++++----
 .../organisms/widgets/social-media.stories.tsx     |  8 ----
 .../widgets/table-of-contents.stories.tsx          |  8 ----
 src/components/templates/layout/layout.stories.tsx | 26 +++++++++++
 .../templates/page/page-layout.stories.tsx         | 24 +++++++++++
 45 files changed, 362 insertions(+), 237 deletions(-)

(limited to 'src/components/templates/layout/layout.stories.tsx')

diff --git a/src/components/atoms/buttons/button-link.stories.tsx b/src/components/atoms/buttons/button-link.stories.tsx
index 2e1c040..d06aff3 100644
--- a/src/components/atoms/buttons/button-link.stories.tsx
+++ b/src/components/atoms/buttons/button-link.stories.tsx
@@ -47,6 +47,20 @@ export default {
         required: false,
       },
     },
+    external: {
+      control: {
+        type: 'boolean',
+      },
+      description: 'Determine if the link is an external link.',
+      table: {
+        category: 'Options',
+        defaultValue: { summary: false },
+      },
+      type: {
+        name: 'boolean',
+        required: false,
+      },
+    },
     kind: {
       control: {
         type: 'select',
diff --git a/src/components/atoms/icons/cc-by-sa.stories.tsx b/src/components/atoms/icons/cc-by-sa.stories.tsx
index f2bc8f0..4229725 100644
--- a/src/components/atoms/icons/cc-by-sa.stories.tsx
+++ b/src/components/atoms/icons/cc-by-sa.stories.tsx
@@ -1,5 +1,4 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import CCBySAIcon from './cc-by-sa';
 
 /**
@@ -23,13 +22,6 @@ export default {
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof CCBySAIcon>;
 
 const Template: ComponentStory<typeof CCBySAIcon> = (args) => (
diff --git a/src/components/atoms/layout/copyright.stories.tsx b/src/components/atoms/layout/copyright.stories.tsx
index b988165..612b114 100644
--- a/src/components/atoms/layout/copyright.stories.tsx
+++ b/src/components/atoms/layout/copyright.stories.tsx
@@ -1,6 +1,5 @@
 import CCBySA from '@components/atoms/icons/cc-by-sa';
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import CopyrightComponent from './copyright';
 
 /**
@@ -39,13 +38,6 @@ export default {
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof CopyrightComponent>;
 
 const Template: ComponentStory<typeof CopyrightComponent> = (args) => (
diff --git a/src/components/atoms/layout/sidebar.stories.tsx b/src/components/atoms/layout/sidebar.stories.tsx
index 175af94..6876f95 100644
--- a/src/components/atoms/layout/sidebar.stories.tsx
+++ b/src/components/atoms/layout/sidebar.stories.tsx
@@ -1,5 +1,4 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import SidebarComponent from './sidebar';
 
 /**
@@ -46,13 +45,6 @@ export default {
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof SidebarComponent>;
 
 const Template: ComponentStory<typeof SidebarComponent> = (args) => (
diff --git a/src/components/atoms/links/sharing-link.stories.tsx b/src/components/atoms/links/sharing-link.stories.tsx
index c91e938..e6bd11b 100644
--- a/src/components/atoms/links/sharing-link.stories.tsx
+++ b/src/components/atoms/links/sharing-link.stories.tsx
@@ -1,5 +1,4 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import SharingLinkComponent from './sharing-link';
 
 /**
@@ -41,9 +40,7 @@ export default {
 } as ComponentMeta<typeof SharingLinkComponent>;
 
 const Template: ComponentStory<typeof SharingLinkComponent> = (args) => (
-  <IntlProvider locale="en">
-    <SharingLinkComponent {...args} />
-  </IntlProvider>
+  <SharingLinkComponent {...args} />
 );
 
 /**
diff --git a/src/components/atoms/lists/description-list-item.stories.tsx b/src/components/atoms/lists/description-list-item.stories.tsx
index e05493c..c7beb0d 100644
--- a/src/components/atoms/lists/description-list-item.stories.tsx
+++ b/src/components/atoms/lists/description-list-item.stories.tsx
@@ -52,7 +52,7 @@ export default {
       description: 'The item layout.',
       options: ['inline', 'inline-values', 'stacked'],
       table: {
-        category: 'Styles',
+        category: 'Options',
         defaultValue: { summary: 'stacked' },
       },
       type: {
diff --git a/src/components/atoms/lists/list.stories.tsx b/src/components/atoms/lists/list.stories.tsx
index 54fdd3a..eac3cd3 100644
--- a/src/components/atoms/lists/list.stories.tsx
+++ b/src/components/atoms/lists/list.stories.tsx
@@ -35,6 +35,19 @@ export default {
         value: {},
       },
     },
+    itemsClassName: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the list items.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     kind: {
       control: {
         type: 'select',
diff --git a/src/components/atoms/loaders/spinner.stories.tsx b/src/components/atoms/loaders/spinner.stories.tsx
index dc577dc..1792c6c 100644
--- a/src/components/atoms/loaders/spinner.stories.tsx
+++ b/src/components/atoms/loaders/spinner.stories.tsx
@@ -1,5 +1,4 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import SpinnerComponent from './spinner';
 
 /**
@@ -23,13 +22,6 @@ export default {
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof SpinnerComponent>;
 
 const Template: ComponentStory<typeof SpinnerComponent> = (args) => (
diff --git a/src/components/molecules/buttons/back-to-top.stories.tsx b/src/components/molecules/buttons/back-to-top.stories.tsx
index 7d4bc39..a338b8b 100644
--- a/src/components/molecules/buttons/back-to-top.stories.tsx
+++ b/src/components/molecules/buttons/back-to-top.stories.tsx
@@ -1,5 +1,4 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import BackToTopComponent from './back-to-top';
 
 /**
@@ -33,13 +32,6 @@ export default {
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof BackToTopComponent>;
 
 const Template: ComponentStory<typeof BackToTopComponent> = (args) => (
diff --git a/src/components/molecules/buttons/heading-button.stories.tsx b/src/components/molecules/buttons/heading-button.stories.tsx
index d1b5ed4..59f7be9 100644
--- a/src/components/molecules/buttons/heading-button.stories.tsx
+++ b/src/components/molecules/buttons/heading-button.stories.tsx
@@ -1,3 +1,4 @@
+import headingStories from '@components/atoms/headings/heading.stories';
 import { ComponentMeta, ComponentStory } from '@storybook/react';
 import { useState } from 'react';
 import HeadingButtonComponent from './heading-button';
diff --git a/src/components/molecules/buttons/help-button.stories.tsx b/src/components/molecules/buttons/help-button.stories.tsx
index cfc603e..4968b27 100644
--- a/src/components/molecules/buttons/help-button.stories.tsx
+++ b/src/components/molecules/buttons/help-button.stories.tsx
@@ -1,5 +1,4 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import HelpButtonComponent from './help-button';
 
 /**
@@ -36,13 +35,6 @@ export default {
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof HelpButtonComponent>;
 
 const Template: ComponentStory<typeof HelpButtonComponent> = (args) => (
diff --git a/src/components/molecules/buttons/help-button.tsx b/src/components/molecules/buttons/help-button.tsx
index e351453..ccf1ebd 100644
--- a/src/components/molecules/buttons/help-button.tsx
+++ b/src/components/molecules/buttons/help-button.tsx
@@ -3,12 +3,7 @@ import { forwardRef, ForwardRefRenderFunction } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './help-button.module.scss';
 
-export type HelpButtonProps = Pick<ButtonProps, 'onClick'> & {
-  /**
-   * Set additional classnames to the button wrapper.
-   */
-  className?: string;
-};
+export type HelpButtonProps = Pick<ButtonProps, 'className' | 'onClick'>;
 
 /**
  * HelpButton component
@@ -18,7 +13,7 @@ export type HelpButtonProps = Pick<ButtonProps, 'onClick'> & {
 const HelpButton: ForwardRefRenderFunction<
   HTMLButtonElement,
   HelpButtonProps
-> = ({ className = '', onClick }, ref) => {
+> = ({ className = '', ...props }, ref) => {
   const intl = useIntl();
   const text = intl.formatMessage({
     defaultMessage: 'Help',
@@ -29,9 +24,9 @@ const HelpButton: ForwardRefRenderFunction<
   return (
     <Button
       className={`${styles.btn} ${className}`}
-      onClick={onClick}
       ref={ref}
       shape="circle"
+      {...props}
     >
       <span className="screen-reader-text">{text}</span>
       <span className={styles.icon}>?</span>
diff --git a/src/components/molecules/forms/labelled-field.stories.tsx b/src/components/molecules/forms/labelled-field.stories.tsx
index 56eef00..795e785 100644
--- a/src/components/molecules/forms/labelled-field.stories.tsx
+++ b/src/components/molecules/forms/labelled-field.stories.tsx
@@ -10,6 +10,7 @@ export default {
   component: LabelledField,
   args: {
     disabled: false,
+    hideLabel: false,
     labelPosition: 'top',
     required: false,
   },
diff --git a/src/components/molecules/forms/select-with-tooltip.stories.tsx b/src/components/molecules/forms/select-with-tooltip.stories.tsx
index d757b2b..ddf5d4c 100644
--- a/src/components/molecules/forms/select-with-tooltip.stories.tsx
+++ b/src/components/molecules/forms/select-with-tooltip.stories.tsx
@@ -1,6 +1,5 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
 import { useState } from 'react';
-import { IntlProvider } from 'react-intl';
 import SelectWithTooltip from './select-with-tooltip';
 
 /**
@@ -178,13 +177,6 @@ export default {
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof SelectWithTooltip>;
 
 const selectOptions = [
diff --git a/src/components/molecules/images/responsive-image.stories.tsx b/src/components/molecules/images/responsive-image.stories.tsx
index 4294208..ca69d4f 100644
--- a/src/components/molecules/images/responsive-image.stories.tsx
+++ b/src/components/molecules/images/responsive-image.stories.tsx
@@ -34,6 +34,19 @@ export default {
         required: false,
       },
     },
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the image wrapper.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     height: {
       control: {
         type: 'number',
diff --git a/src/components/molecules/layout/branding.stories.tsx b/src/components/molecules/layout/branding.stories.tsx
index 1637c99..d2e0694 100644
--- a/src/components/molecules/layout/branding.stories.tsx
+++ b/src/components/molecules/layout/branding.stories.tsx
@@ -1,5 +1,4 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import Branding from './branding';
 
 /**
@@ -10,6 +9,7 @@ export default {
   component: Branding,
   args: {
     isHome: false,
+    withLink: false,
   },
   argTypes: {
     baseline: {
@@ -72,13 +72,6 @@ export default {
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof Branding>;
 
 const Template: ComponentStory<typeof Branding> = (args) => (
diff --git a/src/components/molecules/layout/code.stories.tsx b/src/components/molecules/layout/code.stories.tsx
index a2a6b2c..ac0e98f 100644
--- a/src/components/molecules/layout/code.stories.tsx
+++ b/src/components/molecules/layout/code.stories.tsx
@@ -1,5 +1,4 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import CodeComponent from './code';
 
 /**
@@ -70,13 +69,6 @@ export default {
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof CodeComponent>;
 
 const Template: ComponentStory<typeof CodeComponent> = (args) => (
diff --git a/src/components/molecules/layout/code.tsx b/src/components/molecules/layout/code.tsx
index f5b60b9..30351b9 100644
--- a/src/components/molecules/layout/code.tsx
+++ b/src/components/molecules/layout/code.tsx
@@ -1,38 +1,10 @@
-import usePrism, { OptionalPrismPlugin } from '@utils/hooks/use-prism';
+import usePrism, {
+  type OptionalPrismPlugin,
+  type PrismLanguage,
+} from '@utils/hooks/use-prism';
 import { FC, useRef } from 'react';
 import styles from './code.module.scss';
 
-export type PrismLanguage =
-  | 'apacheconf'
-  | 'bash'
-  | 'css'
-  | 'diff'
-  | 'docker'
-  | 'editorconfig'
-  | 'ejs'
-  | 'git'
-  | 'graphql'
-  | 'html'
-  | 'ignore'
-  | 'ini'
-  | 'javascript'
-  | 'jsdoc'
-  | 'json'
-  | 'jsx'
-  | 'makefile'
-  | 'markup'
-  | 'php'
-  | 'phpdoc'
-  | 'regex'
-  | 'scss'
-  | 'shell-session'
-  | 'smarty'
-  | 'tcl'
-  | 'toml'
-  | 'tsx'
-  | 'twig'
-  | 'yaml';
-
 export type CodeProps = {
   /**
    * The code to highlight.
diff --git a/src/components/molecules/layout/meta.stories.tsx b/src/components/molecules/layout/meta.stories.tsx
index a1755a0..c33680f 100644
--- a/src/components/molecules/layout/meta.stories.tsx
+++ b/src/components/molecules/layout/meta.stories.tsx
@@ -1,3 +1,5 @@
+import descriptionListItemStories from '@components/atoms/lists/description-list-item.stories';
+import descriptionListStories from '@components/atoms/lists/description-list.stories';
 import { ComponentMeta, ComponentStory } from '@storybook/react';
 import MetaComponent, { MetaData } from './meta';
 
@@ -7,7 +9,12 @@ import MetaComponent, { MetaData } from './meta';
 export default {
   title: 'Molecules/Layout',
   component: MetaComponent,
+  args: {
+    itemsLayout: 'inline-values',
+    withSeparator: false,
+  },
   argTypes: {
+    className: descriptionListStories.argTypes?.className,
     data: {
       description: 'The page metadata.',
       type: {
@@ -16,34 +23,23 @@ export default {
         value: {},
       },
     },
+    groupClassName: descriptionListStories.argTypes?.groupClassName,
     itemsLayout: {
-      control: {
-        type: 'select',
-      },
-      description: 'The items layout.',
-      options: ['inline', 'inline-values', 'stacked'],
+      ...descriptionListItemStories.argTypes?.layout,
       table: {
-        category: 'Options',
+        ...descriptionListItemStories.argTypes?.layout?.table,
         defaultValue: { summary: 'inline-values' },
       },
-      type: {
-        name: 'string',
-        required: false,
-      },
     },
+    labelClassName: descriptionListStories.argTypes?.labelClassName,
+    layout: descriptionListStories.argTypes?.layout,
+    valueClassName: descriptionListStories.argTypes?.valueClassName,
     withSeparator: {
-      control: {
-        type: 'boolean',
-      },
-      description: 'Add a slash as separator between multiple values.',
+      ...descriptionListStories.argTypes?.withSeparator,
       table: {
-        category: 'Options',
+        ...descriptionListStories.argTypes?.withSeparator?.table,
         defaultValue: { summary: true },
       },
-      type: {
-        name: 'boolean',
-        required: true,
-      },
     },
   },
 } as ComponentMeta<typeof MetaComponent>;
diff --git a/src/components/molecules/layout/widget.stories.tsx b/src/components/molecules/layout/widget.stories.tsx
index c113db9..dd5a30b 100644
--- a/src/components/molecules/layout/widget.stories.tsx
+++ b/src/components/molecules/layout/widget.stories.tsx
@@ -1,5 +1,5 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
+import headingButtonStories from '../buttons/heading-button.stories';
 import Widget from './widget';
 
 /**
@@ -10,6 +10,7 @@ export default {
   component: Widget,
   args: {
     withBorders: false,
+    withScroll: false,
   },
   argTypes: {
     children: {
@@ -22,6 +23,19 @@ export default {
         required: true,
       },
     },
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the widget wrapper.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     expanded: {
       control: {
         type: 'boolean',
@@ -36,18 +50,7 @@ export default {
         required: false,
       },
     },
-    level: {
-      control: {
-        type: 'number',
-        min: 1,
-        max: 6,
-      },
-      description: 'The heading level.',
-      type: {
-        name: 'number',
-        required: true,
-      },
-    },
+    level: headingButtonStories.argTypes?.level,
     title: {
       control: {
         type: 'text',
@@ -72,14 +75,21 @@ export default {
         required: false,
       },
     },
+    withScroll: {
+      control: {
+        type: 'boolean',
+      },
+      description: 'Define if the widget should be scrollable',
+      table: {
+        category: 'Options',
+        defaultValue: { summary: false },
+      },
+      type: {
+        name: 'boolean',
+        required: false,
+      },
+    },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof Widget>;
 
 const Template: ComponentStory<typeof Widget> = (args) => <Widget {...args} />;
diff --git a/src/components/molecules/modals/modal.stories.tsx b/src/components/molecules/modals/modal.stories.tsx
index 55b7677..f6dd364 100644
--- a/src/components/molecules/modals/modal.stories.tsx
+++ b/src/components/molecules/modals/modal.stories.tsx
@@ -31,6 +31,19 @@ export default {
         required: false,
       },
     },
+    headingClassName: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the modal heading.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     icon: {
       control: {
         type: 'select',
diff --git a/src/components/molecules/nav/breadcrumb.stories.tsx b/src/components/molecules/nav/breadcrumb.stories.tsx
index e26b480..cf67e60 100644
--- a/src/components/molecules/nav/breadcrumb.stories.tsx
+++ b/src/components/molecules/nav/breadcrumb.stories.tsx
@@ -1,5 +1,4 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import Breadcrumb from './breadcrumb';
 
 /**
@@ -44,13 +43,6 @@ export default {
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof Breadcrumb>;
 
 const Template: ComponentStory<typeof Breadcrumb> = (args) => (
diff --git a/src/components/molecules/nav/nav.stories.tsx b/src/components/molecules/nav/nav.stories.tsx
index 5cef5f0..f3a29a6 100644
--- a/src/components/molecules/nav/nav.stories.tsx
+++ b/src/components/molecules/nav/nav.stories.tsx
@@ -1,7 +1,6 @@
 import Envelop from '@components/atoms/icons/envelop';
 import Home from '@components/atoms/icons/home';
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import NavComponent, { type NavItem } from './nav';
 
 /**
@@ -73,13 +72,6 @@ export default {
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof NavComponent>;
 
 const Template: ComponentStory<typeof NavComponent> = (args) => (
diff --git a/src/components/molecules/nav/pagination.stories.tsx b/src/components/molecules/nav/pagination.stories.tsx
index b31c2b5..2e86db4 100644
--- a/src/components/molecules/nav/pagination.stories.tsx
+++ b/src/components/molecules/nav/pagination.stories.tsx
@@ -1,5 +1,4 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import PaginationComponent from './pagination';
 
 /**
@@ -8,6 +7,10 @@ import PaginationComponent from './pagination';
 export default {
   title: 'Molecules/Navigation/Pagination',
   component: PaginationComponent,
+  args: {
+    baseUrl: '/page/',
+    siblings: 1,
+  },
   argTypes: {
     'aria-label': {
       control: {
@@ -95,13 +98,6 @@ export default {
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof PaginationComponent>;
 
 const Template: ComponentStory<typeof PaginationComponent> = (args) => (
diff --git a/src/components/organisms/forms/comment-form.stories.tsx b/src/components/organisms/forms/comment-form.stories.tsx
index 8b11df7..1a9e7b7 100644
--- a/src/components/organisms/forms/comment-form.stories.tsx
+++ b/src/components/organisms/forms/comment-form.stories.tsx
@@ -13,6 +13,8 @@ export default {
   component: CommentForm,
   args: {
     saveComment,
+    titleAlignment: 'left',
+    titleLevel: 2,
   },
   argTypes: {
     className: {
@@ -56,6 +58,9 @@ export default {
         type: null,
       },
       description: 'A callback function to process the comment form data.',
+      table: {
+        category: 'Events',
+      },
       type: {
         name: 'function',
         required: true,
@@ -74,6 +79,21 @@ export default {
         required: false,
       },
     },
+    titleAlignment: {
+      control: {
+        type: 'select',
+      },
+      description: 'The heading alignment.',
+      options: ['center', 'left'],
+      table: {
+        category: 'Options',
+        defaultValue: { summary: 'left' },
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     titleLevel: {
       control: {
         type: 'number',
@@ -83,6 +103,7 @@ export default {
       description: 'The title level (hn).',
       table: {
         category: 'Options',
+        defaultValue: { summary: 2 },
       },
       type: {
         name: 'number',
diff --git a/src/components/organisms/forms/contact-form.stories.tsx b/src/components/organisms/forms/contact-form.stories.tsx
index 39d0b71..191d448 100644
--- a/src/components/organisms/forms/contact-form.stories.tsx
+++ b/src/components/organisms/forms/contact-form.stories.tsx
@@ -1,5 +1,4 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import ContactForm from './contact-form';
 
 /**
@@ -40,19 +39,15 @@ export default {
         type: null,
       },
       description: 'A callback function to process the contact form data.',
+      table: {
+        category: 'Events',
+      },
       type: {
         name: 'function',
         required: true,
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof ContactForm>;
 
 const Template: ComponentStory<typeof ContactForm> = (args) => (
diff --git a/src/components/organisms/forms/search-form.stories.tsx b/src/components/organisms/forms/search-form.stories.tsx
index 6ea6122..d8c8e1e 100644
--- a/src/components/organisms/forms/search-form.stories.tsx
+++ b/src/components/organisms/forms/search-form.stories.tsx
@@ -39,6 +39,16 @@ export default {
         required: false,
       },
     },
+    searchPage: {
+      control: {
+        type: 'text',
+      },
+      description: 'The search results page url.',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
   },
 } as ComponentMeta<typeof SearchForm>;
 
diff --git a/src/components/organisms/forms/settings-form.stories.tsx b/src/components/organisms/forms/settings-form.stories.tsx
index 46305e7..70e1844 100644
--- a/src/components/organisms/forms/settings-form.stories.tsx
+++ b/src/components/organisms/forms/settings-form.stories.tsx
@@ -8,6 +8,16 @@ export default {
   title: 'Organisms/Forms',
   component: SettingsForm,
   argTypes: {
+    ackeeStorageKey: {
+      control: {
+        type: 'text',
+      },
+      description: 'The local storage key for Ackee setting.',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
     className: {
       control: {
         type: 'text',
@@ -21,6 +31,16 @@ export default {
         required: false,
       },
     },
+    motionStorageKey: {
+      control: {
+        type: 'text',
+      },
+      description: 'The local storage key for reduced motion setting.',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
     tooltipClassName: {
       control: {
         type: 'text',
diff --git a/src/components/organisms/layout/comment.stories.tsx b/src/components/organisms/layout/comment.stories.tsx
index c31b77a..c7649ab 100644
--- a/src/components/organisms/layout/comment.stories.tsx
+++ b/src/components/organisms/layout/comment.stories.tsx
@@ -12,6 +12,7 @@ export default {
   title: 'Organisms/Layout',
   component: CommentComponent,
   args: {
+    canReply: true,
     saveComment,
   },
   argTypes: {
diff --git a/src/components/organisms/layout/comments-list.stories.tsx b/src/components/organisms/layout/comments-list.stories.tsx
index 4d95205..1ed159a 100644
--- a/src/components/organisms/layout/comments-list.stories.tsx
+++ b/src/components/organisms/layout/comments-list.stories.tsx
@@ -1,5 +1,5 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import CommentsListComponent, { Comment } from './comments-list';
+import CommentsListComponent, { type Comment } from './comments-list';
 
 const saveComment = async () => {
   /** Do nothing. */
@@ -29,6 +29,8 @@ export default {
     depth: {
       control: {
         type: 'number',
+        min: 0,
+        max: 4,
       },
       description: 'The maximum depth. Use `0` to not display nested comments.',
       type: {
diff --git a/src/components/organisms/layout/header.stories.tsx b/src/components/organisms/layout/header.stories.tsx
index 3ceb337..a5233ea 100644
--- a/src/components/organisms/layout/header.stories.tsx
+++ b/src/components/organisms/layout/header.stories.tsx
@@ -92,6 +92,16 @@ export default {
         required: true,
       },
     },
+    searchPage: {
+      control: {
+        type: 'text',
+      },
+      description: 'The search results page url.',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
     title: {
       control: {
         type: 'text',
diff --git a/src/components/organisms/layout/posts-list.stories.tsx b/src/components/organisms/layout/posts-list.stories.tsx
index 77318f4..96fb2f0 100644
--- a/src/components/organisms/layout/posts-list.stories.tsx
+++ b/src/components/organisms/layout/posts-list.stories.tsx
@@ -10,10 +10,26 @@ export default {
   args: {
     byYear: false,
     isLoading: false,
+    pageNumber: 1,
     showLoadMoreBtn: false,
+    siblings: 1,
     titleLevel: 2,
   },
   argTypes: {
+    baseUrl: {
+      control: {
+        type: 'text',
+      },
+      description: 'The pagination base url.',
+      table: {
+        category: 'Options',
+        defaultValue: { summary: '/page/' },
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     byYear: {
       control: {
         type: 'boolean',
@@ -55,6 +71,20 @@ export default {
         required: false,
       },
     },
+    pageNumber: {
+      control: {
+        type: 'number',
+      },
+      description: 'The current page number.',
+      table: {
+        category: 'Options',
+        defaultValue: { summary: 1 },
+      },
+      type: {
+        name: 'number',
+        required: false,
+      },
+    },
     posts: {
       description: 'The posts data.',
       type: {
@@ -77,6 +107,20 @@ export default {
         required: false,
       },
     },
+    siblings: {
+      control: {
+        type: 'number',
+      },
+      description: 'The number of page siblings inside pagination.',
+      table: {
+        category: 'Options',
+        defaultValue: { summary: 1 },
+      },
+      type: {
+        name: 'number',
+        required: false,
+      },
+    },
     titleLevel: {
       control: {
         type: 'number',
diff --git a/src/components/organisms/modals/search-modal.stories.tsx b/src/components/organisms/modals/search-modal.stories.tsx
index f40696c..5a607c6 100644
--- a/src/components/organisms/modals/search-modal.stories.tsx
+++ b/src/components/organisms/modals/search-modal.stories.tsx
@@ -24,6 +24,16 @@ export default {
         required: false,
       },
     },
+    searchPage: {
+      control: {
+        type: 'text',
+      },
+      description: 'The search results page url.',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
   },
 } as ComponentMeta<typeof SearchModal>;
 
diff --git a/src/components/organisms/modals/settings-modal.stories.tsx b/src/components/organisms/modals/settings-modal.stories.tsx
index 0fe8c18..d263e2b 100644
--- a/src/components/organisms/modals/settings-modal.stories.tsx
+++ b/src/components/organisms/modals/settings-modal.stories.tsx
@@ -8,6 +8,16 @@ export default {
   title: 'Organisms/Modals',
   component: SettingsModal,
   argTypes: {
+    ackeeStorageKey: {
+      control: {
+        type: 'text',
+      },
+      description: 'A local storage key for Ackee.',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
     className: {
       control: {
         type: 'text',
@@ -21,6 +31,16 @@ export default {
         required: false,
       },
     },
+    motionStorageKey: {
+      control: {
+        type: 'text',
+      },
+      description: 'A local storage key for reduced motion setting..',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
     tooltipClassName: {
       control: {
         type: 'text',
diff --git a/src/components/organisms/toolbar/main-nav.stories.tsx b/src/components/organisms/toolbar/main-nav.stories.tsx
index 04b22cb..831636f 100644
--- a/src/components/organisms/toolbar/main-nav.stories.tsx
+++ b/src/components/organisms/toolbar/main-nav.stories.tsx
@@ -1,6 +1,5 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
 import { useState } from 'react';
-import { IntlProvider } from 'react-intl';
 import MainNav from './main-nav';
 
 /**
@@ -46,19 +45,15 @@ export default {
         type: null,
       },
       description: 'A callback function to change main nav state.',
+      table: {
+        category: 'Events',
+      },
       type: {
         name: 'function',
         required: true,
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof MainNav>;
 
 const Template: ComponentStory<typeof MainNav> = ({
diff --git a/src/components/organisms/toolbar/search.stories.tsx b/src/components/organisms/toolbar/search.stories.tsx
index 6aaffde..f0f65b4 100644
--- a/src/components/organisms/toolbar/search.stories.tsx
+++ b/src/components/organisms/toolbar/search.stories.tsx
@@ -35,11 +35,24 @@ export default {
         required: true,
       },
     },
+    searchPage: {
+      control: {
+        type: 'text',
+      },
+      description: 'The search results page url.',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
     setIsActive: {
       control: {
         type: null,
       },
       description: 'A callback function to update modal state.',
+      table: {
+        category: 'Events',
+      },
       type: {
         name: 'function',
         required: true,
diff --git a/src/components/organisms/toolbar/settings.stories.tsx b/src/components/organisms/toolbar/settings.stories.tsx
index 9d04932..08ec579 100644
--- a/src/components/organisms/toolbar/settings.stories.tsx
+++ b/src/components/organisms/toolbar/settings.stories.tsx
@@ -41,6 +41,9 @@ export default {
         type: null,
       },
       description: 'Define the modal state: either opened or closed.',
+      table: {
+        category: 'Events',
+      },
       type: {
         name: 'boolean',
         required: true,
diff --git a/src/components/organisms/toolbar/settings.tsx b/src/components/organisms/toolbar/settings.tsx
index 9985ba0..f558eb7 100644
--- a/src/components/organisms/toolbar/settings.tsx
+++ b/src/components/organisms/toolbar/settings.tsx
@@ -9,14 +9,7 @@ import SettingsModal, {
 import settingsStyles from './settings.module.scss';
 import sharedStyles from './toolbar-items.module.scss';
 
-export type SettingsProps = Pick<
-  SettingsModalProps,
-  'ackeeStorageKey' | 'motionStorageKey' | 'tooltipClassName'
-> & {
-  /**
-   * Set additional classnames to the modal wrapper.
-   */
-  className?: string;
+export type SettingsProps = SettingsModalProps & {
   /**
    * The button state.
    */
diff --git a/src/components/organisms/toolbar/toolbar.stories.tsx b/src/components/organisms/toolbar/toolbar.stories.tsx
index 1f07e79..d613442 100644
--- a/src/components/organisms/toolbar/toolbar.stories.tsx
+++ b/src/components/organisms/toolbar/toolbar.stories.tsx
@@ -54,6 +54,16 @@ export default {
         value: {},
       },
     },
+    searchPage: {
+      control: {
+        type: 'text',
+      },
+      description: 'The search results page url.',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
   },
   parameters: {
     layout: 'fullscreen',
diff --git a/src/components/organisms/widgets/image-widget.stories.tsx b/src/components/organisms/widgets/image-widget.stories.tsx
index 3014b36..fa39745 100644
--- a/src/components/organisms/widgets/image-widget.stories.tsx
+++ b/src/components/organisms/widgets/image-widget.stories.tsx
@@ -1,5 +1,4 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import ImageWidget from './image-widget';
 
 /**
@@ -27,6 +26,19 @@ export default {
         required: false,
       },
     },
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the widget wrapper.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     description: {
       control: {
         type: 'text',
@@ -107,13 +119,6 @@ export default {
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof ImageWidget>;
 
 const Template: ComponentStory<typeof ImageWidget> = (args) => (
diff --git a/src/components/organisms/widgets/links-list-widget.stories.tsx b/src/components/organisms/widgets/links-list-widget.stories.tsx
index 0f03a2c..cdfa96a 100644
--- a/src/components/organisms/widgets/links-list-widget.stories.tsx
+++ b/src/components/organisms/widgets/links-list-widget.stories.tsx
@@ -1,5 +1,4 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import LinksListWidget from './links-list-widget';
 
 /**
@@ -12,6 +11,19 @@ export default {
     kind: 'unordered',
   },
   argTypes: {
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the list wrapper.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     items: {
       description: 'The widget data.',
       type: {
@@ -58,13 +70,6 @@ export default {
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof LinksListWidget>;
 
 const Template: ComponentStory<typeof LinksListWidget> = (args) => (
diff --git a/src/components/organisms/widgets/social-media.stories.tsx b/src/components/organisms/widgets/social-media.stories.tsx
index 2fa8bd7..6c9de4d 100644
--- a/src/components/organisms/widgets/social-media.stories.tsx
+++ b/src/components/organisms/widgets/social-media.stories.tsx
@@ -1,5 +1,4 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import SocialMediaWidget, { Media } from './social-media';
 
 /**
@@ -40,13 +39,6 @@ export default {
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof SocialMediaWidget>;
 
 const Template: ComponentStory<typeof SocialMediaWidget> = (args) => (
diff --git a/src/components/organisms/widgets/table-of-contents.stories.tsx b/src/components/organisms/widgets/table-of-contents.stories.tsx
index fba7c54..d8b3af7 100644
--- a/src/components/organisms/widgets/table-of-contents.stories.tsx
+++ b/src/components/organisms/widgets/table-of-contents.stories.tsx
@@ -1,5 +1,4 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import ToCWidget from './table-of-contents';
 
 /**
@@ -21,13 +20,6 @@ export default {
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof ToCWidget>;
 
 const Template: ComponentStory<typeof ToCWidget> = (args) => (
diff --git a/src/components/templates/layout/layout.stories.tsx b/src/components/templates/layout/layout.stories.tsx
index 105e808..4666b07 100644
--- a/src/components/templates/layout/layout.stories.tsx
+++ b/src/components/templates/layout/layout.stories.tsx
@@ -60,6 +60,32 @@ export default {
         required: false,
       },
     },
+    useGrid: {
+      control: {
+        type: 'boolean',
+      },
+      description: 'Use the grid layout.',
+      table: {
+        category: 'Options',
+      },
+      type: {
+        name: 'boolean',
+        required: false,
+      },
+    },
+    withExtraPadding: {
+      control: {
+        type: 'boolean',
+      },
+      description: 'Set additional padding at the bottom of the page.',
+      table: {
+        category: 'Options',
+      },
+      type: {
+        name: 'boolean',
+        required: false,
+      },
+    },
   },
   decorators: [
     (Story) => (
diff --git a/src/components/templates/page/page-layout.stories.tsx b/src/components/templates/page/page-layout.stories.tsx
index bec1066..12fe26a 100644
--- a/src/components/templates/page/page-layout.stories.tsx
+++ b/src/components/templates/page/page-layout.stories.tsx
@@ -33,6 +33,30 @@ export default {
         required: false,
       },
     },
+    bodyAttributes: {
+      description: 'Set additional HTML attributes to the main content body.',
+      table: {
+        category: 'Options',
+      },
+      type: {
+        name: 'object',
+        required: false,
+        value: {},
+      },
+    },
+    bodyClassName: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the main content body.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     breadcrumb: {
       description: 'The breadcrumb items.',
       type: {
-- 
cgit v1.2.3

