From 872b0c172a38db4f440dc6044eb1d5725c03abb1 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Wed, 13 Apr 2022 19:34:49 +0200
Subject: chore: add a CardsList component

---
 src/components/organisms/layout/cards-list.tsx | 80 ++++++++++++++++++++++++++
 1 file changed, 80 insertions(+)
 create mode 100644 src/components/organisms/layout/cards-list.tsx

(limited to 'src/components/organisms/layout/cards-list.tsx')

diff --git a/src/components/organisms/layout/cards-list.tsx b/src/components/organisms/layout/cards-list.tsx
new file mode 100644
index 0000000..a53df0d
--- /dev/null
+++ b/src/components/organisms/layout/cards-list.tsx
@@ -0,0 +1,80 @@
+import List, {
+  type ListItem,
+  type ListProps,
+} from '@components/atoms/lists/list';
+import Card, { type CardProps } from '@components/molecules/layout/card';
+import { VFC } from 'react';
+import styles from './cards-list.module.scss';
+
+export type CardsListItem = Omit<
+  CardProps,
+  'className' | 'coverFit' | 'titleLevel'
+> & {
+  id: string;
+};
+
+export type CardsListProps = {
+  /**
+   * The cover fit.
+   */
+  coverFit?: CardProps['coverFit'];
+  /**
+   * The cards data.
+   */
+  items: CardsListItem[];
+  /**
+   * The list kind. Either ordered or unordered.
+   */
+  kind?: ListProps['kind'];
+  /**
+   * The title level (hn).
+   */
+  titleLevel: CardProps['titleLevel'];
+};
+
+/**
+ * CardsList component
+ *
+ * Return a list of Card components.
+ */
+const CardsList: VFC<CardsListProps> = ({
+  coverFit,
+  items,
+  kind = 'unordered',
+  titleLevel,
+}) => {
+  const kindModifier = `wrapper--${kind}`;
+
+  /**
+   * Format the cards data to be used by the List component.
+   *
+   * @param {CardsListItem[]} cards - An array of card data.
+   * @returns {ListItem[]} The formatted cards data.
+   */
+  const getCards = (cards: CardsListItem[]): ListItem[] => {
+    return cards.map(({ id, ...card }) => {
+      return {
+        id,
+        value: (
+          <Card
+            key={id}
+            coverFit={coverFit}
+            titleLevel={titleLevel}
+            className={styles.card}
+            {...card}
+          />
+        ),
+      };
+    });
+  };
+
+  return (
+    <List
+      items={getCards(items)}
+      withMargin={false}
+      className={`${styles.wrapper} ${styles[kindModifier]}`}
+    />
+  );
+};
+
+export default CardsList;
-- 
cgit v1.2.3


From 5a6e4eea16047083e2de0e91a1b3ed9be8d6eb68 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Sat, 16 Apr 2022 16:08:49 +0200
Subject: refactor: support React 18

I replaced the deprecated VFC type with FC type and made all children
explicits.
Formatjs is still not compatible with React 18 so I need to skip type
checking when comitting. There are some type errors because of
IntlProvider in Storybook stories.
---
 jest.setup.js                                      |  1 +
 src/components/atoms/buttons/button-link.tsx       |  6 +-
 src/components/atoms/buttons/button.tsx            |  6 +-
 src/components/atoms/forms/checkbox.tsx            |  4 +-
 src/components/atoms/forms/field.tsx               |  4 +-
 src/components/atoms/forms/form.test.tsx           |  6 +-
 src/components/atoms/forms/form.tsx                |  6 +-
 src/components/atoms/forms/label.tsx               |  6 +-
 src/components/atoms/forms/select.tsx              |  4 +-
 src/components/atoms/headings/heading.tsx          |  6 +-
 src/components/atoms/icons/arrow.tsx               |  4 +-
 src/components/atoms/icons/career.tsx              |  4 +-
 src/components/atoms/icons/cc-by-sa.tsx            |  4 +-
 src/components/atoms/icons/close.tsx               |  4 +-
 src/components/atoms/icons/cog.tsx                 |  4 +-
 src/components/atoms/icons/computer-screen.tsx     |  4 +-
 src/components/atoms/icons/envelop.tsx             |  4 +-
 src/components/atoms/icons/hamburger.tsx           |  2 +-
 src/components/atoms/icons/home.tsx                |  4 +-
 src/components/atoms/icons/magnifying-glass.tsx    |  4 +-
 src/components/atoms/icons/moon.tsx                |  6 +-
 src/components/atoms/icons/plus-minus.tsx          |  2 +-
 src/components/atoms/icons/posts-stack.tsx         |  4 +-
 src/components/atoms/icons/sun.tsx                 |  6 +-
 src/components/atoms/images/logo.tsx               |  6 +-
 src/components/atoms/layout/copyright.tsx          |  4 +-
 src/components/atoms/layout/main.tsx               |  6 +-
 src/components/atoms/layout/no-script.tsx          |  4 +-
 src/components/atoms/layout/notice.tsx             |  4 +-
 src/components/atoms/layout/section.tsx            |  4 +-
 src/components/atoms/links/link.module.scss        | 20 ++++--
 src/components/atoms/links/link.tsx                |  6 +-
 src/components/atoms/links/nav-link.tsx            |  4 +-
 src/components/atoms/links/sharing-link.tsx        |  4 +-
 src/components/atoms/links/social-link.tsx         |  4 +-
 src/components/atoms/lists/description-list.tsx    |  4 +-
 src/components/atoms/lists/list.tsx                |  4 +-
 src/components/atoms/loaders/progress-bar.tsx      |  4 +-
 src/components/atoms/loaders/spinner.tsx           |  4 +-
 src/components/molecules/buttons/back-to-top.tsx   | 14 ++--
 .../molecules/buttons/heading-button.tsx           |  4 +-
 src/components/molecules/buttons/help-button.tsx   |  6 +-
 .../molecules/forms/ackee-select.test.tsx          | 16 +++--
 src/components/molecules/forms/ackee-select.tsx    |  6 +-
 src/components/molecules/forms/labelled-field.tsx  |  4 +-
 src/components/molecules/forms/labelled-select.tsx | 15 +++--
 src/components/molecules/forms/motion-toggle.tsx   |  8 +--
 .../molecules/forms/prism-theme-toggle.tsx         |  8 +--
 .../molecules/forms/select-with-tooltip.tsx        | 10 +--
 src/components/molecules/forms/theme-toggle.tsx    |  8 +--
 src/components/molecules/forms/toggle.tsx          | 18 ++---
 .../molecules/images/responsive-image.tsx          | 10 +--
 src/components/molecules/layout/branding.tsx       | 12 ++--
 src/components/molecules/layout/card.tsx           |  8 +--
 src/components/molecules/layout/flipping-logo.tsx  | 10 +--
 src/components/molecules/layout/meta.tsx           |  4 +-
 src/components/molecules/layout/widget.tsx         | 10 ++-
 src/components/molecules/modals/modal.test.tsx     | 13 +++-
 src/components/molecules/modals/modal.tsx          | 14 ++--
 src/components/molecules/modals/tooltip.tsx        |  4 +-
 src/components/molecules/nav/breadcrumb.tsx        |  4 +-
 src/components/molecules/nav/nav.tsx               |  4 +-
 .../organisms/forms/comment-form.stories.tsx       |  1 -
 src/components/organisms/forms/comment-form.tsx    |  4 +-
 src/components/organisms/forms/contact-form.tsx    |  4 +-
 src/components/organisms/forms/search-form.tsx     | 11 +++-
 src/components/organisms/layout/cards-list.tsx     |  4 +-
 src/components/organisms/layout/footer.tsx         | 13 ++--
 src/components/organisms/layout/overview.tsx       |  4 +-
 .../organisms/layout/summary.stories.tsx           |  2 +-
 src/components/organisms/layout/summary.test.tsx   |  2 +-
 src/components/organisms/layout/summary.tsx        | 77 +++++++++++++---------
 src/components/organisms/modals/search-modal.tsx   |  8 +--
 src/components/organisms/modals/settings-modal.tsx | 14 ++--
 src/components/organisms/toolbar/main-nav.tsx      | 18 +++--
 src/components/organisms/toolbar/search.tsx        | 16 ++---
 src/components/organisms/toolbar/settings.tsx      | 14 ++--
 src/components/organisms/toolbar/toolbar.tsx       |  4 +-
 src/components/organisms/widgets/image-widget.tsx  | 34 ++++------
 .../organisms/widgets/links-list-widget.tsx        |  9 ++-
 src/components/organisms/widgets/sharing.tsx       |  6 +-
 81 files changed, 353 insertions(+), 289 deletions(-)

(limited to 'src/components/organisms/layout/cards-list.tsx')

diff --git a/jest.setup.js b/jest.setup.js
index 5c5ecbc..d50c988 100644
--- a/jest.setup.js
+++ b/jest.setup.js
@@ -2,3 +2,4 @@ import '@testing-library/jest-dom/extend-expect';
 import './__tests__/jest/__mocks__/matchMedia.mock';
 
 jest.mock('next/dist/client/router', () => require('next-router-mock'));
+jest.mock('next/dynamic', () => () => 'dynamic-import');
diff --git a/src/components/atoms/buttons/button-link.tsx b/src/components/atoms/buttons/button-link.tsx
index 77a7f7b..906fa76 100644
--- a/src/components/atoms/buttons/button-link.tsx
+++ b/src/components/atoms/buttons/button-link.tsx
@@ -1,5 +1,5 @@
 import Link from 'next/link';
-import { FC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './buttons.module.scss';
 
 export type ButtonLinkProps = {
@@ -7,6 +7,10 @@ export type ButtonLinkProps = {
    * ButtonLink accessible label.
    */
   'aria-label'?: string;
+  /**
+   * The button link body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames to the button link.
    */
diff --git a/src/components/atoms/buttons/button.tsx b/src/components/atoms/buttons/button.tsx
index 545c5c5..a6eef8b 100644
--- a/src/components/atoms/buttons/button.tsx
+++ b/src/components/atoms/buttons/button.tsx
@@ -1,7 +1,11 @@
-import { FC, MouseEventHandler } from 'react';
+import { FC, MouseEventHandler, ReactNode } from 'react';
 import styles from './buttons.module.scss';
 
 export type ButtonProps = {
+  /**
+   * The button body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames to the button wrapper.
    */
diff --git a/src/components/atoms/forms/checkbox.tsx b/src/components/atoms/forms/checkbox.tsx
index 8babcc8..aec97f0 100644
--- a/src/components/atoms/forms/checkbox.tsx
+++ b/src/components/atoms/forms/checkbox.tsx
@@ -1,4 +1,4 @@
-import { SetStateAction, VFC } from 'react';
+import { FC, SetStateAction } from 'react';
 
 export type CheckboxProps = {
   /**
@@ -32,7 +32,7 @@ export type CheckboxProps = {
  *
  * Render a checkbox type input.
  */
-const Checkbox: VFC<CheckboxProps> = ({ value, setValue, ...props }) => {
+const Checkbox: FC<CheckboxProps> = ({ value, setValue, ...props }) => {
   return (
     <input
       type="checkbox"
diff --git a/src/components/atoms/forms/field.tsx b/src/components/atoms/forms/field.tsx
index 2e75d0f..e45a8a7 100644
--- a/src/components/atoms/forms/field.tsx
+++ b/src/components/atoms/forms/field.tsx
@@ -1,4 +1,4 @@
-import { ChangeEvent, SetStateAction, VFC } from 'react';
+import { ChangeEvent, FC, SetStateAction } from 'react';
 import styles from './forms.module.scss';
 
 export type FieldType =
@@ -72,7 +72,7 @@ export type FieldProps = {
  *
  * Render either an input or a textarea.
  */
-const Field: VFC<FieldProps> = ({
+const Field: FC<FieldProps> = ({
   className = '',
   setValue,
   type,
diff --git a/src/components/atoms/forms/form.test.tsx b/src/components/atoms/forms/form.test.tsx
index 9cd3c58..8b534f1 100644
--- a/src/components/atoms/forms/form.test.tsx
+++ b/src/components/atoms/forms/form.test.tsx
@@ -3,7 +3,11 @@ import Form from './form';
 
 describe('Form', () => {
   it('renders a form', () => {
-    render(<Form aria-label="Jest form" onSubmit={() => null}></Form>);
+    render(
+      <Form aria-label="Jest form" onSubmit={() => null}>
+        Fields
+      </Form>
+    );
     expect(screen.getByRole('form', { name: 'Jest form' })).toBeInTheDocument();
   });
 });
diff --git a/src/components/atoms/forms/form.tsx b/src/components/atoms/forms/form.tsx
index 8e80930..ef8dce4 100644
--- a/src/components/atoms/forms/form.tsx
+++ b/src/components/atoms/forms/form.tsx
@@ -1,4 +1,4 @@
-import { Children, FC, FormEvent, Fragment } from 'react';
+import { Children, FC, FormEvent, Fragment, ReactNode } from 'react';
 import styles from './forms.module.scss';
 
 export type FormProps = {
@@ -10,6 +10,10 @@ export type FormProps = {
    * One or more ids that refers to the form name.
    */
   'aria-labelledby'?: string;
+  /**
+   * The form body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames to the form wrapper.
    */
diff --git a/src/components/atoms/forms/label.tsx b/src/components/atoms/forms/label.tsx
index 8d57ee2..ce4c70f 100644
--- a/src/components/atoms/forms/label.tsx
+++ b/src/components/atoms/forms/label.tsx
@@ -1,7 +1,11 @@
-import { FC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './label.module.scss';
 
 export type LabelProps = {
+  /**
+   * The label body.
+   */
+  children: ReactNode;
   /**
    * Add classnames to the label.
    */
diff --git a/src/components/atoms/forms/select.tsx b/src/components/atoms/forms/select.tsx
index 25e86e0..dbe9b37 100644
--- a/src/components/atoms/forms/select.tsx
+++ b/src/components/atoms/forms/select.tsx
@@ -1,4 +1,4 @@
-import { ChangeEvent, SetStateAction, VFC } from 'react';
+import { ChangeEvent, FC, SetStateAction } from 'react';
 import styles from './forms.module.scss';
 
 export type SelectOptions = {
@@ -60,7 +60,7 @@ export type SelectProps = {
  *
  * Render a HTML select element.
  */
-const Select: VFC<SelectProps> = ({
+const Select: FC<SelectProps> = ({
   className = '',
   options,
   setValue,
diff --git a/src/components/atoms/headings/heading.tsx b/src/components/atoms/headings/heading.tsx
index 4703b5d..c5bf4ca 100644
--- a/src/components/atoms/headings/heading.tsx
+++ b/src/components/atoms/headings/heading.tsx
@@ -1,9 +1,13 @@
-import { FC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './heading.module.scss';
 
 export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
 
 export type HeadingProps = {
+  /**
+   * The heading body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames.
    */
diff --git a/src/components/atoms/icons/arrow.tsx b/src/components/atoms/icons/arrow.tsx
index 5f3c460..2962530 100644
--- a/src/components/atoms/icons/arrow.tsx
+++ b/src/components/atoms/icons/arrow.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './arrow.module.scss';
 
 export type ArrowDirection = 'top' | 'right' | 'bottom' | 'left';
@@ -19,7 +19,7 @@ export type ArrowProps = {
  *
  * Render a svg arrow icon.
  */
-const Arrow: VFC<ArrowProps> = ({ className = '', direction }) => {
+const Arrow: FC<ArrowProps> = ({ className = '', direction }) => {
   const directionClass = styles[`icon--${direction}`];
   const classes = `${styles.icon} ${directionClass} ${className}`;
 
diff --git a/src/components/atoms/icons/career.tsx b/src/components/atoms/icons/career.tsx
index 28edcc7..f28d399 100644
--- a/src/components/atoms/icons/career.tsx
+++ b/src/components/atoms/icons/career.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './career.module.scss';
 
 export type CareerProps = {
@@ -13,7 +13,7 @@ export type CareerProps = {
  *
  * Render a career svg icon.
  */
-const Career: VFC<CareerProps> = ({ className = '' }) => {
+const Career: FC<CareerProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/cc-by-sa.tsx b/src/components/atoms/icons/cc-by-sa.tsx
index 552504e..8239154 100644
--- a/src/components/atoms/icons/cc-by-sa.tsx
+++ b/src/components/atoms/icons/cc-by-sa.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './cc-by-sa.module.scss';
 
@@ -14,7 +14,7 @@ export type CCBySAProps = {
  *
  * Render a CC BY SA svg icon.
  */
-const CCBySA: VFC<CCBySAProps> = ({ className = '' }) => {
+const CCBySA: FC<CCBySAProps> = ({ className = '' }) => {
   const intl = useIntl();
 
   return (
diff --git a/src/components/atoms/icons/close.tsx b/src/components/atoms/icons/close.tsx
index eb9ce7c..3e0adb5 100644
--- a/src/components/atoms/icons/close.tsx
+++ b/src/components/atoms/icons/close.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './close.module.scss';
 
 export type CloseProps = {
@@ -13,7 +13,7 @@ export type CloseProps = {
  *
  * Render a close svg icon.
  */
-const Close: VFC<CloseProps> = ({ className = '' }) => {
+const Close: FC<CloseProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/cog.tsx b/src/components/atoms/icons/cog.tsx
index df6d54d..9e78a7b 100644
--- a/src/components/atoms/icons/cog.tsx
+++ b/src/components/atoms/icons/cog.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './cog.module.scss';
 
 export type CogProps = {
@@ -13,7 +13,7 @@ export type CogProps = {
  *
  * Render a cog svg icon.
  */
-const Cog: VFC<CogProps> = ({ className = '' }) => {
+const Cog: FC<CogProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/computer-screen.tsx b/src/components/atoms/icons/computer-screen.tsx
index 310836f..8786139 100644
--- a/src/components/atoms/icons/computer-screen.tsx
+++ b/src/components/atoms/icons/computer-screen.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './computer-screen.module.scss';
 
 export type ComputerScreenProps = {
@@ -13,7 +13,7 @@ export type ComputerScreenProps = {
  *
  * Render a computer screen svg icon.
  */
-const ComputerScreen: VFC<ComputerScreenProps> = ({ className = '' }) => {
+const ComputerScreen: FC<ComputerScreenProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/envelop.tsx b/src/components/atoms/icons/envelop.tsx
index 7b50d1d..84dca97 100644
--- a/src/components/atoms/icons/envelop.tsx
+++ b/src/components/atoms/icons/envelop.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './envelop.module.scss';
 
 export type EnvelopProps = {
@@ -13,7 +13,7 @@ export type EnvelopProps = {
  *
  * Render an envelop svg icon.
  */
-const Envelop: VFC<EnvelopProps> = ({ className = '' }) => {
+const Envelop: FC<EnvelopProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/hamburger.tsx b/src/components/atoms/icons/hamburger.tsx
index 7e7c2c9..93aed2a 100644
--- a/src/components/atoms/icons/hamburger.tsx
+++ b/src/components/atoms/icons/hamburger.tsx
@@ -1,7 +1,7 @@
 import { FC } from 'react';
 import styles from './hamburger.module.scss';
 
-type HamburgerProps = {
+export type HamburgerProps = {
   /**
    * Set additional classnames to the icon wrapper.
    */
diff --git a/src/components/atoms/icons/home.tsx b/src/components/atoms/icons/home.tsx
index 71bbc4a..3b6732d 100644
--- a/src/components/atoms/icons/home.tsx
+++ b/src/components/atoms/icons/home.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './home.module.scss';
 
 export type HomeProps = {
@@ -13,7 +13,7 @@ export type HomeProps = {
  *
  * Render a home svg icon.
  */
-const Home: VFC<HomeProps> = ({ className = '' }) => {
+const Home: FC<HomeProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/magnifying-glass.tsx b/src/components/atoms/icons/magnifying-glass.tsx
index 445ef10..1ca2a44 100644
--- a/src/components/atoms/icons/magnifying-glass.tsx
+++ b/src/components/atoms/icons/magnifying-glass.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './magnifying-glass.module.scss';
 
 export type MagnifyingGlassProps = {
@@ -13,7 +13,7 @@ export type MagnifyingGlassProps = {
  *
  * Render a magnifying glass svg icon.
  */
-const MagnifyingGlass: VFC<MagnifyingGlassProps> = ({ className = '' }) => {
+const MagnifyingGlass: FC<MagnifyingGlassProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/moon.tsx b/src/components/atoms/icons/moon.tsx
index 4f52319..ec4fa0c 100644
--- a/src/components/atoms/icons/moon.tsx
+++ b/src/components/atoms/icons/moon.tsx
@@ -1,7 +1,7 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './moon.module.scss';
 
-type MoonProps = {
+export type MoonProps = {
   /**
    * Set additional classnames to the icon.
    */
@@ -12,7 +12,7 @@ type MoonProps = {
   title?: string;
 };
 
-const Moon: VFC<MoonProps> = ({ className = '', title }) => {
+const Moon: FC<MoonProps> = ({ className = '', title }) => {
   return (
     <svg
       className={`${styles.icon} ${className}`}
diff --git a/src/components/atoms/icons/plus-minus.tsx b/src/components/atoms/icons/plus-minus.tsx
index 78aa14a..e8897b7 100644
--- a/src/components/atoms/icons/plus-minus.tsx
+++ b/src/components/atoms/icons/plus-minus.tsx
@@ -1,7 +1,7 @@
 import { FC } from 'react';
 import styles from './plus-minus.module.scss';
 
-type PlusMinusProps = {
+export type PlusMinusProps = {
   /**
    * Set additional classnames to the icon.
    */
diff --git a/src/components/atoms/icons/posts-stack.tsx b/src/components/atoms/icons/posts-stack.tsx
index 1998d25..ab21323 100644
--- a/src/components/atoms/icons/posts-stack.tsx
+++ b/src/components/atoms/icons/posts-stack.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './posts-stack.module.scss';
 
 export type PostsStackProps = {
@@ -13,7 +13,7 @@ export type PostsStackProps = {
  *
  * Render a posts stack svg icon.
  */
-const PostsStack: VFC<PostsStackProps> = ({ className = '' }) => {
+const PostsStack: FC<PostsStackProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/sun.tsx b/src/components/atoms/icons/sun.tsx
index fa9d922..ca31747 100644
--- a/src/components/atoms/icons/sun.tsx
+++ b/src/components/atoms/icons/sun.tsx
@@ -1,7 +1,7 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './sun.module.scss';
 
-type SunProps = {
+export type SunProps = {
   /**
    * Set additional classnames to the icon.
    */
@@ -17,7 +17,7 @@ type SunProps = {
  *
  * Render a svg sun icon.
  */
-const Sun: VFC<SunProps> = ({ className = '', title }) => {
+const Sun: FC<SunProps> = ({ className = '', title }) => {
   return (
     <svg
       className={`${styles.sun} ${className}`}
diff --git a/src/components/atoms/images/logo.tsx b/src/components/atoms/images/logo.tsx
index 2e52110..3978882 100644
--- a/src/components/atoms/images/logo.tsx
+++ b/src/components/atoms/images/logo.tsx
@@ -1,7 +1,7 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './logo.module.scss';
 
-type LogoProps = {
+export type LogoProps = {
   /**
    * SVG Image title.
    */
@@ -13,7 +13,7 @@ type LogoProps = {
  *
  * Render a SVG logo.
  */
-const Logo: VFC<LogoProps> = ({ title }) => {
+const Logo: FC<LogoProps> = ({ title }) => {
   return (
     <svg
       viewBox="0 0 512 512"
diff --git a/src/components/atoms/layout/copyright.tsx b/src/components/atoms/layout/copyright.tsx
index 76252ee..f70695d 100644
--- a/src/components/atoms/layout/copyright.tsx
+++ b/src/components/atoms/layout/copyright.tsx
@@ -1,4 +1,4 @@
-import { ReactNode, VFC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './copyright.module.scss';
 
 export type CopyrightDates = {
@@ -32,7 +32,7 @@ export type CopyrightProps = {
  *
  * Renders a copyright information (owner, dates, license icon).
  */
-const Copyright: VFC<CopyrightProps> = ({ owner, dates, icon }) => {
+const Copyright: FC<CopyrightProps> = ({ owner, dates, icon }) => {
   const getFormattedDate = (date: string) => {
     const datetime = new Date(date).toISOString();
 
diff --git a/src/components/atoms/layout/main.tsx b/src/components/atoms/layout/main.tsx
index 4549328..d92a5c7 100644
--- a/src/components/atoms/layout/main.tsx
+++ b/src/components/atoms/layout/main.tsx
@@ -1,6 +1,10 @@
-import { FC } from 'react';
+import { FC, ReactNode } from 'react';
 
 export type MainProps = {
+  /**
+   * The main body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames to the main element.
    */
diff --git a/src/components/atoms/layout/no-script.tsx b/src/components/atoms/layout/no-script.tsx
index 6358cf8..a503e0c 100644
--- a/src/components/atoms/layout/no-script.tsx
+++ b/src/components/atoms/layout/no-script.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './no-script.module.scss';
 
 export type NoScriptProps = {
@@ -12,7 +12,7 @@ export type NoScriptProps = {
   position?: 'initial' | 'top';
 };
 
-const NoScript: VFC<NoScriptProps> = ({ message, position = 'initial' }) => {
+const NoScript: FC<NoScriptProps> = ({ message, position = 'initial' }) => {
   const positionClass = styles[`noscript--${position}`];
 
   return <div className={`${styles.noscript} ${positionClass}`}>{message}</div>;
diff --git a/src/components/atoms/layout/notice.tsx b/src/components/atoms/layout/notice.tsx
index b6e09c5..115bd9c 100644
--- a/src/components/atoms/layout/notice.tsx
+++ b/src/components/atoms/layout/notice.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './notice.module.scss';
 
 export type NoticeKind = 'error' | 'info' | 'success' | 'warning';
@@ -19,7 +19,7 @@ export type NoticeProps = {
  *
  * Render a colored message depending on notice kind.
  */
-const Notice: VFC<NoticeProps> = ({ kind, message }) => {
+const Notice: FC<NoticeProps> = ({ kind, message }) => {
   const kindClass = `wrapper--${kind}`;
 
   return (
diff --git a/src/components/atoms/layout/section.tsx b/src/components/atoms/layout/section.tsx
index f1bbb34..cb727ff 100644
--- a/src/components/atoms/layout/section.tsx
+++ b/src/components/atoms/layout/section.tsx
@@ -1,4 +1,4 @@
-import { ReactNode, VFC } from 'react';
+import { FC, ReactNode } from 'react';
 import Heading from '../headings/heading';
 import styles from './section.module.scss';
 
@@ -32,7 +32,7 @@ export type SectionProps = {
  *
  * Render a section element.
  */
-const Section: VFC<SectionProps> = ({
+const Section: FC<SectionProps> = ({
   className = '',
   content,
   title,
diff --git a/src/components/atoms/links/link.module.scss b/src/components/atoms/links/link.module.scss
index e7ead86..d23667a 100644
--- a/src/components/atoms/links/link.module.scss
+++ b/src/components/atoms/links/link.module.scss
@@ -5,7 +5,9 @@
   &[hreflang] {
     &::after {
       display: inline-block;
-      content: "\0000a0["attr(hreflang) "]";
+      /* Prettier is removing spacing between content parts. */
+      /* prettier-ignore */
+      content: "\0000a0[" attr(hreflang) "]";
       font-size: var(--font-size-sm);
     }
   }
@@ -13,22 +15,30 @@
   &--external {
     &::after {
       display: inline-block;
-      content: "\0000a0"url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+      /* Prettier is removing spacing between content parts. */
+      /* prettier-ignore */
+      content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
     }
 
     &:focus:not(:active)::after {
-      content: "\0000a0"url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_white}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+      /* Prettier is removing spacing between content parts. */
+      /* prettier-ignore */
+      content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_white}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
     }
 
     &[hreflang] {
       &::after {
-        content: "\0000a0["attr(hreflang) "]\0000a0"url(fun.encode-svg(
+        /* Prettier is removing spacing between content parts. */
+        /* prettier-ignore */
+        content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg(
             '<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'
           ));
       }
 
       &:focus:not(:active)::after {
-        content: "\0000a0["attr(hreflang) "]\0000a0"url(fun.encode-svg(
+        /* Prettier is removing spacing between content parts. */
+        /* prettier-ignore */
+        content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg(
             '<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_white}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'
           ));
       }
diff --git a/src/components/atoms/links/link.tsx b/src/components/atoms/links/link.tsx
index 87f11fc..674c07b 100644
--- a/src/components/atoms/links/link.tsx
+++ b/src/components/atoms/links/link.tsx
@@ -1,8 +1,12 @@
 import NextLink from 'next/link';
-import { FC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './link.module.scss';
 
 export type LinkProps = {
+  /**
+   * The link body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames to the link.
    */
diff --git a/src/components/atoms/links/nav-link.tsx b/src/components/atoms/links/nav-link.tsx
index 25c0e7d..7c6fede 100644
--- a/src/components/atoms/links/nav-link.tsx
+++ b/src/components/atoms/links/nav-link.tsx
@@ -1,5 +1,5 @@
 import Link from 'next/link';
-import { VFC, ReactNode } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './nav-link.module.scss';
 
 export type NavLinkProps = {
@@ -22,7 +22,7 @@ export type NavLinkProps = {
  *
  * Render a navigation link.
  */
-const NavLink: VFC<NavLinkProps> = ({ href, label, logo }) => {
+const NavLink: FC<NavLinkProps> = ({ href, label, logo }) => {
   return (
     <Link href={href}>
       <a className={styles.link}>
diff --git a/src/components/atoms/links/sharing-link.tsx b/src/components/atoms/links/sharing-link.tsx
index 3cd2dd1..ca53ef9 100644
--- a/src/components/atoms/links/sharing-link.tsx
+++ b/src/components/atoms/links/sharing-link.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './sharing-link.module.scss';
 
@@ -26,7 +26,7 @@ export type SharingLinkProps = {
  *
  * Render a sharing link.
  */
-const SharingLink: VFC<SharingLinkProps> = ({ medium, url }) => {
+const SharingLink: FC<SharingLinkProps> = ({ medium, url }) => {
   const intl = useIntl();
   const text = intl.formatMessage(
     {
diff --git a/src/components/atoms/links/social-link.tsx b/src/components/atoms/links/social-link.tsx
index 8c7c790..464bc60 100644
--- a/src/components/atoms/links/social-link.tsx
+++ b/src/components/atoms/links/social-link.tsx
@@ -2,7 +2,7 @@ import GithubIcon from '@assets/images/social-media/github.svg';
 import GitlabIcon from '@assets/images/social-media/gitlab.svg';
 import LinkedInIcon from '@assets/images/social-media/linkedin.svg';
 import TwitterIcon from '@assets/images/social-media/twitter.svg';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './social-link.module.scss';
 
 export type SocialWebsite = 'Github' | 'Gitlab' | 'LinkedIn' | 'Twitter';
@@ -23,7 +23,7 @@ export type SocialLinkProps = {
  *
  * Render a social icon link.
  */
-const SocialLink: VFC<SocialLinkProps> = ({ name, url }) => {
+const SocialLink: FC<SocialLinkProps> = ({ name, url }) => {
   /**
    * Retrieve a social link icon by id.
    * @param {string} id - The social website id.
diff --git a/src/components/atoms/lists/description-list.tsx b/src/components/atoms/lists/description-list.tsx
index 0a92465..a60a6a1 100644
--- a/src/components/atoms/lists/description-list.tsx
+++ b/src/components/atoms/lists/description-list.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './description-list.module.scss';
 
 export type DescriptionListItem = {
@@ -52,7 +52,7 @@ export type DescriptionListProps = {
  *
  * Render a description list.
  */
-const DescriptionList: VFC<DescriptionListProps> = ({
+const DescriptionList: FC<DescriptionListProps> = ({
   className = '',
   descriptionClassName = '',
   groupClassName = '',
diff --git a/src/components/atoms/lists/list.tsx b/src/components/atoms/lists/list.tsx
index d100a31..6726802 100644
--- a/src/components/atoms/lists/list.tsx
+++ b/src/components/atoms/lists/list.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './list.module.scss';
 
 export type ListItem = {
@@ -44,7 +44,7 @@ export type ListProps = {
  *
  * Render either an ordered or an unordered list.
  */
-const List: VFC<ListProps> = ({
+const List: FC<ListProps> = ({
   className = '',
   items,
   itemsClassName = '',
diff --git a/src/components/atoms/loaders/progress-bar.tsx b/src/components/atoms/loaders/progress-bar.tsx
index 1b1ff06..9bac847 100644
--- a/src/components/atoms/loaders/progress-bar.tsx
+++ b/src/components/atoms/loaders/progress-bar.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './progress-bar.module.scss';
 
 export type ProgressBarProps = {
@@ -29,7 +29,7 @@ export type ProgressBarProps = {
  *
  * Render a progress bar.
  */
-const ProgressBar: VFC<ProgressBarProps> = ({
+const ProgressBar: FC<ProgressBarProps> = ({
   current,
   info,
   min,
diff --git a/src/components/atoms/loaders/spinner.tsx b/src/components/atoms/loaders/spinner.tsx
index bff0f25..6655141 100644
--- a/src/components/atoms/loaders/spinner.tsx
+++ b/src/components/atoms/loaders/spinner.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './spinner.module.scss';
 
@@ -14,7 +14,7 @@ export type SpinnerProps = {
  *
  * Render a loading message with animation.
  */
-const Spinner: VFC<SpinnerProps> = ({ message }) => {
+const Spinner: FC<SpinnerProps> = ({ message }) => {
   const intl = useIntl();
 
   return (
diff --git a/src/components/molecules/buttons/back-to-top.tsx b/src/components/molecules/buttons/back-to-top.tsx
index 8a52231..bd1925a 100644
--- a/src/components/molecules/buttons/back-to-top.tsx
+++ b/src/components/molecules/buttons/back-to-top.tsx
@@ -1,18 +1,16 @@
-import ButtonLink from '@components/atoms/buttons/button-link';
+import ButtonLink, {
+  type ButtonLinkProps,
+} from '@components/atoms/buttons/button-link';
 import Arrow from '@components/atoms/icons/arrow';
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './back-to-top.module.scss';
 
-export type BackToTopProps = {
+export type BackToTopProps = Pick<ButtonLinkProps, 'target'> & {
   /**
    * Set additional classnames to the button wrapper.
    */
   className?: string;
-  /**
-   * An element id (without hashtag) to use as anchor.
-   */
-  target: string;
 };
 
 /**
@@ -20,7 +18,7 @@ export type BackToTopProps = {
  *
  * Render a back to top link.
  */
-const BackToTop: VFC<BackToTopProps> = ({ className = '', target }) => {
+const BackToTop: FC<BackToTopProps> = ({ className = '', target }) => {
   const intl = useIntl();
   const linkName = intl.formatMessage({
     defaultMessage: 'Back to top',
diff --git a/src/components/molecules/buttons/heading-button.tsx b/src/components/molecules/buttons/heading-button.tsx
index fc79749..0ed9a76 100644
--- a/src/components/molecules/buttons/heading-button.tsx
+++ b/src/components/molecules/buttons/heading-button.tsx
@@ -1,6 +1,6 @@
 import Heading, { type HeadingProps } from '@components/atoms/headings/heading';
 import PlusMinus from '@components/atoms/icons/plus-minus';
-import { SetStateAction, VFC } from 'react';
+import { FC, SetStateAction } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './heading-button.module.scss';
 
@@ -28,7 +28,7 @@ export type HeadingButtonProps = Pick<HeadingProps, 'level'> & {
  *
  * Render a button as accordion title to toggle body.
  */
-const HeadingButton: VFC<HeadingButtonProps> = ({
+const HeadingButton: FC<HeadingButtonProps> = ({
   className = '',
   expanded,
   level,
diff --git a/src/components/molecules/buttons/help-button.tsx b/src/components/molecules/buttons/help-button.tsx
index aeb84ec..f19322f 100644
--- a/src/components/molecules/buttons/help-button.tsx
+++ b/src/components/molecules/buttons/help-button.tsx
@@ -1,5 +1,5 @@
-import Button, { ButtonProps } from '@components/atoms/buttons/button';
-import { VFC } from 'react';
+import Button, { type ButtonProps } from '@components/atoms/buttons/button';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './help-button.module.scss';
 
@@ -15,7 +15,7 @@ export type HelpButtonProps = Pick<ButtonProps, 'onClick'> & {
  *
  * Render a button with an interrogation mark icon.
  */
-const HelpButton: VFC<HelpButtonProps> = ({ className = '', onClick }) => {
+const HelpButton: FC<HelpButtonProps> = ({ className = '', onClick }) => {
   const intl = useIntl();
   const text = intl.formatMessage({
     defaultMessage: 'Help',
diff --git a/src/components/molecules/forms/ackee-select.test.tsx b/src/components/molecules/forms/ackee-select.test.tsx
index e1e6b2d..ec27922 100644
--- a/src/components/molecules/forms/ackee-select.test.tsx
+++ b/src/components/molecules/forms/ackee-select.test.tsx
@@ -1,5 +1,5 @@
-import userEvent from '@testing-library/user-event';
-import { render, screen } from '@test-utils';
+import user from '@testing-library/user-event';
+import { act, render, screen } from '@test-utils';
 import AckeeSelect from './ackee-select';
 
 describe('Select', () => {
@@ -9,13 +9,15 @@ describe('Select', () => {
     expect(screen.queryByRole('combobox')).not.toHaveValue('partial');
   });
 
-  it('should correctly change value when user choose another option', () => {
+  it('should correctly change value when user choose another option', async () => {
     render(<AckeeSelect initialValue="full" />);
 
-    userEvent.selectOptions(
-      screen.getByRole('combobox'),
-      screen.getByRole('option', { name: 'Partial' })
-    );
+    await act(async () => {
+      await user.selectOptions(
+        screen.getByRole('combobox'),
+        screen.getByRole('option', { name: 'Partial' })
+      );
+    });
 
     expect(screen.getByRole('combobox')).toHaveValue('partial');
     expect(screen.queryByRole('combobox')).not.toHaveValue('full');
diff --git a/src/components/molecules/forms/ackee-select.tsx b/src/components/molecules/forms/ackee-select.tsx
index 4a8410c..101e5b5 100644
--- a/src/components/molecules/forms/ackee-select.tsx
+++ b/src/components/molecules/forms/ackee-select.tsx
@@ -1,8 +1,8 @@
 import { SelectOptions } from '@components/atoms/forms/select';
-import { Dispatch, SetStateAction, useState, VFC } from 'react';
+import { Dispatch, FC, SetStateAction, useState } from 'react';
 import { useIntl } from 'react-intl';
 import SelectWithTooltip, {
-  SelectWithTooltipProps,
+  type SelectWithTooltipProps,
 } from './select-with-tooltip';
 
 export type AckeeOptions = 'full' | 'partial';
@@ -22,7 +22,7 @@ export type AckeeSelectProps = Pick<
  *
  * Render a select to set Ackee settings.
  */
-const AckeeSelect: VFC<AckeeSelectProps> = ({ initialValue, ...props }) => {
+const AckeeSelect: FC<AckeeSelectProps> = ({ initialValue, ...props }) => {
   const intl = useIntl();
   const [value, setValue] = useState<AckeeOptions>(initialValue);
 
diff --git a/src/components/molecules/forms/labelled-field.tsx b/src/components/molecules/forms/labelled-field.tsx
index 08d0126..ecc9255 100644
--- a/src/components/molecules/forms/labelled-field.tsx
+++ b/src/components/molecules/forms/labelled-field.tsx
@@ -1,6 +1,6 @@
 import Field, { type FieldProps } from '@components/atoms/forms/field';
 import Label from '@components/atoms/forms/label';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './labelled-field.module.scss';
 
 export type LabelledFieldProps = FieldProps & {
@@ -23,7 +23,7 @@ export type LabelledFieldProps = FieldProps & {
  *
  * Render a field tied to a label.
  */
-const LabelledField: VFC<LabelledFieldProps> = ({
+const LabelledField: FC<LabelledFieldProps> = ({
   hideLabel = false,
   id,
   label,
diff --git a/src/components/molecules/forms/labelled-select.tsx b/src/components/molecules/forms/labelled-select.tsx
index 7d4237a..23057d0 100644
--- a/src/components/molecules/forms/labelled-select.tsx
+++ b/src/components/molecules/forms/labelled-select.tsx
@@ -1,6 +1,6 @@
-import Label, { LabelProps } from '@components/atoms/forms/label';
+import Label, { type LabelProps } from '@components/atoms/forms/label';
 import Select, { type SelectProps } from '@components/atoms/forms/select';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './labelled-select.module.scss';
 
 export type LabelledSelectProps = Omit<
@@ -14,7 +14,7 @@ export type LabelledSelectProps = Omit<
   /**
    * Set additional classnames to the label.
    */
-  labelClassName?: string;
+  labelClassName?: LabelProps['className'];
   /**
    * The label position. Default: top.
    */
@@ -26,10 +26,15 @@ export type LabelledSelectProps = Omit<
   /**
    * Set additional classnames to the select field.
    */
-  selectClassName?: string;
+  selectClassName?: SelectProps['className'];
 };
 
-const LabelledSelect: VFC<LabelledSelectProps> = ({
+/**
+ * LabelledSelect component
+ *
+ * Render a select with a label.
+ */
+const LabelledSelect: FC<LabelledSelectProps> = ({
   id,
   label,
   labelClassName = '',
diff --git a/src/components/molecules/forms/motion-toggle.tsx b/src/components/molecules/forms/motion-toggle.tsx
index 9f30b42..24b54ae 100644
--- a/src/components/molecules/forms/motion-toggle.tsx
+++ b/src/components/molecules/forms/motion-toggle.tsx
@@ -1,8 +1,8 @@
 import Toggle, {
-  ToggleChoices,
-  ToggleProps,
+  type ToggleChoices,
+  type ToggleProps,
 } from '@components/molecules/forms/toggle';
-import { useState, VFC } from 'react';
+import { FC, useState } from 'react';
 import { useIntl } from 'react-intl';
 
 export type MotionToggleProps = Pick<ToggleProps, 'labelClassName' | 'value'>;
@@ -12,7 +12,7 @@ export type MotionToggleProps = Pick<ToggleProps, 'labelClassName' | 'value'>;
  *
  * Render a Toggle component to set reduce motion.
  */
-const MotionToggle: VFC<MotionToggleProps> = ({ value, ...props }) => {
+const MotionToggle: FC<MotionToggleProps> = ({ value, ...props }) => {
   const intl = useIntl();
   const [isDeactivated, setIsDeactivated] = useState<boolean>(value);
   const reduceMotionLabel = intl.formatMessage({
diff --git a/src/components/molecules/forms/prism-theme-toggle.tsx b/src/components/molecules/forms/prism-theme-toggle.tsx
index daee6bd..0b9c447 100644
--- a/src/components/molecules/forms/prism-theme-toggle.tsx
+++ b/src/components/molecules/forms/prism-theme-toggle.tsx
@@ -1,10 +1,10 @@
 import Moon from '@components/atoms/icons/moon';
 import Sun from '@components/atoms/icons/sun';
 import Toggle, {
-  ToggleChoices,
-  ToggleProps,
+  type ToggleChoices,
+  type ToggleProps,
 } from '@components/molecules/forms/toggle';
-import { useState, VFC } from 'react';
+import { FC, useState } from 'react';
 import { useIntl } from 'react-intl';
 
 export type PrismThemeToggleProps = Pick<
@@ -17,7 +17,7 @@ export type PrismThemeToggleProps = Pick<
  *
  * Render a Toggle component to set code blocks theme.
  */
-const PrismThemeToggle: VFC<PrismThemeToggleProps> = ({ value, ...props }) => {
+const PrismThemeToggle: FC<PrismThemeToggleProps> = ({ value, ...props }) => {
   const intl = useIntl();
   const [isDarkTheme, setIsDarkTheme] = useState<boolean>(value);
   const themeLabel = intl.formatMessage({
diff --git a/src/components/molecules/forms/select-with-tooltip.tsx b/src/components/molecules/forms/select-with-tooltip.tsx
index f537e1e..cf7b041 100644
--- a/src/components/molecules/forms/select-with-tooltip.tsx
+++ b/src/components/molecules/forms/select-with-tooltip.tsx
@@ -1,4 +1,4 @@
-import { useState, VFC } from 'react';
+import { FC, useState } from 'react';
 import HelpButton from '../buttons/help-button';
 import Tooltip, { type TooltipProps } from '../modals/tooltip';
 import LabelledSelect, { type LabelledSelectProps } from './labelled-select';
@@ -9,14 +9,10 @@ export type SelectWithTooltipProps = Omit<
   'labelPosition'
 > &
   Pick<TooltipProps, 'title' | 'content'> & {
-    /**
-     * The select label.
-     */
-    label: string;
     /**
      * Set additional classnames to the tooltip wrapper.
      */
-    tooltipClassName?: string;
+    tooltipClassName?: TooltipProps['className'];
   };
 
 /**
@@ -24,7 +20,7 @@ export type SelectWithTooltipProps = Omit<
  *
  * Render a select with a button to display a tooltip about options.
  */
-const SelectWithTooltip: VFC<SelectWithTooltipProps> = ({
+const SelectWithTooltip: FC<SelectWithTooltipProps> = ({
   title,
   content,
   id,
diff --git a/src/components/molecules/forms/theme-toggle.tsx b/src/components/molecules/forms/theme-toggle.tsx
index eb56ce9..10c6c47 100644
--- a/src/components/molecules/forms/theme-toggle.tsx
+++ b/src/components/molecules/forms/theme-toggle.tsx
@@ -1,10 +1,10 @@
 import Moon from '@components/atoms/icons/moon';
 import Sun from '@components/atoms/icons/sun';
 import Toggle, {
-  ToggleChoices,
-  ToggleProps,
+  type ToggleChoices,
+  type ToggleProps,
 } from '@components/molecules/forms/toggle';
-import { useState, VFC } from 'react';
+import { FC, useState } from 'react';
 import { useIntl } from 'react-intl';
 
 export type ThemeToggleProps = Pick<ToggleProps, 'labelClassName' | 'value'>;
@@ -14,7 +14,7 @@ export type ThemeToggleProps = Pick<ToggleProps, 'labelClassName' | 'value'>;
  *
  * Render a Toggle component to set theme.
  */
-const ThemeToggle: VFC<ThemeToggleProps> = ({ value, ...props }) => {
+const ThemeToggle: FC<ThemeToggleProps> = ({ value, ...props }) => {
   const intl = useIntl();
   const [isDarkTheme, setIsDarkTheme] = useState<boolean>(value);
   const themeLabel = intl.formatMessage({
diff --git a/src/components/molecules/forms/toggle.tsx b/src/components/molecules/forms/toggle.tsx
index dff2d2d..288062d 100644
--- a/src/components/molecules/forms/toggle.tsx
+++ b/src/components/molecules/forms/toggle.tsx
@@ -1,6 +1,6 @@
-import Checkbox from '@components/atoms/forms/checkbox';
+import Checkbox, { type CheckboxProps } from '@components/atoms/forms/checkbox';
 import Label, { type LabelProps } from '@components/atoms/forms/label';
-import { ReactNode, VFC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './toggle.module.scss';
 
 export type ToggleChoices = {
@@ -14,15 +14,11 @@ export type ToggleChoices = {
   right: ReactNode;
 };
 
-export type ToggleProps = {
+export type ToggleProps = Pick<CheckboxProps, 'id' | 'name'> & {
   /**
    * The toggle choices.
    */
   choices: ToggleChoices;
-  /**
-   * The input id.
-   */
-  id: string;
   /**
    * The toggle label.
    */
@@ -30,15 +26,11 @@ export type ToggleProps = {
   /**
    * Set additional classnames to the label.
    */
-  labelClassName?: string;
+  labelClassName?: LabelProps['className'];
   /**
    * The label size.
    */
   labelSize?: LabelProps['size'];
-  /**
-   * The input name.
-   */
-  name: string;
   /**
    * The toggle value. True if checked.
    */
@@ -54,7 +46,7 @@ export type ToggleProps = {
  *
  * Render a toggle with a label and two choices.
  */
-const Toggle: VFC<ToggleProps> = ({
+const Toggle: FC<ToggleProps> = ({
   choices,
   id,
   label,
diff --git a/src/components/molecules/images/responsive-image.tsx b/src/components/molecules/images/responsive-image.tsx
index 1d8787e..31cbcd1 100644
--- a/src/components/molecules/images/responsive-image.tsx
+++ b/src/components/molecules/images/responsive-image.tsx
@@ -1,6 +1,6 @@
-import Link from '@components/atoms/links/link';
-import Image, { ImageProps } from 'next/image';
-import { VFC } from 'react';
+import Link, { type LinkProps } from '@components/atoms/links/link';
+import Image, { type ImageProps } from 'next/image';
+import { FC } from 'react';
 import styles from './responsive-image.module.scss';
 
 export type ResponsiveImageProps = Omit<
@@ -26,7 +26,7 @@ export type ResponsiveImageProps = Omit<
   /**
    * A link target.
    */
-  target?: string;
+  target?: LinkProps['href'];
   /**
    * The image width.
    */
@@ -38,7 +38,7 @@ export type ResponsiveImageProps = Omit<
  *
  * Render a responsive image wrapped in a figure element.
  */
-const ResponsiveImage: VFC<ResponsiveImageProps> = ({
+const ResponsiveImage: FC<ResponsiveImageProps> = ({
   alt,
   caption,
   className = '',
diff --git a/src/components/molecules/layout/branding.tsx b/src/components/molecules/layout/branding.tsx
index 9f564bf..9fe89e7 100644
--- a/src/components/molecules/layout/branding.tsx
+++ b/src/components/molecules/layout/branding.tsx
@@ -1,11 +1,11 @@
 import Heading from '@components/atoms/headings/heading';
 import Link from 'next/link';
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './branding.module.scss';
-import FlippingLogo from './flipping-logo';
+import FlippingLogo, { type FlippingLogoProps } from './flipping-logo';
 
-type BrandingProps = {
+export type BrandingProps = Pick<FlippingLogoProps, 'photo'> & {
   /**
    * The Branding baseline.
    */
@@ -14,10 +14,6 @@ type BrandingProps = {
    * Use H1 if the current page is homepage. Default: false.
    */
   isHome?: boolean;
-  /**
-   * A photography URL.
-   */
-  photo: string;
   /**
    * The Branding title;
    */
@@ -33,7 +29,7 @@ type BrandingProps = {
  *
  * Render the branding logo, title and optional baseline.
  */
-const Branding: VFC<BrandingProps> = ({
+const Branding: FC<BrandingProps> = ({
   baseline,
   isHome = false,
   photo,
diff --git a/src/components/molecules/layout/card.tsx b/src/components/molecules/layout/card.tsx
index 23a0e54..89f100e 100644
--- a/src/components/molecules/layout/card.tsx
+++ b/src/components/molecules/layout/card.tsx
@@ -1,11 +1,11 @@
 import ButtonLink from '@components/atoms/buttons/button-link';
 import Heading, { type HeadingLevel } from '@components/atoms/headings/heading';
 import DescriptionList, {
-  DescriptionListItem,
+  type DescriptionListItem,
 } from '@components/atoms/lists/description-list';
-import { VFC } from 'react';
+import { FC } from 'react';
 import ResponsiveImage, {
-  ResponsiveImageProps,
+  type ResponsiveImageProps,
 } from '../images/responsive-image';
 import styles from './card.module.scss';
 
@@ -68,7 +68,7 @@ export type CardProps = {
  *
  * Render a link with minimal information about its content.
  */
-const Card: VFC<CardProps> = ({
+const Card: FC<CardProps> = ({
   className = '',
   cover,
   coverFit = 'cover',
diff --git a/src/components/molecules/layout/flipping-logo.tsx b/src/components/molecules/layout/flipping-logo.tsx
index 6f7645f..4a216ef 100644
--- a/src/components/molecules/layout/flipping-logo.tsx
+++ b/src/components/molecules/layout/flipping-logo.tsx
@@ -1,9 +1,9 @@
-import Logo from '@components/atoms/images/logo';
+import Logo, { type LogoProps } from '@components/atoms/images/logo';
 import Image from 'next/image';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './flipping-logo.module.scss';
 
-type FlippingLogoProps = {
+export type FlippingLogoProps = {
   /**
    * Set additional classnames to the logo wrapper.
    */
@@ -15,7 +15,7 @@ type FlippingLogoProps = {
   /**
    * Logo image title.
    */
-  logoTitle?: string;
+  logoTitle?: LogoProps['title'];
   /**
    * Photo url.
    */
@@ -27,7 +27,7 @@ type FlippingLogoProps = {
  *
  * Render a logo and a photo with a flipping effect.
  */
-const FlippingLogo: VFC<FlippingLogoProps> = ({
+const FlippingLogo: FC<FlippingLogoProps> = ({
   className = '',
   altText,
   logoTitle,
diff --git a/src/components/molecules/layout/meta.tsx b/src/components/molecules/layout/meta.tsx
index 218ebd9..fcce473 100644
--- a/src/components/molecules/layout/meta.tsx
+++ b/src/components/molecules/layout/meta.tsx
@@ -2,7 +2,7 @@ import DescriptionList, {
   type DescriptionListProps,
   type DescriptionListItem,
 } from '@components/atoms/lists/description-list';
-import { ReactNode, VFC } from 'react';
+import { FC, ReactNode } from 'react';
 
 export type MetaItem = {
   /**
@@ -43,7 +43,7 @@ export type MetaProps = {
  *
  * Renders the page metadata.
  */
-const Meta: VFC<MetaProps> = ({ data, ...props }) => {
+const Meta: FC<MetaProps> = ({ data, ...props }) => {
   /**
    * Transform the metadata to description list item format.
    *
diff --git a/src/components/molecules/layout/widget.tsx b/src/components/molecules/layout/widget.tsx
index c04362a..feb2add 100644
--- a/src/components/molecules/layout/widget.tsx
+++ b/src/components/molecules/layout/widget.tsx
@@ -1,11 +1,17 @@
-import { FC, useState } from 'react';
-import HeadingButton, { HeadingButtonProps } from '../buttons/heading-button';
+import { FC, ReactNode, useState } from 'react';
+import HeadingButton, {
+  type HeadingButtonProps,
+} from '../buttons/heading-button';
 import styles from './widget.module.scss';
 
 export type WidgetProps = Pick<
   HeadingButtonProps,
   'expanded' | 'level' | 'title'
 > & {
+  /**
+   * The widget body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames to the widget wrapper.
    */
diff --git a/src/components/molecules/modals/modal.test.tsx b/src/components/molecules/modals/modal.test.tsx
index 14fb224..9a0e237 100644
--- a/src/components/molecules/modals/modal.test.tsx
+++ b/src/components/molecules/modals/modal.test.tsx
@@ -1,9 +1,18 @@
 import { render, screen } from '@test-utils';
 import Modal from './modal';
 
+const title = 'A custom title';
+const children =
+  'Labore ullam delectus sit modi quam dolores. Ratione id sint aliquid facilis ipsum. Unde necessitatibus provident minus.';
+
 describe('Modal', () => {
   it('renders a title', () => {
-    render(<Modal title="A custom title" />);
-    expect(screen.getByText('A custom title')).toBeInTheDocument();
+    render(<Modal title={title}>{children}</Modal>);
+    expect(screen.getByText(title)).toBeInTheDocument();
+  });
+
+  it('renders the modal body', () => {
+    render(<Modal title={title}>{children}</Modal>);
+    expect(screen.getByText(children)).toBeInTheDocument();
   });
 });
diff --git a/src/components/molecules/modals/modal.tsx b/src/components/molecules/modals/modal.tsx
index 52ada57..58f5fa0 100644
--- a/src/components/molecules/modals/modal.tsx
+++ b/src/components/molecules/modals/modal.tsx
@@ -1,13 +1,17 @@
-import Heading from '@components/atoms/headings/heading';
-import { CogProps } from '@components/atoms/icons/cog';
-import { MagnifyingGlassProps } from '@components/atoms/icons/magnifying-glass';
+import Heading, { type HeadingProps } from '@components/atoms/headings/heading';
+import { type CogProps } from '@components/atoms/icons/cog';
+import { type MagnifyingGlassProps } from '@components/atoms/icons/magnifying-glass';
 import dynamic from 'next/dynamic';
-import { FC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './modal.module.scss';
 
 export type Icons = 'cogs' | 'search';
 
 export type ModalProps = {
+  /**
+   * The modal body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames.
    */
@@ -15,7 +19,7 @@ export type ModalProps = {
   /**
    * Set additional classnames to the heading.
    */
-  headingClassName?: string;
+  headingClassName?: HeadingProps['className'];
   /**
    * A icon to illustrate the modal.
    */
diff --git a/src/components/molecules/modals/tooltip.tsx b/src/components/molecules/modals/tooltip.tsx
index 73f36e7..80721f3 100644
--- a/src/components/molecules/modals/tooltip.tsx
+++ b/src/components/molecules/modals/tooltip.tsx
@@ -1,5 +1,5 @@
 import List, { type ListItem } from '@components/atoms/lists/list';
-import { ReactNode, VFC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './tooltip.module.scss';
 
 export type TooltipProps = {
@@ -26,7 +26,7 @@ export type TooltipProps = {
  *
  * Render a tooltip modal.
  */
-const Tooltip: VFC<TooltipProps> = ({
+const Tooltip: FC<TooltipProps> = ({
   className = '',
   content,
   icon,
diff --git a/src/components/molecules/nav/breadcrumb.tsx b/src/components/molecules/nav/breadcrumb.tsx
index 33af735..6dc86a0 100644
--- a/src/components/molecules/nav/breadcrumb.tsx
+++ b/src/components/molecules/nav/breadcrumb.tsx
@@ -1,7 +1,7 @@
 import Link from '@components/atoms/links/link';
 import { settings } from '@utils/config';
 import Script from 'next/script';
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import { BreadcrumbList, ListItem, WithContext } from 'schema-dts';
 import styles from './breadcrumb.module.scss';
@@ -37,7 +37,7 @@ export type BreadcrumbProps = {
  *
  * Render a breadcrumb navigation.
  */
-const Breadcrumb: VFC<BreadcrumbProps> = ({ items, ...props }) => {
+const Breadcrumb: FC<BreadcrumbProps> = ({ items, ...props }) => {
   const intl = useIntl();
 
   /**
diff --git a/src/components/molecules/nav/nav.tsx b/src/components/molecules/nav/nav.tsx
index 6ef9158..2666ea2 100644
--- a/src/components/molecules/nav/nav.tsx
+++ b/src/components/molecules/nav/nav.tsx
@@ -1,6 +1,6 @@
 import Link from '@components/atoms/links/link';
 import NavLink from '@components/atoms/links/nav-link';
-import { ReactNode, VFC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './nav.module.scss';
 
 export type NavItem = {
@@ -46,7 +46,7 @@ export type NavProps = {
  *
  * Render the nav links.
  */
-const Nav: VFC<NavProps> = ({
+const Nav: FC<NavProps> = ({
   className = '',
   items,
   kind,
diff --git a/src/components/organisms/forms/comment-form.stories.tsx b/src/components/organisms/forms/comment-form.stories.tsx
index 1ab7cf2..670176c 100644
--- a/src/components/organisms/forms/comment-form.stories.tsx
+++ b/src/components/organisms/forms/comment-form.stories.tsx
@@ -1,4 +1,3 @@
-import Notice from '@components/atoms/layout/notice';
 import { ComponentMeta, ComponentStory } from '@storybook/react';
 import { IntlProvider } from 'react-intl';
 import CommentFormComponent from './comment-form';
diff --git a/src/components/organisms/forms/comment-form.tsx b/src/components/organisms/forms/comment-form.tsx
index 6acbf94..d7cb0f5 100644
--- a/src/components/organisms/forms/comment-form.tsx
+++ b/src/components/organisms/forms/comment-form.tsx
@@ -3,7 +3,7 @@ import Form from '@components/atoms/forms/form';
 import Heading, { type HeadingLevel } from '@components/atoms/headings/heading';
 import Spinner from '@components/atoms/loaders/spinner';
 import LabelledField from '@components/molecules/forms/labelled-field';
-import { ReactNode, useState, VFC } from 'react';
+import { FC, ReactNode, useState } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './comment-form.module.scss';
 
@@ -31,7 +31,7 @@ export type CommentFormProps = {
   titleLevel?: HeadingLevel;
 };
 
-const CommentForm: VFC<CommentFormProps> = ({
+const CommentForm: FC<CommentFormProps> = ({
   className = '',
   Notice,
   saveComment,
diff --git a/src/components/organisms/forms/contact-form.tsx b/src/components/organisms/forms/contact-form.tsx
index 994244a..4a6902b 100644
--- a/src/components/organisms/forms/contact-form.tsx
+++ b/src/components/organisms/forms/contact-form.tsx
@@ -2,7 +2,7 @@ import Button from '@components/atoms/buttons/button';
 import Form from '@components/atoms/forms/form';
 import Spinner from '@components/atoms/loaders/spinner';
 import LabelledField from '@components/molecules/forms/labelled-field';
-import { ReactNode, useState, VFC } from 'react';
+import { FC, ReactNode, useState } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './contact-form.module.scss';
 
@@ -27,7 +27,7 @@ export type ContactFormProps = {
  *
  * Render a contact form.
  */
-const ContactForm: VFC<ContactFormProps> = ({
+const ContactForm: FC<ContactFormProps> = ({
   className = '',
   Notice,
   sendMail,
diff --git a/src/components/organisms/forms/search-form.tsx b/src/components/organisms/forms/search-form.tsx
index 351d93c..18b7c08 100644
--- a/src/components/organisms/forms/search-form.tsx
+++ b/src/components/organisms/forms/search-form.tsx
@@ -2,15 +2,20 @@ import Button from '@components/atoms/buttons/button';
 import Form from '@components/atoms/forms/form';
 import MagnifyingGlass from '@components/atoms/icons/magnifying-glass';
 import LabelledField, {
-  LabelledFieldProps,
+  type LabelledFieldProps,
 } from '@components/molecules/forms/labelled-field';
-import { useState, VFC } from 'react';
+import { FC, useState } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './search-form.module.scss';
 
 export type SearchFormProps = Pick<LabelledFieldProps, 'hideLabel'>;
 
-const SearchForm: VFC<SearchFormProps> = ({ hideLabel }) => {
+/**
+ * SearchForm component
+ *
+ * Render a search form.
+ */
+const SearchForm: FC<SearchFormProps> = ({ hideLabel }) => {
   const intl = useIntl();
   const fieldLabel = intl.formatMessage({
     defaultMessage: 'Search for:',
diff --git a/src/components/organisms/layout/cards-list.tsx b/src/components/organisms/layout/cards-list.tsx
index a53df0d..33ffe23 100644
--- a/src/components/organisms/layout/cards-list.tsx
+++ b/src/components/organisms/layout/cards-list.tsx
@@ -3,7 +3,7 @@ import List, {
   type ListProps,
 } from '@components/atoms/lists/list';
 import Card, { type CardProps } from '@components/molecules/layout/card';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './cards-list.module.scss';
 
 export type CardsListItem = Omit<
@@ -37,7 +37,7 @@ export type CardsListProps = {
  *
  * Return a list of Card components.
  */
-const CardsList: VFC<CardsListProps> = ({
+const CardsList: FC<CardsListProps> = ({
   coverFit,
   items,
   kind = 'unordered',
diff --git a/src/components/organisms/layout/footer.tsx b/src/components/organisms/layout/footer.tsx
index c9cb067..15bfa24 100644
--- a/src/components/organisms/layout/footer.tsx
+++ b/src/components/organisms/layout/footer.tsx
@@ -1,7 +1,9 @@
-import Copyright, { CopyrightProps } from '@components/atoms/layout/copyright';
+import Copyright, {
+  type CopyrightProps,
+} from '@components/atoms/layout/copyright';
 import BackToTop from '@components/molecules/buttons/back-to-top';
 import Nav, { type NavItem } from '@components/molecules/nav/nav';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './footer.module.scss';
 
 export type FooterProps = {
@@ -28,12 +30,7 @@ export type FooterProps = {
  *
  * Renders a footer with copyright and nav;
  */
-const Footer: VFC<FooterProps> = ({
-  className,
-  copyright,
-  navItems,
-  topId,
-}) => {
+const Footer: FC<FooterProps> = ({ className, copyright, navItems, topId }) => {
   return (
     <footer className={`${styles.wrapper} ${className}`}>
       <Copyright
diff --git a/src/components/organisms/layout/overview.tsx b/src/components/organisms/layout/overview.tsx
index 3f83342..42d0431 100644
--- a/src/components/organisms/layout/overview.tsx
+++ b/src/components/organisms/layout/overview.tsx
@@ -2,7 +2,7 @@ import ResponsiveImage, {
   type ResponsiveImageProps,
 } from '@components/molecules/images/responsive-image';
 import Meta, { type MetaMap } from '@components/molecules/layout/meta';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './overview.module.scss';
 
 export type OverviewProps = {
@@ -15,7 +15,7 @@ export type OverviewProps = {
  *
  * Render an overview.
  */
-const Overview: VFC<OverviewProps> = ({ cover, meta }) => {
+const Overview: FC<OverviewProps> = ({ cover, meta }) => {
   return (
     <div className={styles.wrapper}>
       {cover && (
diff --git a/src/components/organisms/layout/summary.stories.tsx b/src/components/organisms/layout/summary.stories.tsx
index 5214d70..b33acde 100644
--- a/src/components/organisms/layout/summary.stories.tsx
+++ b/src/components/organisms/layout/summary.stories.tsx
@@ -103,7 +103,7 @@ Summary.args = {
   cover: {
     alt: 'A cover',
     height: 480,
-    url: 'http://placeimg.com/640/480',
+    src: 'http://placeimg.com/640/480',
     width: 640,
   },
   excerpt:
diff --git a/src/components/organisms/layout/summary.test.tsx b/src/components/organisms/layout/summary.test.tsx
index ce87c0c..4944805 100644
--- a/src/components/organisms/layout/summary.test.tsx
+++ b/src/components/organisms/layout/summary.test.tsx
@@ -4,7 +4,7 @@ import Summary from './summary';
 const cover = {
   alt: 'A cover',
   height: 480,
-  url: 'http://placeimg.com/640/480',
+  src: 'http://placeimg.com/640/480',
   width: 640,
 };
 
diff --git a/src/components/organisms/layout/summary.tsx b/src/components/organisms/layout/summary.tsx
index 3624e5d..733a660 100644
--- a/src/components/organisms/layout/summary.tsx
+++ b/src/components/organisms/layout/summary.tsx
@@ -2,18 +2,18 @@ import ButtonLink from '@components/atoms/buttons/button-link';
 import Heading, { type HeadingLevel } from '@components/atoms/headings/heading';
 import Arrow from '@components/atoms/icons/arrow';
 import Link from '@components/atoms/links/link';
-import ResponsiveImage from '@components/molecules/images/responsive-image';
+import ResponsiveImage, {
+  type ResponsiveImageProps,
+} from '@components/molecules/images/responsive-image';
 import Meta, { type MetaItem } from '@components/molecules/layout/meta';
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './summary.module.scss';
 
-export type Cover = {
-  alt: string;
-  height: number;
-  url: string;
-  width: number;
-};
+export type Cover = Pick<
+  ResponsiveImageProps,
+  'alt' | 'src' | 'width' | 'height'
+>;
 
 export type RequiredMetaKey = 'publication';
 
@@ -35,11 +35,29 @@ export type OptionalMeta = {
 export type Meta = RequiredMeta & OptionalMeta;
 
 export type SummaryProps = {
+  /**
+   * The post cover.
+   */
   cover?: Cover;
+  /**
+   * The post excerpt.
+   */
   excerpt: string;
+  /**
+   * The post meta.
+   */
   meta: Meta;
+  /**
+   * The post title.
+   */
   title: string;
+  /**
+   * The heading level (hn).
+   */
   titleLevel?: HeadingLevel;
+  /**
+   * The post url.
+   */
   url: string;
 };
 
@@ -48,7 +66,7 @@ export type SummaryProps = {
  *
  * Render a page summary.
  */
-const Summary: VFC<SummaryProps> = ({
+const Summary: FC<SummaryProps> = ({
   cover,
   excerpt,
   meta,
@@ -57,18 +75,23 @@ const Summary: VFC<SummaryProps> = ({
   url,
 }) => {
   const intl = useIntl();
+  const readMore = intl.formatMessage(
+    {
+      defaultMessage: 'Read more<a11y> about {title}</a11y>',
+      description: 'Summary: read more link',
+      id: 'Zpgv+f',
+    },
+    {
+      title,
+      a11y: (chunks: string) => (
+        <span className="screen-reader-text">{chunks}</span>
+      ),
+    }
+  );
 
   return (
     <article className={styles.wrapper}>
-      {cover && (
-        <ResponsiveImage
-          alt={cover.alt}
-          src={cover.url}
-          width={cover.width}
-          height={cover.height}
-          className={styles.cover}
-        />
-      )}
+      {cover && <ResponsiveImage className={styles.cover} {...cover} />}
       <header className={styles.header}>
         <Link href={url}>
           <Heading level={titleLevel} className={styles.title}>
@@ -79,20 +102,10 @@ const Summary: VFC<SummaryProps> = ({
       <div className={styles.body}>
         {excerpt}
         <ButtonLink target={url} className={styles['read-more']}>
-          {intl.formatMessage(
-            {
-              defaultMessage: 'Read more<a11y> about {title}</a11y>',
-              description: 'Summary: read more link',
-              id: 'Zpgv+f',
-            },
-            {
-              title,
-              a11y: (chunks: string) => (
-                <span className="screen-reader-text">{chunks}</span>
-              ),
-            }
-          )}
-          <Arrow direction="right" />
+          <>
+            {readMore}
+            <Arrow direction="right" />
+          </>
         </ButtonLink>
       </div>
       <footer className={styles.footer}>
diff --git a/src/components/organisms/modals/search-modal.tsx b/src/components/organisms/modals/search-modal.tsx
index 883b783..0e0ceed 100644
--- a/src/components/organisms/modals/search-modal.tsx
+++ b/src/components/organisms/modals/search-modal.tsx
@@ -1,5 +1,5 @@
-import Modal from '@components/molecules/modals/modal';
-import { VFC } from 'react';
+import Modal, { type ModalProps } from '@components/molecules/modals/modal';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import SearchForm from '../forms/search-form';
 import styles from './search-modal.module.scss';
@@ -8,7 +8,7 @@ export type SearchModalProps = {
   /**
    * Set additional classnames to modal wrapper.
    */
-  className?: string;
+  className?: ModalProps['className'];
 };
 
 /**
@@ -16,7 +16,7 @@ export type SearchModalProps = {
  *
  * Render a search form modal.
  */
-const SearchModal: VFC<SearchModalProps> = ({ className }) => {
+const SearchModal: FC<SearchModalProps> = ({ className }) => {
   const intl = useIntl();
   const modalTitle = intl.formatMessage({
     defaultMessage: 'Search',
diff --git a/src/components/organisms/modals/settings-modal.tsx b/src/components/organisms/modals/settings-modal.tsx
index 25d6f6f..20d2605 100644
--- a/src/components/organisms/modals/settings-modal.tsx
+++ b/src/components/organisms/modals/settings-modal.tsx
@@ -1,10 +1,12 @@
 import Form from '@components/atoms/forms/form';
-import AckeeSelect from '@components/molecules/forms/ackee-select';
+import AckeeSelect, {
+  type AckeeSelectProps,
+} from '@components/molecules/forms/ackee-select';
 import MotionToggle from '@components/molecules/forms/motion-toggle';
 import PrismThemeToggle from '@components/molecules/forms/prism-theme-toggle';
 import ThemeToggle from '@components/molecules/forms/theme-toggle';
-import Modal from '@components/molecules/modals/modal';
-import { VFC } from 'react';
+import Modal, { type ModalProps } from '@components/molecules/modals/modal';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './settings-modal.module.scss';
 
@@ -12,11 +14,11 @@ export type SettingsModalProps = {
   /**
    * Set additional classnames to the modal wrapper.
    */
-  className?: string;
+  className?: ModalProps['className'];
   /**
    * Set additional classnames to the tooltip wrapper.
    */
-  tooltipClassName?: string;
+  tooltipClassName?: AckeeSelectProps['tooltipClassName'];
 };
 
 /**
@@ -24,7 +26,7 @@ export type SettingsModalProps = {
  *
  * Render a modal with settings options.
  */
-const SettingsModal: VFC<SettingsModalProps> = ({
+const SettingsModal: FC<SettingsModalProps> = ({
   className = '',
   tooltipClassName = '',
 }) => {
diff --git a/src/components/organisms/toolbar/main-nav.tsx b/src/components/organisms/toolbar/main-nav.tsx
index 50bbe8b..35e3fd6 100644
--- a/src/components/organisms/toolbar/main-nav.tsx
+++ b/src/components/organisms/toolbar/main-nav.tsx
@@ -1,17 +1,20 @@
 import Checkbox, { type CheckboxProps } from '@components/atoms/forms/checkbox';
 import Label from '@components/atoms/forms/label';
 import Hamburger from '@components/atoms/icons/hamburger';
-import Nav, { type NavItem } from '@components/molecules/nav/nav';
-import { VFC } from 'react';
+import Nav, {
+  type NavProps,
+  type NavItem,
+} from '@components/molecules/nav/nav';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
-import sharedStyles from './toolbar-items.module.scss';
 import mainNavStyles from './main-nav.module.scss';
+import sharedStyles from './toolbar-items.module.scss';
 
 export type MainNavProps = {
   /**
    * Set additional classnames to the nav element.
    */
-  className?: string;
+  className?: NavProps['className'];
   /**
    * The button state.
    */
@@ -26,7 +29,12 @@ export type MainNavProps = {
   setIsActive: CheckboxProps['setValue'];
 };
 
-const MainNav: VFC<MainNavProps> = ({
+/**
+ * MainNav component
+ *
+ * Render the main navigation.
+ */
+const MainNav: FC<MainNavProps> = ({
   className = '',
   isActive,
   items,
diff --git a/src/components/organisms/toolbar/search.tsx b/src/components/organisms/toolbar/search.tsx
index 070bce0..72cd576 100644
--- a/src/components/organisms/toolbar/search.tsx
+++ b/src/components/organisms/toolbar/search.tsx
@@ -1,17 +1,17 @@
-import Checkbox, { CheckboxProps } from '@components/atoms/forms/checkbox';
+import Checkbox, { type CheckboxProps } from '@components/atoms/forms/checkbox';
 import Label from '@components/atoms/forms/label';
 import MagnifyingGlass from '@components/atoms/icons/magnifying-glass';
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
-import SearchModal from '../modals/search-modal';
-import sharedStyles from './toolbar-items.module.scss';
+import SearchModal, { type SearchModalProps } from '../modals/search-modal';
 import searchStyles from './search.module.scss';
+import sharedStyles from './toolbar-items.module.scss';
 
 export type SearchProps = {
   /**
    * Set additional classnames to the modal wrapper.
    */
-  className?: string;
+  className?: SearchModalProps['className'];
   /**
    * The button state.
    */
@@ -22,11 +22,7 @@ export type SearchProps = {
   setIsActive: CheckboxProps['setValue'];
 };
 
-const Search: VFC<SearchProps> = ({
-  className = '',
-  isActive,
-  setIsActive,
-}) => {
+const Search: FC<SearchProps> = ({ className = '', isActive, setIsActive }) => {
   const intl = useIntl();
   const label = isActive
     ? intl.formatMessage({
diff --git a/src/components/organisms/toolbar/settings.tsx b/src/components/organisms/toolbar/settings.tsx
index 88539fb..3b10226 100644
--- a/src/components/organisms/toolbar/settings.tsx
+++ b/src/components/organisms/toolbar/settings.tsx
@@ -1,11 +1,13 @@
-import Checkbox, { CheckboxProps } from '@components/atoms/forms/checkbox';
+import Checkbox, { type CheckboxProps } from '@components/atoms/forms/checkbox';
 import Label from '@components/atoms/forms/label';
 import Cog from '@components/atoms/icons/cog';
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
-import SettingsModal from '../modals/settings-modal';
-import sharedStyles from './toolbar-items.module.scss';
+import SettingsModal, {
+  type SettingsModalProps,
+} from '../modals/settings-modal';
 import settingsStyles from './settings.module.scss';
+import sharedStyles from './toolbar-items.module.scss';
 
 export type SettingsProps = {
   /**
@@ -23,10 +25,10 @@ export type SettingsProps = {
   /**
    * Set additional classnames to the tooltip wrapper.
    */
-  tooltipClassName?: string;
+  tooltipClassName?: SettingsModalProps['tooltipClassName'];
 };
 
-const Settings: VFC<SettingsProps> = ({
+const Settings: FC<SettingsProps> = ({
   className = '',
   isActive,
   setIsActive,
diff --git a/src/components/organisms/toolbar/toolbar.tsx b/src/components/organisms/toolbar/toolbar.tsx
index 81e80cf..f1ce530 100644
--- a/src/components/organisms/toolbar/toolbar.tsx
+++ b/src/components/organisms/toolbar/toolbar.tsx
@@ -1,4 +1,4 @@
-import { useState, VFC } from 'react';
+import { FC, useState } from 'react';
 import MainNav, { type MainNavProps } from '../toolbar/main-nav';
 import Search from '../toolbar/search';
 import Settings from '../toolbar/settings';
@@ -20,7 +20,7 @@ export type ToolbarProps = {
  *
  * Render the website toolbar.
  */
-const Toolbar: VFC<ToolbarProps> = ({ className = '', nav }) => {
+const Toolbar: FC<ToolbarProps> = ({ className = '', nav }) => {
   const [isNavOpened, setIsNavOpened] = useState<boolean>(false);
   const [isSettingsOpened, setIsSettingsOpened] = useState<boolean>(false);
   const [isSearchOpened, setIsSearchOpened] = useState<boolean>(false);
diff --git a/src/components/organisms/widgets/image-widget.tsx b/src/components/organisms/widgets/image-widget.tsx
index 928d5ea..ba04c6a 100644
--- a/src/components/organisms/widgets/image-widget.tsx
+++ b/src/components/organisms/widgets/image-widget.tsx
@@ -1,28 +1,16 @@
-import ResponsiveImage from '@components/molecules/images/responsive-image';
+import ResponsiveImage, {
+  type ResponsiveImageProps,
+} from '@components/molecules/images/responsive-image';
 import Widget, { type WidgetProps } from '@components/molecules/layout/widget';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './image-widget.module.scss';
 
 export type Alignment = 'left' | 'center' | 'right';
 
-export type Image = {
-  /**
-   * An alternative text for the image.
-   */
-  alt: string;
-  /**
-   * The image height.
-   */
-  height: number;
-  /**
-   * The image source.
-   */
-  src: string;
-  /**
-   * The image width.
-   */
-  width: number;
-};
+export type Image = Pick<
+  ResponsiveImageProps,
+  'alt' | 'height' | 'src' | 'width'
+>;
 
 export type ImageWidgetProps = Pick<
   WidgetProps,
@@ -35,7 +23,7 @@ export type ImageWidgetProps = Pick<
   /**
    * Add a caption to the image.
    */
-  description?: string;
+  description?: ResponsiveImageProps['caption'];
   /**
    * An object describing the image.
    */
@@ -43,7 +31,7 @@ export type ImageWidgetProps = Pick<
   /**
    * Add a link to the image.
    */
-  url?: string;
+  url?: ResponsiveImageProps['target'];
 };
 
 /**
@@ -51,7 +39,7 @@ export type ImageWidgetProps = Pick<
  *
  * Renders a widget that print an image and an optional text.
  */
-const ImageWidget: VFC<ImageWidgetProps> = ({
+const ImageWidget: FC<ImageWidgetProps> = ({
   alignment = 'left',
   description,
   img,
diff --git a/src/components/organisms/widgets/links-list-widget.tsx b/src/components/organisms/widgets/links-list-widget.tsx
index 155354e..559d0b6 100644
--- a/src/components/organisms/widgets/links-list-widget.tsx
+++ b/src/components/organisms/widgets/links-list-widget.tsx
@@ -1,8 +1,11 @@
 import Link from '@components/atoms/links/link';
-import List, { ListProps, type ListItem } from '@components/atoms/lists/list';
+import List, {
+  type ListProps,
+  type ListItem,
+} from '@components/atoms/lists/list';
 import Widget, { type WidgetProps } from '@components/molecules/layout/widget';
 import { slugify } from '@utils/helpers/slugify';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './links-list-widget.module.scss';
 
 export type LinksListItems = {
@@ -33,7 +36,7 @@ export type LinksListWidgetProps = Pick<WidgetProps, 'level' | 'title'> &
  *
  * Render a list of links inside a widget.
  */
-const LinksListWidget: VFC<LinksListWidgetProps> = ({
+const LinksListWidget: FC<LinksListWidgetProps> = ({
   items,
   kind = 'unordered',
   ...props
diff --git a/src/components/organisms/widgets/sharing.tsx b/src/components/organisms/widgets/sharing.tsx
index ccd3a21..571c1a4 100644
--- a/src/components/organisms/widgets/sharing.tsx
+++ b/src/components/organisms/widgets/sharing.tsx
@@ -2,7 +2,7 @@ import SharingLink, {
   type SharingMedium,
 } from '@components/atoms/links/sharing-link';
 import Widget, { type WidgetProps } from '@components/molecules/layout/widget';
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './sharing.module.scss';
 
@@ -21,7 +21,7 @@ export type SharingData = {
   url: string;
 };
 
-export type SharingProps = WidgetProps & {
+export type SharingProps = Omit<WidgetProps, 'children'> & {
   /**
    * The page data to share.
    */
@@ -37,7 +37,7 @@ export type SharingProps = WidgetProps & {
  *
  * Render a list of sharing links inside a widget.
  */
-const Sharing: VFC<SharingProps> = ({ data, media, ...props }) => {
+const Sharing: FC<SharingProps> = ({ data, media, ...props }) => {
   const intl = useIntl();
 
   /**
-- 
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/organisms/layout/cards-list.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 802548124071039f83ebfa2605e839ded82b5689 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Fri, 20 May 2022 22:22:35 +0200
Subject: chore: update some styles

* Improve wp-blocks-columns, wp-block-quote and some images
* Make Sharing widget consistent between pages
* Remove margin option from lists
* Fix Topic logo alignement
---
 src/components/atoms/lists/list.module.scss        | 10 ------
 src/components/atoms/lists/list.tsx                | 12 ++-----
 src/components/organisms/layout/cards-list.tsx     |  1 -
 .../organisms/layout/posts-list.module.scss        |  9 ++++-
 src/components/organisms/layout/posts-list.tsx     |  4 +--
 .../organisms/widgets/links-list-widget.tsx        |  1 -
 src/pages/article/[slug].tsx                       |  5 +--
 src/pages/projets/[slug].tsx                       |  9 +++--
 src/pages/sujet/[slug].tsx                         |  2 +-
 src/styles/pages/partials/_article-wp-blocks.scss  | 38 +++++++++++-----------
 src/styles/pages/project.module.scss               | 10 ++++++
 src/styles/pages/topic.module.scss                 |  2 +-
 12 files changed, 51 insertions(+), 52 deletions(-)
 create mode 100644 src/styles/pages/project.module.scss

(limited to 'src/components/organisms/layout/cards-list.tsx')

diff --git a/src/components/atoms/lists/list.module.scss b/src/components/atoms/lists/list.module.scss
index f647072..95f9b40 100644
--- a/src/components/atoms/lists/list.module.scss
+++ b/src/components/atoms/lists/list.module.scss
@@ -7,10 +7,6 @@
     color: var(--color-primary-dark);
   }
 
-  & & {
-    margin-top: var(--spacing-2xs);
-  }
-
   &--ordered {
     padding: 0;
     counter-reset: li;
@@ -33,12 +29,6 @@
     padding: 0 0 0 var(--spacing-sm);
   }
 
-  &--has-margin &__item {
-    &:not(:last-child) {
-      margin-bottom: var(--spacing-2xs);
-    }
-  }
-
   &--flex {
     @extend %reset-list;
 
diff --git a/src/components/atoms/lists/list.tsx b/src/components/atoms/lists/list.tsx
index 711ade1..aa0a241 100644
--- a/src/components/atoms/lists/list.tsx
+++ b/src/components/atoms/lists/list.tsx
@@ -33,10 +33,6 @@ export type ListProps = {
    * The list kind.
    */
   kind?: 'ordered' | 'unordered' | 'flex';
-  /**
-   * Set margin between list items. Default: true.
-   */
-  withMargin?: boolean;
 };
 
 /**
@@ -49,11 +45,9 @@ const List: FC<ListProps> = ({
   items,
   itemsClassName = '',
   kind = 'unordered',
-  withMargin = true,
 }) => {
   const ListTag = kind === 'ordered' ? 'ol' : 'ul';
   const kindClass = `list--${kind}`;
-  const marginClass = withMargin ? 'list--has-margin' : 'list--no-margin';
 
   /**
    * Retrieve the list items.
@@ -66,7 +60,7 @@ const List: FC<ListProps> = ({
         {value}
         {child && (
           <ListTag
-            className={`${styles.list} ${styles[kindClass]} ${styles[marginClass]} ${className}`}
+            className={`${styles.list} ${styles[kindClass]} ${className}`}
           >
             {getItems(child)}
           </ListTag>
@@ -76,9 +70,7 @@ const List: FC<ListProps> = ({
   };
 
   return (
-    <ListTag
-      className={`${styles.list} ${styles[kindClass]} ${styles[marginClass]} ${className}`}
-    >
+    <ListTag className={`${styles.list} ${styles[kindClass]} ${className}`}>
       {getItems(items)}
     </ListTag>
   );
diff --git a/src/components/organisms/layout/cards-list.tsx b/src/components/organisms/layout/cards-list.tsx
index 1558d7c..0168bb2 100644
--- a/src/components/organisms/layout/cards-list.tsx
+++ b/src/components/organisms/layout/cards-list.tsx
@@ -77,7 +77,6 @@ const CardsList: FC<CardsListProps> = ({
     <List
       kind="flex"
       items={getCards(items)}
-      withMargin={false}
       className={`${styles.wrapper} ${styles[kindModifier]} ${className}`}
     />
   );
diff --git a/src/components/organisms/layout/posts-list.module.scss b/src/components/organisms/layout/posts-list.module.scss
index a006914..b09bb12 100644
--- a/src/components/organisms/layout/posts-list.module.scss
+++ b/src/components/organisms/layout/posts-list.module.scss
@@ -3,6 +3,10 @@
 @use "@styles/abstracts/placeholders";
 
 .section {
+  &:not(:last-of-type) {
+    margin-bottom: var(--spacing-md);
+  }
+
   @include mix.media("screen") {
     @include mix.dimensions("md") {
       display: grid;
@@ -17,8 +21,11 @@
   @extend %reset-ordered-list;
 
   .item {
-    margin-bottom: var(--spacing-md);
     border-bottom: fun.convert-px(1) solid var(--color-border);
+
+    &:not(:last-child) {
+      margin-bottom: var(--spacing-md);
+    }
   }
 }
 
diff --git a/src/components/organisms/layout/posts-list.tsx b/src/components/organisms/layout/posts-list.tsx
index 50192dd..608130e 100644
--- a/src/components/organisms/layout/posts-list.tsx
+++ b/src/components/organisms/layout/posts-list.tsx
@@ -100,6 +100,8 @@ const PostsList: FC<PostsListProps> = ({
   const isMounted = useIsMounted(listRef);
   const { blog } = useSettings();
 
+  const lastPostId = posts.length ? posts[posts.length - 1].id : 0;
+
   /**
    * Retrieve the list of posts.
    *
@@ -111,8 +113,6 @@ const PostsList: FC<PostsListProps> = ({
     allPosts: Post[],
     headingLevel: HeadingLevel = 2
   ): JSX.Element => {
-    const lastPostId = allPosts[allPosts.length - 1].id;
-
     return (
       <ol className={styles.list} ref={listRef}>
         {allPosts.map(({ id, ...post }) => (
diff --git a/src/components/organisms/widgets/links-list-widget.tsx b/src/components/organisms/widgets/links-list-widget.tsx
index 90dae4a..a9c677b 100644
--- a/src/components/organisms/widgets/links-list-widget.tsx
+++ b/src/components/organisms/widgets/links-list-widget.tsx
@@ -75,7 +75,6 @@ const LinksListWidget: FC<LinksListWidgetProps> = ({
       <List
         items={getListItems(items)}
         kind={kind}
-        withMargin={false}
         className={`${styles.list} ${styles[listKindClass]} ${className}`}
         itemsClassName={styles.list__item}
       />
diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx
index 7abbabc..812fffe 100644
--- a/src/pages/article/[slug].tsx
+++ b/src/pages/article/[slug].tsx
@@ -99,10 +99,7 @@ const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({
             target={`/sujet/${topic.slug}`}
             className={styles.btn}
           >
-            {topic.logo && (
-              <ResponsiveImage className={styles.btn__icon} {...topic.logo} />
-            )}{' '}
-            {topic.name}
+            {topic.logo && <ResponsiveImage {...topic.logo} />} {topic.name}
           </ButtonLink>
         );
       }),
diff --git a/src/pages/projets/[slug].tsx b/src/pages/projets/[slug].tsx
index cf7d0dc..e9b36fa 100644
--- a/src/pages/projets/[slug].tsx
+++ b/src/pages/projets/[slug].tsx
@@ -10,6 +10,7 @@ import { getLayout } from '@components/templates/layout/layout';
 import PageLayout, {
   PageLayoutProps,
 } from '@components/templates/page/page-layout';
+import styles from '@styles/pages/project.module.scss';
 import {
   type NextPageWithLayout,
   type ProjectPreview,
@@ -53,7 +54,7 @@ const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => {
   const components: NestedMDXComponents = {
     Code: (props) => <Code {...props} />,
     Gallery: (props) => <Gallery {...props} />,
-    Image: (props) => <ResponsiveImage {...props} />,
+    Image: (props) => <ResponsiveImage withBorders={true} {...props} />,
     Link: (props) => <Link {...props} />,
     pre: ({ children }) => <Code {...children.props} />,
   };
@@ -66,7 +67,10 @@ const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => {
 
   const headerMeta: PageLayoutProps['headerMeta'] = {
     publication: { date: dates.publication },
-    update: dates.update ? { date: dates.update } : undefined,
+    update:
+      dates.update && dates.update !== dates.publication
+        ? { date: dates.update }
+        : undefined,
   };
 
   /**
@@ -202,6 +206,7 @@ const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => {
               'linkedin',
               'twitter',
             ]}
+            className={styles.widget}
           />,
         ]}
       >
diff --git a/src/pages/sujet/[slug].tsx b/src/pages/sujet/[slug].tsx
index 271f4ec..b4845eb 100644
--- a/src/pages/sujet/[slug].tsx
+++ b/src/pages/sujet/[slug].tsx
@@ -191,7 +191,7 @@ const TopicPage: NextPageWithLayout<TopicPageProps> = ({
             : []
         }
       >
-        <div dangerouslySetInnerHTML={{ __html: content }} />
+        {content && <div dangerouslySetInnerHTML={{ __html: content }} />}
         {articles && (
           <>
             <Heading level={2}>
diff --git a/src/styles/pages/partials/_article-wp-blocks.scss b/src/styles/pages/partials/_article-wp-blocks.scss
index 33e59a1..86be062 100644
--- a/src/styles/pages/partials/_article-wp-blocks.scss
+++ b/src/styles/pages/partials/_article-wp-blocks.scss
@@ -7,13 +7,12 @@
     margin: var(--spacing-sm) 0;
     padding: var(--spacing-sm);
     position: relative;
-    border: fun.convert-px(1) solid var(--color-primary-lighter);
-    border-left: fun.convert-px(5) solid var(--color-primary-lighter);
-    box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow),
-      fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0
-        var(--color-shadow-light),
-      fun.convert-px(3) fun.convert-px(3) fun.convert-px(6) 0
-        var(--color-shadow-light);
+    border: fun.convert-px(1) solid var(--color-border-dark);
+    border-left: fun.convert-px(5) solid var(--color-border-dark);
+    box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) 0
+        var(--color-shadow),
+      fun.convert-px(3) fun.convert-px(3) fun.convert-px(4) 0
+        var(--color-shadow);
     font-style: italic;
 
     > *:last-child {
@@ -21,6 +20,7 @@
     }
 
     cite {
+      color: var(--color-fg-light);
       font-size: var(--font-size-sm);
       font-style: normal;
       font-weight: 600;
@@ -62,18 +62,26 @@
     }
   }
 
+  .wp-block-image,
+  .wp-block-video {
+    padding: fun.convert-px(4);
+    border: fun.convert-px(1) solid var(--color-border);
+    box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) 0
+      var(--color-shadow);
+  }
+
   .wp-block-image {
     display: flex;
     flex-flow: column;
     width: fit-content;
     margin: 0 auto;
-    padding: fun.convert-px(4);
     position: relative;
-    border: fun.convert-px(1) solid var(--color-border);
-    box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) 0
-      var(--color-shadow);
     text-align: center;
 
+    img {
+      margin: auto;
+    }
+
     figcaption {
       margin-top: fun.convert-px(4);
       font-size: var(--font-size-sm);
@@ -166,12 +174,4 @@
       object-fit: cover;
     }
   }
-
-  .wp-block-video {
-    box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow),
-      fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0
-        var(--color-shadow-light),
-      fun.convert-px(3) fun.convert-px(3) fun.convert-px(6) 0
-        var(--color-shadow-light);
-  }
 }
diff --git a/src/styles/pages/project.module.scss b/src/styles/pages/project.module.scss
new file mode 100644
index 0000000..3b1b5cc
--- /dev/null
+++ b/src/styles/pages/project.module.scss
@@ -0,0 +1,10 @@
+@use "@styles/abstracts/mixins" as mix;
+
+.widget {
+  @include mix.media("screen") {
+    @include mix.dimensions("md") {
+      width: min-content;
+      gap: var(--spacing-2xs);
+    }
+  }
+}
diff --git a/src/styles/pages/topic.module.scss b/src/styles/pages/topic.module.scss
index badb694..fd5f742 100644
--- a/src/styles/pages/topic.module.scss
+++ b/src/styles/pages/topic.module.scss
@@ -2,5 +2,5 @@
 
 .logo {
   max-width: fun.convert-px(50);
-  margin-right: var(--spacing-xs);
+  margin: 0 var(--spacing-xs) 0 0;
 }
-- 
cgit v1.2.3


From 34e216546151eaf8a0a3cbb0bc8b65dae4c63bf2 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Mon, 23 May 2022 14:07:02 +0200
Subject: refactor: reduce the number of data transformation

---
 src/components/molecules/layout/card.tsx           |  22 +---
 src/components/organisms/layout/cards-list.tsx     |  36 +++---
 .../organisms/layout/comment.fixture.tsx           |  41 +++++++
 .../organisms/layout/comment.stories.tsx           |  28 ++---
 src/components/organisms/layout/comment.test.tsx   |  39 ++-----
 src/components/organisms/layout/comment.tsx        |  98 ++++++++---------
 .../organisms/layout/comments-list.fixture.tsx     | 106 ++++++++++++++++++
 .../organisms/layout/comments-list.stories.tsx     |  59 +---------
 .../organisms/layout/comments-list.test.tsx        |  64 +----------
 src/components/organisms/layout/comments-list.tsx  |  11 +-
 .../organisms/layout/posts-list.fixture.tsx        |  61 +++++++++++
 .../organisms/layout/posts-list.stories.tsx        |  64 +----------
 .../organisms/layout/posts-list.test.tsx           |  64 +----------
 src/components/organisms/layout/posts-list.tsx     |   4 +-
 .../organisms/layout/summary.fixture.tsx           |  25 +++++
 .../organisms/layout/summary.stories.tsx           |  28 +----
 src/components/organisms/layout/summary.test.tsx   |  44 ++------
 src/components/organisms/layout/summary.tsx        |  73 ++++---------
 .../templates/page/page-layout.stories.tsx         | 121 +--------------------
 src/components/templates/page/page-layout.test.tsx |  13 +--
 src/pages/article/[slug].tsx                       |  43 ++------
 src/pages/blog/index.tsx                           |  75 +------------
 src/pages/blog/page/[number].tsx                   |  66 +----------
 src/pages/projets/[slug].tsx                       |  14 ++-
 src/pages/projets/index.tsx                        |   4 +-
 src/pages/recherche/index.tsx                      |  76 +------------
 src/pages/sujet/[slug].tsx                         |  28 +----
 src/pages/thematique/[slug].tsx                    |  28 +----
 src/services/graphql/articles.query.ts             |   7 --
 src/services/graphql/articles.ts                   |   3 +-
 src/services/graphql/thematics.query.ts            |   9 --
 src/services/graphql/thematics.ts                  |   1 -
 src/services/graphql/topics.query.ts               |   9 --
 src/services/graphql/topics.ts                     |   1 -
 src/ts/types/app.ts                                |   5 +-
 src/ts/types/mdx.ts                                |   5 +-
 src/ts/types/raw-data.ts                           |   3 +-
 src/utils/helpers/pages.ts                         |  49 ++++++---
 38 files changed, 456 insertions(+), 971 deletions(-)
 create mode 100644 src/components/organisms/layout/comment.fixture.tsx
 create mode 100644 src/components/organisms/layout/comments-list.fixture.tsx
 create mode 100644 src/components/organisms/layout/posts-list.fixture.tsx
 create mode 100644 src/components/organisms/layout/summary.fixture.tsx

(limited to 'src/components/organisms/layout/cards-list.tsx')

diff --git a/src/components/molecules/layout/card.tsx b/src/components/molecules/layout/card.tsx
index e465f76..68e364a 100644
--- a/src/components/molecules/layout/card.tsx
+++ b/src/components/molecules/layout/card.tsx
@@ -1,5 +1,6 @@
 import ButtonLink from '@components/atoms/buttons/button-link';
 import Heading, { type HeadingLevel } from '@components/atoms/headings/heading';
+import { type Image } from '@ts/types/app';
 import { FC } from 'react';
 import ResponsiveImage, {
   type ResponsiveImageProps,
@@ -7,25 +8,6 @@ import ResponsiveImage, {
 import styles from './card.module.scss';
 import Meta, { type MetaData } from './meta';
 
-export type Cover = {
-  /**
-   * The cover alternative text.
-   */
-  alt: string;
-  /**
-   * The cover height.
-   */
-  height: number;
-  /**
-   * The cover source.
-   */
-  src: string;
-  /**
-   * The cover width.
-   */
-  width: number;
-};
-
 export type CardProps = {
   /**
    * Set additional classnames to the card wrapper.
@@ -34,7 +16,7 @@ export type CardProps = {
   /**
    * The card cover.
    */
-  cover?: Cover;
+  cover?: Image;
   /**
    * The cover fit. Default: cover.
    */
diff --git a/src/components/organisms/layout/cards-list.tsx b/src/components/organisms/layout/cards-list.tsx
index 0168bb2..1feddd0 100644
--- a/src/components/organisms/layout/cards-list.tsx
+++ b/src/components/organisms/layout/cards-list.tsx
@@ -10,32 +10,24 @@ export type CardsListItem = Omit<
   CardProps,
   'className' | 'coverFit' | 'titleLevel'
 > & {
-  id: string;
-};
-
-export type CardsListProps = {
-  /**
-   * Set additional classnames to the list wrapper.
-   */
-  className?: string;
-  /**
-   * The cover fit.
-   */
-  coverFit?: CardProps['coverFit'];
   /**
-   * The cards data.
+   * The card id.
    */
-  items: CardsListItem[];
-  /**
-   * The list kind. Either ordered or unordered.
-   */
-  kind?: ListProps['kind'];
-  /**
-   * The title level (hn).
-   */
-  titleLevel: CardProps['titleLevel'];
+  id: string;
 };
 
+export type CardsListProps = Pick<CardProps, 'coverFit' | 'titleLevel'> &
+  Pick<ListProps, 'kind'> & {
+    /**
+     * Set additional classnames to the list wrapper.
+     */
+    className?: string;
+    /**
+     * The cards data.
+     */
+    items: CardsListItem[];
+  };
+
 /**
  * CardsList component
  *
diff --git a/src/components/organisms/layout/comment.fixture.tsx b/src/components/organisms/layout/comment.fixture.tsx
new file mode 100644
index 0000000..0118139
--- /dev/null
+++ b/src/components/organisms/layout/comment.fixture.tsx
@@ -0,0 +1,41 @@
+import { getFormattedDate, getFormattedTime } from '@utils/helpers/dates';
+import { CommentProps } from './comment';
+
+export const author = {
+  avatar: {
+    alt: 'Author avatar',
+    height: 480,
+    src: 'http://placeimg.com/640/480',
+    width: 640,
+  },
+  name: 'Armand',
+  website: 'https://www.armandphilippot.com/',
+};
+
+export const content =
+  'Harum aut cumque iure fugit neque sequi cupiditate repudiandae laudantium. Ratione aut assumenda qui illum voluptas accusamus quis officiis exercitationem. Consectetur est harum eius perspiciatis officiis nihil. Aut corporis minima debitis adipisci possimus debitis et.';
+
+export const date = '2021-04-03 23:04:24';
+
+export const meta = {
+  author,
+  date,
+};
+
+export const id = 5;
+
+export const saveComment = async () => {
+  /** Do nothing. */
+};
+
+export const data: CommentProps = {
+  approved: true,
+  content,
+  id,
+  meta,
+  parentId: 0,
+  saveComment,
+};
+
+export const formattedDate = getFormattedDate(date);
+export const formattedTime = getFormattedTime(date);
diff --git a/src/components/organisms/layout/comment.stories.tsx b/src/components/organisms/layout/comment.stories.tsx
index fa4bba9..7a8ac95 100644
--- a/src/components/organisms/layout/comment.stories.tsx
+++ b/src/components/organisms/layout/comment.stories.tsx
@@ -1,5 +1,6 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
 import CommentComponent from './comment';
+import { data } from './comment.fixture';
 
 const saveComment = async () => {
   /** Do nothing. */
@@ -9,7 +10,7 @@ const saveComment = async () => {
  * Comment - Storybook Meta
  */
 export default {
-  title: 'Organisms/Layout',
+  title: 'Organisms/Layout/Comment',
   component: CommentComponent,
   args: {
     canReply: true,
@@ -110,17 +111,18 @@ const Template: ComponentStory<typeof CommentComponent> = (args) => (
 );
 
 /**
- * Layout Stories - Comment
+ * Layout Stories - Approved
  */
-export const Comment = Template.bind({});
-Comment.args = {
-  author: {
-    avatar: 'http://placeimg.com/640/480',
-    name: 'Armand',
-    url: 'https://www.armandphilippot.com/',
-  },
-  content:
-    'Harum aut cumque iure fugit neque sequi cupiditate repudiandae laudantium. Ratione aut assumenda qui illum voluptas accusamus quis officiis exercitationem. Consectetur est harum eius perspiciatis officiis nihil. Aut corporis minima debitis adipisci possimus debitis et.',
-  id: 2,
-  publication: '2021-04-03 23:04:24',
+export const Approved = Template.bind({});
+Approved.args = {
+  ...data,
+};
+
+/**
+ * Layout Stories - Unapproved
+ */
+export const Unapproved = Template.bind({});
+Unapproved.args = {
+  ...data,
+  approved: false,
 };
diff --git a/src/components/organisms/layout/comment.test.tsx b/src/components/organisms/layout/comment.test.tsx
index 02a51dc..490a52b 100644
--- a/src/components/organisms/layout/comment.test.tsx
+++ b/src/components/organisms/layout/comment.test.tsx
@@ -1,48 +1,31 @@
 import { render, screen } from '@test-utils';
-import { getFormattedDate, getFormattedTime } from '@utils/helpers/dates';
 import Comment from './comment';
-
-const author = {
-  avatar: 'http://placeimg.com/640/480',
-  name: 'Your name',
-  url: 'https://www.example.test/',
-};
-const content =
-  'Harum aut cumque iure fugit neque sequi cupiditate repudiandae laudantium. Ratione aut assumenda qui illum voluptas accusamus quis officiis exercitationem. Consectetur est harum eius perspiciatis officiis nihil. Aut corporis minima debitis adipisci possimus debitis et.';
-const publication = '2021-04-03 23:04:24';
-const id = 5;
-const saveComment = async () => {
-  /** Do nothing. */
-};
-const data = {
+import {
   author,
-  content,
+  data,
+  formattedDate,
+  formattedTime,
   id,
-  publication,
-  saveComment,
-};
-
-const formattedDate = getFormattedDate(publication);
-const formattedTime = getFormattedTime(publication);
+} from './comment.fixture';
 
 describe('Comment', () => {
   it('renders an avatar', () => {
-    render(<Comment {...data} />);
+    render(<Comment canReply={true} {...data} />);
     expect(
       screen.getByRole('img', { name: 'Your name avatar' })
     ).toBeInTheDocument();
   });
 
   it('renders the author website url', () => {
-    render(<Comment {...data} />);
+    render(<Comment canReply={true} {...data} />);
     expect(screen.getByRole('link', { name: author.name })).toHaveAttribute(
       'href',
-      author.url
+      author.website
     );
   });
 
   it('renders a permalink to the comment', () => {
-    render(<Comment {...data} />);
+    render(<Comment canReply={true} {...data} />);
     expect(
       screen.getByRole('link', {
         name: `${formattedDate} at ${formattedTime}`,
@@ -51,12 +34,12 @@ describe('Comment', () => {
   });
 
   it('renders a reply button', () => {
-    render(<Comment {...data} canReply={true} />);
+    render(<Comment canReply={true} {...data} />);
     expect(screen.getByRole('button', { name: 'Reply' })).toBeInTheDocument();
   });
 
   it('does not render a reply button', () => {
-    render(<Comment {...data} canReply={false} />);
+    render(<Comment canReply={false} {...data} />);
     expect(
       screen.queryByRole('button', { name: 'Reply' })
     ).not.toBeInTheDocument();
diff --git a/src/components/organisms/layout/comment.tsx b/src/components/organisms/layout/comment.tsx
index 6df393b..1dc2c71 100644
--- a/src/components/organisms/layout/comment.tsx
+++ b/src/components/organisms/layout/comment.tsx
@@ -1,6 +1,7 @@
 import Button from '@components/atoms/buttons/button';
 import Link from '@components/atoms/links/link';
 import Meta from '@components/molecules/layout/meta';
+import { type Comment as CommentType } from '@ts/types/app';
 import useSettings from '@utils/hooks/use-settings';
 import Image from 'next/image';
 import Script from 'next/script';
@@ -25,32 +26,16 @@ export type CommentAuthor = {
   url?: string;
 };
 
-export type CommentProps = Pick<CommentFormProps, 'Notice' | 'saveComment'> & {
-  /**
-   * The comment author data.
-   */
-  author: CommentAuthor;
-  /**
-   * Enable or disable the reply button. Default: true.
-   */
-  canReply?: boolean;
-  /**
-   * The comment body.
-   */
-  content: string;
-  /**
-   * The comment id.
-   */
-  id: number | string;
-  /**
-   * The comment parent id.
-   */
-  parentId?: number | string;
-  /**
-   * The comment date and time separated with a space.
-   */
-  publication: string;
-};
+export type CommentProps = Pick<
+  CommentType,
+  'approved' | 'content' | 'id' | 'meta' | 'parentId'
+> &
+  Pick<CommentFormProps, 'Notice' | 'saveComment'> & {
+    /**
+     * Enable or disable the reply button. Default: true.
+     */
+    canReply?: boolean;
+  };
 
 /**
  * Comment component
@@ -58,19 +43,34 @@ export type CommentProps = Pick<CommentFormProps, 'Notice' | 'saveComment'> & {
  * Render a single comment.
  */
 const Comment: FC<CommentProps> = ({
-  author,
+  approved,
   canReply = true,
   content,
   id,
+  meta,
   Notice,
   parentId,
-  publication,
   saveComment,
   ...props
 }) => {
   const intl = useIntl();
+  const { website } = useSettings();
   const [isReplying, setIsReplying] = useState<boolean>(false);
-  const [publicationDate, publicationTime] = publication.split(' ');
+
+  if (!approved) {
+    return (
+      <div className={styles.wrapper}>
+        {intl.formatMessage({
+          defaultMessage: 'This comment is awaiting moderation...',
+          description: 'Comment: awaiting moderation',
+          id: '6a1Uo6',
+        })}
+      </div>
+    );
+  }
+
+  const { author, date } = meta;
+  const [publicationDate, publicationTime] = date.split(' ');
 
   const avatarAltText = intl.formatMessage(
     {
@@ -97,8 +97,6 @@ const Comment: FC<CommentProps> = ({
     id: '2fD5CI',
   });
 
-  const { website } = useSettings();
-
   const commentSchema: WithContext<CommentSchema> = {
     '@context': 'https://schema.org',
     '@id': `${website.url}/#comment-${id}`,
@@ -110,17 +108,17 @@ const Comment: FC<CommentProps> = ({
     author: {
       '@type': 'Person',
       name: author.name,
-      image: author.avatar,
-      url: author.url,
+      image: author.avatar?.src,
+      url: author.website,
     },
     creator: {
       '@type': 'Person',
       name: author.name,
-      image: author.avatar,
-      url: author.url,
+      image: author.avatar?.src,
+      url: author.website,
     },
-    dateCreated: publication,
-    datePublished: publication,
+    dateCreated: date,
+    datePublished: date,
     text: content,
   };
 
@@ -136,17 +134,19 @@ const Comment: FC<CommentProps> = ({
         className={`${styles.wrapper} ${styles['wrapper--comment']}`}
       >
         <header className={styles.header}>
-          <div className={styles.avatar}>
-            <Image
-              src={author.avatar}
-              alt={avatarAltText}
-              layout="fill"
-              objectFit="cover"
-              {...props}
-            />
-          </div>
-          {author.url ? (
-            <Link href={author.url} className={styles.author}>
+          {author.avatar && (
+            <div className={styles.avatar}>
+              <Image
+                src={author.avatar?.src}
+                alt={avatarAltText}
+                layout="fill"
+                objectFit="cover"
+                {...props}
+              />
+            </div>
+          )}
+          {author.website ? (
+            <Link href={author.website} className={styles.author}>
               {author.name}
             </Link>
           ) : (
@@ -181,7 +181,7 @@ const Comment: FC<CommentProps> = ({
       {isReplying && (
         <CommentForm
           Notice={Notice}
-          parentId={id as number}
+          parentId={id}
           saveComment={saveComment}
           title={formTitle}
           className={`${styles.wrapper} ${styles['wrapper--form']}`}
diff --git a/src/components/organisms/layout/comments-list.fixture.tsx b/src/components/organisms/layout/comments-list.fixture.tsx
new file mode 100644
index 0000000..2618f77
--- /dev/null
+++ b/src/components/organisms/layout/comments-list.fixture.tsx
@@ -0,0 +1,106 @@
+import { Comment } from '@ts/types/app';
+
+export const comments: Comment[] = [
+  {
+    approved: true,
+    content:
+      'Voluptas ducimus inventore. Libero ut et doloribus. Earum nostrum ab. Aliquam rem dolores omnis voluptate. Sunt aut ut et.',
+    id: 1,
+    meta: {
+      author: {
+        avatar: {
+          alt: 'Author 1 avatar',
+          height: 480,
+          src: 'http://placeimg.com/640/480',
+          width: 640,
+        },
+        name: 'Author 1',
+      },
+      date: '2021-04-03 18:04:11',
+    },
+    parentId: 0,
+    replies: [],
+  },
+  {
+    approved: true,
+    content:
+      'Sit sed error quasi voluptatem velit voluptas aut. Aut debitis eveniet. Praesentium dolores quia voluptate vero quis dicta quasi vel. Aut voluptas accusantium ut aut quidem consectetur itaque laboriosam occaecati.',
+    id: 2,
+    meta: {
+      author: {
+        avatar: {
+          alt: 'Author 2 avatar',
+          height: 480,
+          src: 'http://placeimg.com/640/480',
+          width: 640,
+        },
+        name: 'Author 2',
+        website: '#',
+      },
+      date: '2021-04-03 23:30:20',
+    },
+    parentId: 0,
+    replies: [
+      {
+        approved: true,
+        content:
+          'Vel ullam in porro tempore. Maiores quos quia magnam beatae nemo libero velit numquam. Sapiente aliquid cumque. Velit neque in adipisci aut assumenda voluptates earum. Autem esse autem provident in tempore. Aut distinctio dolor qui repellat et et adipisci velit aspernatur.',
+        id: 4,
+        meta: {
+          author: {
+            avatar: {
+              alt: 'Author 4 avatar',
+              height: 480,
+              src: 'http://placeimg.com/640/480',
+              width: 640,
+            },
+            name: 'Author 4',
+          },
+          date: '2021-04-03 23:04:24',
+        },
+        parentId: 2,
+        replies: [],
+      },
+      {
+        approved: true,
+        content:
+          'Sed non omnis. Quam porro est. Quae tempore quae. Exercitationem eos non velit voluptatem velit voluptas iusto. Sit debitis qui ipsam quo asperiores numquam veniam praesentium ut.',
+        id: 5,
+        meta: {
+          author: {
+            avatar: {
+              alt: 'Author 1 avatar',
+              height: 480,
+              src: 'http://placeimg.com/640/480',
+              width: 640,
+            },
+            name: 'Author 1',
+          },
+          date: '2021-04-04 08:05:14',
+        },
+        parentId: 2,
+        replies: [],
+      },
+    ],
+  },
+  {
+    approved: false,
+    content:
+      'Natus consequatur maiores aperiam dolore eius nesciunt ut qui et. Ab ea nobis est. Eaque dolor corrupti id aut. Impedit architecto autem qui neque rerum ab dicta dignissimos voluptates.',
+    id: 3,
+    meta: {
+      author: {
+        avatar: {
+          alt: 'Author 3',
+          height: 480,
+          src: 'http://placeimg.com/640/480',
+          width: 640,
+        },
+        name: 'Author 3',
+      },
+      date: '2021-09-13 13:24:54',
+    },
+    parentId: 0,
+    replies: [],
+  },
+];
diff --git a/src/components/organisms/layout/comments-list.stories.tsx b/src/components/organisms/layout/comments-list.stories.tsx
index 22cc9d9..5ed0f2a 100644
--- a/src/components/organisms/layout/comments-list.stories.tsx
+++ b/src/components/organisms/layout/comments-list.stories.tsx
@@ -1,5 +1,6 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import CommentsListComponent, { type Comment } from './comments-list';
+import CommentsListComponent from './comments-list';
+import { comments } from './comments-list.fixture';
 
 const saveComment = async () => {
   /** Do nothing. */
@@ -71,62 +72,6 @@ const Template: ComponentStory<typeof CommentsListComponent> = (args) => (
   <CommentsListComponent {...args} />
 );
 
-const comments: Comment[] = [
-  {
-    author: {
-      avatar: 'http://placeimg.com/640/480',
-      name: 'Author 1',
-    },
-    content:
-      'Voluptas ducimus inventore. Libero ut et doloribus. Earum nostrum ab. Aliquam rem dolores omnis voluptate. Sunt aut ut et.',
-    id: 1,
-    publication: '2021-04-03 18:04:11',
-  },
-  {
-    child: [
-      {
-        author: {
-          avatar: 'http://placeimg.com/640/480',
-          name: 'Author 4',
-        },
-        content:
-          'Vel ullam in porro tempore. Maiores quos quia magnam beatae nemo libero velit numquam. Sapiente aliquid cumque. Velit neque in adipisci aut assumenda voluptates earum. Autem esse autem provident in tempore. Aut distinctio dolor qui repellat et et adipisci velit aspernatur.',
-        id: 4,
-        publication: '2021-04-03 23:04:24',
-      },
-      {
-        author: {
-          avatar: 'http://placeimg.com/640/480',
-          name: 'Author 1',
-        },
-        content:
-          'Sed non omnis. Quam porro est. Quae tempore quae. Exercitationem eos non velit voluptatem velit voluptas iusto. Sit debitis qui ipsam quo asperiores numquam veniam praesentium ut.',
-        id: 5,
-        publication: '2021-04-04 08:05:14',
-      },
-    ],
-    author: {
-      avatar: 'http://placeimg.com/640/480',
-      name: 'Author 2',
-      url: '#',
-    },
-    content:
-      'Sit sed error quasi voluptatem velit voluptas aut. Aut debitis eveniet. Praesentium dolores quia voluptate vero quis dicta quasi vel. Aut voluptas accusantium ut aut quidem consectetur itaque laboriosam occaecati.',
-    id: 2,
-    publication: '2021-04-03 23:30:20',
-  },
-  {
-    author: {
-      avatar: 'http://placeimg.com/640/480',
-      name: 'Author 3',
-    },
-    content:
-      'Natus consequatur maiores aperiam dolore eius nesciunt ut qui et. Ab ea nobis est. Eaque dolor corrupti id aut. Impedit architecto autem qui neque rerum ab dicta dignissimos voluptates.',
-    id: 3,
-    publication: '2021-09-13 13:24:54',
-  },
-];
-
 /**
  * Layout Stories - Without child comments
  */
diff --git a/src/components/organisms/layout/comments-list.test.tsx b/src/components/organisms/layout/comments-list.test.tsx
index e135ec9..b0a2467 100644
--- a/src/components/organisms/layout/comments-list.test.tsx
+++ b/src/components/organisms/layout/comments-list.test.tsx
@@ -1,65 +1,7 @@
 import { render } from '@test-utils';
-import CommentsList, { type Comment } from './comments-list';
-
-const comments: Comment[] = [
-  {
-    author: {
-      avatar: 'http://placeimg.com/640/480',
-      name: 'Author 1',
-    },
-    content:
-      'Voluptas ducimus inventore. Libero ut et doloribus. Earum nostrum ab. Aliquam rem dolores omnis voluptate. Sunt aut ut et.',
-    id: 1,
-    publication: '2021-04-03 18:04:11',
-  },
-  {
-    child: [
-      {
-        author: {
-          avatar: 'http://placeimg.com/640/480',
-          name: 'Author 4',
-        },
-        content:
-          'Vel ullam in porro tempore. Maiores quos quia magnam beatae nemo libero velit numquam. Sapiente aliquid cumque. Velit neque in adipisci aut assumenda voluptates earum. Autem esse autem provident in tempore. Aut distinctio dolor qui repellat et et adipisci velit aspernatur.',
-        id: 4,
-        publication: '2021-04-03 23:04:24',
-      },
-      {
-        author: {
-          avatar: 'http://placeimg.com/640/480',
-          name: 'Author 1',
-        },
-        content:
-          'Sed non omnis. Quam porro est. Quae tempore quae. Exercitationem eos non velit voluptatem velit voluptas iusto. Sit debitis qui ipsam quo asperiores numquam veniam praesentium ut.',
-        id: 5,
-        publication: '2021-04-04 08:05:14',
-      },
-    ],
-    author: {
-      avatar: 'http://placeimg.com/640/480',
-      name: 'Author 2',
-      url: '#',
-    },
-    content:
-      'Sit sed error quasi voluptatem velit voluptas aut. Aut debitis eveniet. Praesentium dolores quia voluptate vero quis dicta quasi vel. Aut voluptas accusantium ut aut quidem consectetur itaque laboriosam occaecati.',
-    id: 2,
-    publication: '2021-04-03 23:30:20',
-  },
-  {
-    author: {
-      avatar: 'http://placeimg.com/640/480',
-      name: 'Author 3',
-    },
-    content:
-      'Natus consequatur maiores aperiam dolore eius nesciunt ut qui et. Ab ea nobis est. Eaque dolor corrupti id aut. Impedit architecto autem qui neque rerum ab dicta dignissimos voluptates.',
-    id: 3,
-    publication: '2021-05-13 13:24:54',
-  },
-];
-
-const saveComment = async () => {
-  /** Do nothing. */
-};
+import { saveComment } from './comment.fixture';
+import CommentsList from './comments-list';
+import { comments } from './comments-list.fixture';
 
 describe('CommentsList', () => {
   it('renders a comments list', () => {
diff --git a/src/components/organisms/layout/comments-list.tsx b/src/components/organisms/layout/comments-list.tsx
index f04354c..97eccb7 100644
--- a/src/components/organisms/layout/comments-list.tsx
+++ b/src/components/organisms/layout/comments-list.tsx
@@ -1,13 +1,10 @@
 import SingleComment, {
   type CommentProps,
 } from '@components/organisms/layout/comment';
+import { Comment } from '@ts/types/app';
 import { FC } from 'react';
 import styles from './comments-list.module.scss';
 
-export type Comment = Omit<CommentProps, 'canReply' | 'saveComment'> & {
-  child?: Comment[];
-};
-
 export type CommentsListProps = Pick<CommentProps, 'Notice' | 'saveComment'> & {
   /**
    * An array of comments.
@@ -42,7 +39,7 @@ const CommentsList: FC<CommentsListProps> = ({
   ): JSX.Element[] => {
     const isLastLevel = startLevel === depth;
 
-    return commentsList.map(({ child, ...comment }) => (
+    return commentsList.map(({ replies, ...comment }) => (
       <li key={comment.id} className={styles.item}>
         <SingleComment
           canReply={!isLastLevel}
@@ -50,8 +47,8 @@ const CommentsList: FC<CommentsListProps> = ({
           saveComment={saveComment}
           {...comment}
         />
-        {child && !isLastLevel && (
-          <ol className={styles.list}>{getItems(child, startLevel + 1)}</ol>
+        {replies && !isLastLevel && (
+          <ol className={styles.list}>{getItems(replies, startLevel + 1)}</ol>
         )}
       </li>
     ));
diff --git a/src/components/organisms/layout/posts-list.fixture.tsx b/src/components/organisms/layout/posts-list.fixture.tsx
new file mode 100644
index 0000000..b0afdf8
--- /dev/null
+++ b/src/components/organisms/layout/posts-list.fixture.tsx
@@ -0,0 +1,61 @@
+import { type Post } from './posts-list';
+
+export const introPost1 =
+  'Esse et voluptas sapiente modi impedit unde et. Ducimus nulla ea impedit sit placeat nihil assumenda. Rem est fugiat amet quo hic. Corrupti fuga quod animi autem dolorem ullam corrupti vel aut.';
+
+export const introPost2 =
+  'Illum quae asperiores quod aut necessitatibus itaque excepturi voluptas. Incidunt exercitationem ullam saepe alias consequatur sed. Quam veniam quaerat voluptatum earum quia quisquam fugiat sed perspiciatis. Et velit saepe est recusandae facilis eos eum ipsum.';
+
+export const introPost3 =
+  'Sunt aperiam ut rem impedit dolor id sit. Reprehenderit ipsum iusto fugiat. Quaerat laboriosam magnam facilis. Totam sint aliquam voluptatem in quis laborum sunt eum. Enim aut debitis officiis porro iure quia nihil voluptas ipsum. Praesentium quis necessitatibus cumque quia qui velit quos dolorem.';
+
+export const cover = {
+  alt: 'cover',
+  height: 480,
+  src: 'http://placeimg.com/640/480',
+  width: 640,
+};
+
+export const posts: Post[] = [
+  {
+    intro: introPost1,
+    id: 'post-1',
+    meta: {
+      cover,
+      dates: { publication: '2022-02-26' },
+      wordsCount: introPost1.split(' ').length,
+      thematics: [
+        { id: 1, name: 'Cat 1', slug: '#' },
+        { id: 2, name: 'Cat 2', slug: '#' },
+      ],
+      commentsCount: 1,
+    },
+    title: 'Ratione velit fuga',
+    url: '#',
+  },
+  {
+    intro: introPost2,
+    id: 'post-2',
+    meta: {
+      dates: { publication: '2022-02-20' },
+      wordsCount: introPost2.split(' ').length,
+      thematics: [{ id: 2, name: 'Cat 2', slug: '#' }],
+      commentsCount: 0,
+    },
+    title: 'Debitis laudantium laudantium',
+    url: '#',
+  },
+  {
+    intro: introPost3,
+    id: 'post-3',
+    meta: {
+      cover,
+      dates: { publication: '2021-12-20' },
+      wordsCount: introPost3.split(' ').length,
+      thematics: [{ id: 1, name: 'Cat 1', slug: '#' }],
+      commentsCount: 3,
+    },
+    title: 'Quaerat ut corporis',
+    url: '#',
+  },
+];
diff --git a/src/components/organisms/layout/posts-list.stories.tsx b/src/components/organisms/layout/posts-list.stories.tsx
index 360d441..68e3dc0 100644
--- a/src/components/organisms/layout/posts-list.stories.tsx
+++ b/src/components/organisms/layout/posts-list.stories.tsx
@@ -1,5 +1,6 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import PostsList, { type Post } from './posts-list';
+import PostsList from './posts-list';
+import { posts } from './posts-list.fixture';
 
 /**
  * PostsList - Storybook Meta
@@ -154,67 +155,6 @@ const Template: ComponentStory<typeof PostsList> = (args) => (
   <PostsList {...args} />
 );
 
-const excerpt1 =
-  'Esse et voluptas sapiente modi impedit unde et. Ducimus nulla ea impedit sit placeat nihil assumenda. Rem est fugiat amet quo hic. Corrupti fuga quod animi autem dolorem ullam corrupti vel aut.';
-const excerpt2 =
-  'Illum quae asperiores quod aut necessitatibus itaque excepturi voluptas. Incidunt exercitationem ullam saepe alias consequatur sed. Quam veniam quaerat voluptatum earum quia quisquam fugiat sed perspiciatis. Et velit saepe est recusandae facilis eos eum ipsum.';
-const excerpt3 =
-  'Sunt aperiam ut rem impedit dolor id sit. Reprehenderit ipsum iusto fugiat. Quaerat laboriosam magnam facilis. Totam sint aliquam voluptatem in quis laborum sunt eum. Enim aut debitis officiis porro iure quia nihil voluptas ipsum. Praesentium quis necessitatibus cumque quia qui velit quos dolorem.';
-
-const posts: Post[] = [
-  {
-    excerpt: excerpt1,
-    id: 'post-1',
-    meta: {
-      dates: { publication: '2022-02-26' },
-      readingTime: { wordsCount: excerpt1.split(' ').length },
-      thematics: [
-        { id: 'cat-1', name: 'Cat 1', url: '#' },
-        { id: 'cat-2', name: 'Cat 2', url: '#' },
-      ],
-      commentsCount: 1,
-    },
-    title: 'Ratione velit fuga',
-    url: '#',
-    cover: {
-      alt: 'cover',
-      height: 480,
-      src: 'http://placeimg.com/640/480',
-      width: 640,
-    },
-  },
-  {
-    excerpt: excerpt2,
-    id: 'post-2',
-    meta: {
-      dates: { publication: '2022-02-20' },
-      readingTime: { wordsCount: excerpt2.split(' ').length },
-      thematics: [{ id: 'cat-2', name: 'Cat 2', url: '#' }],
-      commentsCount: 0,
-    },
-    title: 'Debitis laudantium laudantium',
-    url: '#',
-  },
-  {
-    excerpt: excerpt3,
-    id: 'post-3',
-    meta: {
-      dates: { publication: '2021-12-20' },
-      readingTime: { wordsCount: excerpt3.split(' ').length },
-      thematics: [{ id: 'cat-1', name: 'Cat 1', url: '#' }],
-      commentsCount: 3,
-    },
-    title: 'Quaerat ut corporis',
-    url: '#',
-    cover: {
-      alt: 'cover',
-      height: 480,
-      src: 'http://placeimg.com/640/480',
-      width: 640,
-    },
-  },
-];
-
 /**
  * PostsList Stories - Default
  */
diff --git a/src/components/organisms/layout/posts-list.test.tsx b/src/components/organisms/layout/posts-list.test.tsx
index dfb9825..1bab466 100644
--- a/src/components/organisms/layout/posts-list.test.tsx
+++ b/src/components/organisms/layout/posts-list.test.tsx
@@ -1,66 +1,6 @@
 import { render, screen } from '@test-utils';
-import PostsList, { Post } from './posts-list';
-
-const excerpt1 =
-  'Esse et voluptas sapiente modi impedit unde et. Ducimus nulla ea impedit sit placeat nihil assumenda. Rem est fugiat amet quo hic. Corrupti fuga quod animi autem dolorem ullam corrupti vel aut.';
-const excerpt2 =
-  'Illum quae asperiores quod aut necessitatibus itaque excepturi voluptas. Incidunt exercitationem ullam saepe alias consequatur sed. Quam veniam quaerat voluptatum earum quia quisquam fugiat sed perspiciatis. Et velit saepe est recusandae facilis eos eum ipsum.';
-const excerpt3 =
-  'Sunt aperiam ut rem impedit dolor id sit. Reprehenderit ipsum iusto fugiat. Quaerat laboriosam magnam facilis. Totam sint aliquam voluptatem in quis laborum sunt eum. Enim aut debitis officiis porro iure quia nihil voluptas ipsum. Praesentium quis necessitatibus cumque quia qui velit quos dolorem.';
-
-const posts: Post[] = [
-  {
-    excerpt: excerpt1,
-    id: 'post-1',
-    meta: {
-      dates: { publication: '2022-02-26' },
-      readingTime: { wordsCount: excerpt1.split(' ').length },
-      thematics: [
-        { id: 'cat-1', name: 'Cat 1', url: '#' },
-        { id: 'cat-2', name: 'Cat 2', url: '#' },
-      ],
-      commentsCount: 1,
-    },
-    title: 'Ratione velit fuga',
-    url: '#',
-    cover: {
-      alt: 'cover',
-      height: 480,
-      src: 'http://placeimg.com/640/480',
-      width: 640,
-    },
-  },
-  {
-    excerpt: excerpt2,
-    id: 'post-2',
-    meta: {
-      dates: { publication: '2022-02-20' },
-      readingTime: { wordsCount: excerpt2.split(' ').length },
-      thematics: [{ id: 'cat-2', name: 'Cat 2', url: '#' }],
-      commentsCount: 0,
-    },
-    title: 'Debitis laudantium laudantium',
-    url: '#',
-  },
-  {
-    excerpt: excerpt3,
-    id: 'post-3',
-    meta: {
-      dates: { publication: '2021-12-20' },
-      readingTime: { wordsCount: excerpt3.split(' ').length },
-      thematics: [{ id: 'cat-1', name: 'Cat 1', url: '#' }],
-      commentsCount: 3,
-    },
-    title: 'Quaerat ut corporis',
-    url: '#',
-    cover: {
-      alt: 'cover',
-      height: 480,
-      src: 'http://placeimg.com/640/480',
-      width: 640,
-    },
-  },
-];
+import PostsList from './posts-list';
+import { posts } from './posts-list.fixture';
 
 describe('PostsList', () => {
   it('renders the correct number of posts', () => {
diff --git a/src/components/organisms/layout/posts-list.tsx b/src/components/organisms/layout/posts-list.tsx
index 608130e..91fc62d 100644
--- a/src/components/organisms/layout/posts-list.tsx
+++ b/src/components/organisms/layout/posts-list.tsx
@@ -3,7 +3,7 @@ import Heading, { type HeadingLevel } from '@components/atoms/headings/heading';
 import ProgressBar from '@components/atoms/loaders/progress-bar';
 import Spinner from '@components/atoms/loaders/spinner';
 import Pagination, {
-  PaginationProps,
+  type PaginationProps,
 } from '@components/molecules/nav/pagination';
 import useIsMounted from '@utils/hooks/use-is-mounted';
 import useSettings from '@utils/hooks/use-settings';
@@ -12,7 +12,7 @@ import { useIntl } from 'react-intl';
 import styles from './posts-list.module.scss';
 import Summary, { type SummaryProps } from './summary';
 
-export type Post = SummaryProps & {
+export type Post = Omit<SummaryProps, 'titleLevel'> & {
   /**
    * The post id.
    */
diff --git a/src/components/organisms/layout/summary.fixture.tsx b/src/components/organisms/layout/summary.fixture.tsx
new file mode 100644
index 0000000..fbc46a5
--- /dev/null
+++ b/src/components/organisms/layout/summary.fixture.tsx
@@ -0,0 +1,25 @@
+import { type SummaryMeta } from './summary';
+
+export const cover = {
+  alt: 'A cover',
+  height: 480,
+  src: 'http://placeimg.com/640/480',
+  width: 640,
+};
+
+export const intro =
+  'Perspiciatis quasi libero nemo non eligendi nam minima. Deleniti expedita tempore. Praesentium explicabo molestiae eaque consectetur vero. Quae nostrum quisquam similique. Ut hic est quas ut esse quisquam nobis.';
+
+export const meta: SummaryMeta = {
+  dates: { publication: '2022-04-11' },
+  wordsCount: intro.split(' ').length,
+  thematics: [
+    { id: 1, name: 'Cat 1', slug: '#' },
+    { id: 2, name: 'Cat 2', slug: '#' },
+  ],
+  commentsCount: 1,
+};
+
+export const title = 'Odio odit necessitatibus';
+
+export const url = '#';
diff --git a/src/components/organisms/layout/summary.stories.tsx b/src/components/organisms/layout/summary.stories.tsx
index 2512051..0b91e24 100644
--- a/src/components/organisms/layout/summary.stories.tsx
+++ b/src/components/organisms/layout/summary.stories.tsx
@@ -1,5 +1,6 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
 import Summary from './summary';
+import { cover, intro, meta } from './summary.fixture';
 
 /**
  * Summary - Storybook Meta
@@ -83,32 +84,12 @@ const Template: ComponentStory<typeof Summary> = (args) => (
   <Summary {...args} />
 );
 
-const cover = {
-  alt: 'A cover',
-  height: 480,
-  src: 'http://placeimg.com/640/480',
-  width: 640,
-};
-
-const excerpt =
-  'Perspiciatis quasi libero nemo non eligendi nam minima. Deleniti expedita tempore. Praesentium explicabo molestiae eaque consectetur vero. Quae nostrum quisquam similique. Ut hic est quas ut esse quisquam nobis.';
-
-const meta = {
-  dates: { publication: '2022-04-11' },
-  readingTime: { wordsCount: excerpt.split(' ').length },
-  thematics: [
-    { id: 'cat-1', name: 'Cat 1', url: '#' },
-    { id: 'cat-2', name: 'Cat 2', url: '#' },
-  ],
-  commentsCount: 1,
-};
-
 /**
  * Summary Stories - Default
  */
 export const Default = Template.bind({});
 Default.args = {
-  excerpt,
+  intro,
   meta,
   title: 'Odio odit necessitatibus',
   url: '#',
@@ -119,9 +100,8 @@ Default.args = {
  */
 export const WithCover = Template.bind({});
 WithCover.args = {
-  cover,
-  excerpt,
-  meta,
+  intro,
+  meta: { ...meta, cover },
   title: 'Odio odit necessitatibus',
   url: '#',
 };
diff --git a/src/components/organisms/layout/summary.test.tsx b/src/components/organisms/layout/summary.test.tsx
index 9e34254..7617c26 100644
--- a/src/components/organisms/layout/summary.test.tsx
+++ b/src/components/organisms/layout/summary.test.tsx
@@ -1,35 +1,12 @@
 import { render, screen } from '@test-utils';
 import Summary from './summary';
-
-const cover = {
-  alt: 'A cover',
-  height: 480,
-  src: 'http://placeimg.com/640/480',
-  width: 640,
-};
-
-const excerpt =
-  'Perspiciatis quasi libero nemo non eligendi nam minima. Deleniti expedita tempore. Praesentium explicabo molestiae eaque consectetur vero. Quae nostrum quisquam similique. Ut hic est quas ut esse quisquam nobis.';
-
-const meta = {
-  dates: { publication: '2022-04-11' },
-  readingTime: { wordsCount: excerpt.split(' ').length },
-  thematics: [
-    { id: 'cat-1', name: 'Cat 1', url: '#' },
-    { id: 'cat-2', name: 'Cat 2', url: '#' },
-  ],
-  commentsCount: 1,
-};
-
-const title = 'Odio odit necessitatibus';
-
-const url = '#';
+import { cover, intro, meta, title, url } from './summary.fixture';
 
 describe('Summary', () => {
   it('renders a title wrapped in a h2 element', () => {
     render(
       <Summary
-        excerpt={excerpt}
+        intro={intro}
         meta={meta}
         title={title}
         titleLevel={2}
@@ -42,16 +19,15 @@ describe('Summary', () => {
   });
 
   it('renders an excerpt', () => {
-    render(<Summary excerpt={excerpt} meta={meta} title={title} url={url} />);
-    expect(screen.getByText(excerpt)).toBeInTheDocument();
+    render(<Summary intro={intro} meta={meta} title={title} url={url} />);
+    expect(screen.getByText(intro)).toBeInTheDocument();
   });
 
   it('renders a cover', () => {
     render(
       <Summary
-        cover={cover}
-        excerpt={excerpt}
-        meta={meta}
+        intro={intro}
+        meta={{ ...meta, cover }}
         title={title}
         url={url}
       />
@@ -60,19 +36,19 @@ describe('Summary', () => {
   });
 
   it('renders a link to the full post', () => {
-    render(<Summary excerpt={excerpt} meta={meta} title={title} url={url} />);
+    render(<Summary intro={intro} meta={meta} title={title} url={url} />);
     expect(screen.getByRole('link', { name: title })).toBeInTheDocument();
   });
 
   it('renders a read more link', () => {
-    render(<Summary excerpt={excerpt} meta={meta} title={title} url={url} />);
+    render(<Summary intro={intro} meta={meta} title={title} url={url} />);
     expect(
       screen.getByRole('link', { name: `Read more about ${title}` })
     ).toBeInTheDocument();
   });
 
   it('renders some meta', () => {
-    render(<Summary excerpt={excerpt} meta={meta} title={title} url={url} />);
-    expect(screen.getByText(meta.thematics[0].name)).toBeInTheDocument();
+    render(<Summary intro={intro} meta={meta} title={title} url={url} />);
+    expect(screen.getByText(meta.thematics![0].name)).toBeInTheDocument();
   });
 });
diff --git a/src/components/organisms/layout/summary.tsx b/src/components/organisms/layout/summary.tsx
index 3831c0c..ca51cd6 100644
--- a/src/components/organisms/layout/summary.tsx
+++ b/src/components/organisms/layout/summary.tsx
@@ -6,7 +6,7 @@ import ResponsiveImage, {
   type ResponsiveImageProps,
 } from '@components/molecules/images/responsive-image';
 import Meta, { type MetaData } from '@components/molecules/layout/meta';
-import { type Dates } from '@ts/types/app';
+import { type Article, type Meta as MetaType } from '@ts/types/app';
 import useReadingTime from '@utils/hooks/use-reading-time';
 import { FC, ReactNode } from 'react';
 import { useIntl } from 'react-intl';
@@ -17,43 +17,22 @@ export type Cover = Pick<
   'alt' | 'src' | 'width' | 'height'
 >;
 
-export type SummaryMetaLink = {
-  id: number | string;
-  name: string;
-  url: string;
-};
-
-export type SummaryMetaReadingTime = {
-  wordsCount: number;
-  onlyMinutes?: boolean;
-};
-
-export type SummaryMeta = {
-  author?: string;
-  commentsCount?: number;
-  dates: Dates;
-  readingTime: SummaryMetaReadingTime;
-  thematics?: SummaryMetaLink[];
-  topics?: SummaryMetaLink[];
-};
+export type SummaryMeta = Pick<
+  MetaType<'article'>,
+  | 'author'
+  | 'commentsCount'
+  | 'cover'
+  | 'dates'
+  | 'thematics'
+  | 'topics'
+  | 'wordsCount'
+>;
 
-export type SummaryProps = {
+export type SummaryProps = Pick<Article, 'intro' | 'title'> & {
   /**
-   * The post cover.
-   */
-  cover?: Cover;
-  /**
-   * The post excerpt.
-   */
-  excerpt: string;
-  /**
-   * The post meta.
+   * The post metadata.
    */
   meta: SummaryMeta;
-  /**
-   * The post title.
-   */
-  title: string;
   /**
    * The heading level (hn).
    */
@@ -70,8 +49,7 @@ export type SummaryProps = {
  * Render a page summary.
  */
 const Summary: FC<SummaryProps> = ({
-  cover,
-  excerpt,
+  intro,
   meta,
   title,
   titleLevel = 2,
@@ -91,14 +69,13 @@ const Summary: FC<SummaryProps> = ({
       ),
     }
   );
-  const { wordsCount, onlyMinutes } = meta.readingTime;
-  const readingTime = useReadingTime(wordsCount, onlyMinutes);
-
-  const getMeta = (data: SummaryMeta): MetaData => {
-    const { author, commentsCount, dates, thematics, topics } = data;
+  const { author, commentsCount, cover, dates, thematics, topics, wordsCount } =
+    meta;
+  const readingTime = useReadingTime(wordsCount, true);
 
+  const getMeta = (): MetaData => {
     return {
-      author,
+      author: author?.name,
       publication: { date: dates.publication },
       update:
         dates.update && dates.publication !== dates.update
@@ -106,12 +83,12 @@ const Summary: FC<SummaryProps> = ({
           : undefined,
       readingTime,
       thematics: thematics?.map((thematic) => (
-        <Link key={thematic.id} href={thematic.url}>
+        <Link key={thematic.id} href={thematic.slug}>
           {thematic.name}
         </Link>
       )),
       topics: topics?.map((topic) => (
-        <Link key={topic.id} href={topic.url}>
+        <Link key={topic.id} href={topic.slug}>
           {topic.name}
         </Link>
       )),
@@ -134,11 +111,7 @@ const Summary: FC<SummaryProps> = ({
         </Link>
       </header>
       <div className={styles.body}>
-        {typeof excerpt === 'string' ? (
-          <div dangerouslySetInnerHTML={{ __html: excerpt }} />
-        ) : (
-          excerpt
-        )}
+        <div dangerouslySetInnerHTML={{ __html: intro }} />
         <ButtonLink target={url} className={styles['read-more']}>
           <>
             {readMore}
@@ -148,7 +121,7 @@ const Summary: FC<SummaryProps> = ({
       </div>
       <footer className={styles.footer}>
         <Meta
-          data={getMeta(meta)}
+          data={getMeta()}
           layout="column"
           itemsLayout="stacked"
           withSeparator={false}
diff --git a/src/components/templates/page/page-layout.stories.tsx b/src/components/templates/page/page-layout.stories.tsx
index bc1d7b6..88e3785 100644
--- a/src/components/templates/page/page-layout.stories.tsx
+++ b/src/components/templates/page/page-layout.stories.tsx
@@ -1,7 +1,9 @@
 import ButtonLink from '@components/atoms/buttons/button-link';
 import Heading from '@components/atoms/headings/heading';
 import Link from '@components/atoms/links/link';
+import { comments } from '@components/organisms/layout/comments-list.fixture';
 import PostsList from '@components/organisms/layout/posts-list';
+import { posts } from '@components/organisms/layout/posts-list.fixture';
 import LinksListWidget from '@components/organisms/widgets/links-list-widget';
 import Sharing from '@components/organisms/widgets/sharing';
 import { ComponentMeta, ComponentStory } from '@storybook/react';
@@ -267,62 +269,6 @@ const postBreadcrumb = [
   { id: 'post', url: '#', name: pageTitle },
 ];
 
-const comments = [
-  {
-    author: {
-      avatar: 'http://placeimg.com/640/480',
-      name: 'Author 1',
-    },
-    content:
-      'Voluptas ducimus inventore. Libero ut et doloribus. Earum nostrum ab. Aliquam rem dolores omnis voluptate. Sunt aut ut et.',
-    id: 1,
-    publication: '2021-04-03 18:04:11',
-  },
-  {
-    child: [
-      {
-        author: {
-          avatar: 'http://placeimg.com/640/480',
-          name: 'Author 4',
-        },
-        content:
-          'Vel ullam in porro tempore. Maiores quos quia magnam beatae nemo libero velit numquam. Sapiente aliquid cumque. Velit neque in adipisci aut assumenda voluptates earum. Autem esse autem provident in tempore. Aut distinctio dolor qui repellat et et adipisci velit aspernatur.',
-        id: 4,
-        publication: '2021-04-03 23:04:24',
-      },
-      {
-        author: {
-          avatar: 'http://placeimg.com/640/480',
-          name: 'Author 1',
-        },
-        content:
-          'Sed non omnis. Quam porro est. Quae tempore quae. Exercitationem eos non velit voluptatem velit voluptas iusto. Sit debitis qui ipsam quo asperiores numquam veniam praesentium ut.',
-        id: 5,
-        publication: '2021-04-04 08:05:14',
-      },
-    ],
-    author: {
-      avatar: 'http://placeimg.com/640/480',
-      name: 'Author 2',
-      url: '#',
-    },
-    content:
-      'Sit sed error quasi voluptatem velit voluptas aut. Aut debitis eveniet. Praesentium dolores quia voluptate vero quis dicta quasi vel. Aut voluptas accusantium ut aut quidem consectetur itaque laboriosam occaecati.',
-    id: 2,
-    publication: '2021-04-03 23:30:20',
-  },
-  {
-    author: {
-      avatar: 'http://placeimg.com/640/480',
-      name: 'Author 3',
-    },
-    content:
-      'Natus consequatur maiores aperiam dolore eius nesciunt ut qui et. Ab ea nobis est. Eaque dolor corrupti id aut. Impedit architecto autem qui neque rerum ab dicta dignissimos voluptates.',
-    id: 3,
-    publication: '2021-09-13 13:24:54',
-  },
-];
-
 /**
  * Page Layout Stories - Post
  */
@@ -393,7 +339,7 @@ Post.args = {
     />,
   ],
   withToC: true,
-  comments,
+  comments: comments,
   allowComments: true,
 };
 
@@ -402,67 +348,6 @@ const postsListBreadcrumb = [
   { id: 'blog', url: '#', name: 'Blog' },
 ];
 
-const excerpt1 =
-  'Esse et voluptas sapiente modi impedit unde et. Ducimus nulla ea impedit sit placeat nihil assumenda. Rem est fugiat amet quo hic. Corrupti fuga quod animi autem dolorem ullam corrupti vel aut.';
-const excerpt2 =
-  'Illum quae asperiores quod aut necessitatibus itaque excepturi voluptas. Incidunt exercitationem ullam saepe alias consequatur sed. Quam veniam quaerat voluptatum earum quia quisquam fugiat sed perspiciatis. Et velit saepe est recusandae facilis eos eum ipsum.';
-const excerpt3 =
-  'Sunt aperiam ut rem impedit dolor id sit. Reprehenderit ipsum iusto fugiat. Quaerat laboriosam magnam facilis. Totam sint aliquam voluptatem in quis laborum sunt eum. Enim aut debitis officiis porro iure quia nihil voluptas ipsum. Praesentium quis necessitatibus cumque quia qui velit quos dolorem.';
-
-const posts = [
-  {
-    excerpt: excerpt1,
-    id: 'post-1',
-    meta: {
-      dates: { publication: '2022-02-26' },
-      readingTime: { wordsCount: excerpt1.split(' ').length },
-      thematics: [
-        { id: 'cat-1', name: 'Cat 1', url: '#' },
-        { id: 'cat-2', name: 'Cat 2', url: '#' },
-      ],
-      commentsCount: 1,
-    },
-    title: 'Ratione velit fuga',
-    url: '#',
-    cover: {
-      alt: 'cover',
-      height: 480,
-      src: 'http://placeimg.com/640/480',
-      width: 640,
-    },
-  },
-  {
-    excerpt: excerpt2,
-    id: 'post-2',
-    meta: {
-      dates: { publication: '2022-02-20' },
-      readingTime: { wordsCount: excerpt2.split(' ').length },
-      thematics: [{ id: 'cat-2', name: 'Cat 2', url: '#' }],
-      commentsCount: 0,
-    },
-    title: 'Debitis laudantium laudantium',
-    url: '#',
-  },
-  {
-    excerpt: excerpt3,
-    id: 'post-3',
-    meta: {
-      dates: { publication: '2021-12-20' },
-      readingTime: { wordsCount: excerpt3.split(' ').length },
-      thematics: [{ id: 'cat-1', name: 'Cat 1', url: '#' }],
-      commentsCount: 3,
-    },
-    title: 'Quaerat ut corporis',
-    url: '#',
-    cover: {
-      alt: 'cover',
-      height: 480,
-      src: 'http://placeimg.com/640/480',
-      width: 640,
-    },
-  },
-];
-
 const blogCategories = [
   { name: 'Cat 1', url: '#' },
   {
diff --git a/src/components/templates/page/page-layout.test.tsx b/src/components/templates/page/page-layout.test.tsx
index a0c8923..f2d07d7 100644
--- a/src/components/templates/page/page-layout.test.tsx
+++ b/src/components/templates/page/page-layout.test.tsx
@@ -1,3 +1,4 @@
+import { comments } from '@components/organisms/layout/comments-list.fixture';
 import { render, screen } from '@test-utils';
 import { BreadcrumbList } from 'schema-dts';
 import PageLayout from './page-layout';
@@ -88,18 +89,6 @@ describe('PageLayout', () => {
   });
 
   it('renders the comments list', () => {
-    const comments = [
-      {
-        author: {
-          avatar: 'http://placeimg.com/640/480',
-          name: 'Author 1',
-        },
-        content:
-          'Voluptas ducimus inventore. Libero ut et doloribus. Earum nostrum ab. Aliquam rem dolores omnis voluptate. Sunt aut ut et.',
-        id: 1,
-        publication: '2021-04-03 18:04:11',
-      },
-    ];
     render(
       <PageLayout
         breadcrumb={breadcrumb}
diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx
index 812fffe..2878538 100644
--- a/src/pages/article/[slug].tsx
+++ b/src/pages/article/[slug].tsx
@@ -18,11 +18,8 @@ import {
   type NextPageWithLayout,
 } from '@ts/types/app';
 import { loadTranslation, type Messages } from '@utils/helpers/i18n';
-import useAddClassName from '@utils/hooks/use-add-classname';
-import useAttributes from '@utils/hooks/use-attributes';
 import useBreadcrumb from '@utils/hooks/use-breadcrumb';
 import usePrism, { type OptionalPrismPlugin } from '@utils/hooks/use-prism';
-import useQuerySelectorAll from '@utils/hooks/use-query-selector-all';
 import useReadingTime from '@utils/hooks/use-reading-time';
 import useSettings from '@utils/hooks/use-settings';
 import { GetStaticPaths, GetStaticProps } from 'next';
@@ -66,12 +63,15 @@ const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({
     title,
     url: `/article/${slug}`,
   });
-  const readingTime = useReadingTime(wordsCount || 0, true);
+  const readingTime = useReadingTime(wordsCount, true);
 
   const headerMeta: PageLayoutProps['headerMeta'] = {
     author: author?.name,
     publication: { date: dates.publication },
-    update: dates.update ? { date: dates.update } : undefined,
+    update:
+      dates.update && dates.publication !== dates.update
+        ? { date: dates.update }
+        : undefined,
     readingTime,
     thematics:
       thematics &&
@@ -167,35 +167,6 @@ const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({
     '@graph': [webpageSchema, blogSchema, blogPostSchema],
   };
 
-  /**
-   * Convert the comments list to the right format.
-   *
-   * @param {Comment[]} list - The comments list.
-   * @returns {PageLayoutProps['comments']} - The formatted comments list.
-   */
-  const getCommentsList = (list: Comment[]): PageLayoutProps['comments'] => {
-    return list.map((comment) => {
-      const {
-        content: commentBody,
-        id: commentId,
-        meta: commentMeta,
-        parentId,
-        replies,
-      } = comment;
-      const { author: commentAuthor, date } = commentMeta;
-      const { name, avatar, website: authorUrl } = commentAuthor;
-
-      return {
-        author: { name, avatar: avatar!.src, url: authorUrl },
-        content: commentBody,
-        id: commentId,
-        publication: date,
-        child: getCommentsList(replies),
-        parentId,
-      };
-    });
-  };
-
   const prismPlugins: OptionalPrismPlugin[] = ['command-line', 'line-numbers'];
   const { attributes, className } = usePrism({ plugins: prismPlugins });
   const lineNumbersClassName = className
@@ -254,7 +225,7 @@ const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({
         bodyClassName={styles.body}
         breadcrumb={breadcrumbItems}
         breadcrumbSchema={breadcrumbSchema}
-        comments={data && getCommentsList(data)}
+        comments={data}
         footerMeta={footerMeta}
         headerMeta={headerMeta}
         id={id as number}
@@ -314,7 +285,7 @@ export const getStaticPaths: GetStaticPaths = async () => {
 
   return {
     paths,
-    fallback: true,
+    fallback: false,
   };
 };
 
diff --git a/src/pages/blog/index.tsx b/src/pages/blog/index.tsx
index 90f56be..dd04fad 100644
--- a/src/pages/blog/index.tsx
+++ b/src/pages/blog/index.tsx
@@ -1,34 +1,27 @@
 import Notice from '@components/atoms/layout/notice';
-import PostsList, { type Post } from '@components/organisms/layout/posts-list';
+import PostsList from '@components/organisms/layout/posts-list';
 import LinksListWidget from '@components/organisms/widgets/links-list-widget';
 import { getLayout } from '@components/templates/layout/layout';
 import PageLayout from '@components/templates/page/page-layout';
 import { type EdgesResponse } from '@services/graphql/api';
-import {
-  getArticleFromRawData,
-  getArticles,
-  getTotalArticles,
-} from '@services/graphql/articles';
+import { getArticles, getTotalArticles } from '@services/graphql/articles';
 import {
   getThematicsPreview,
   getTotalThematics,
 } from '@services/graphql/thematics';
 import { getTopicsPreview, getTotalTopics } from '@services/graphql/topics';
+import { type NextPageWithLayout } from '@ts/types/app';
 import {
-  type Article,
-  type Meta,
-  type NextPageWithLayout,
-} from '@ts/types/app';
-import {
-  RawThematicPreview,
-  RawTopicPreview,
   type RawArticle,
+  type RawThematicPreview,
+  type RawTopicPreview,
 } from '@ts/types/raw-data';
 import { settings } from '@utils/config';
 import { loadTranslation, type Messages } from '@utils/helpers/i18n';
 import {
   getLinksListItems,
   getPageLinkFromRawData,
+  getPostsList,
 } from '@utils/helpers/pages';
 import useBreadcrumb from '@utils/hooks/use-breadcrumb';
 import usePagination from '@utils/hooks/use-pagination';
@@ -119,62 +112,6 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({
     '@graph': [webpageSchema, blogSchema],
   };
 
-  /**
-   * Retrieve the formatted meta.
-   *
-   * @param {Meta<'article'>} meta - The article meta.
-   * @returns {Post['meta']} The formatted meta.
-   */
-  const getPostMeta = (meta: Meta<'article'>): Post['meta'] => {
-    const { commentsCount, dates, thematics, wordsCount } = meta;
-
-    return {
-      commentsCount,
-      dates,
-      readingTime: { wordsCount: wordsCount || 0, onlyMinutes: true },
-      thematics: thematics?.map((thematic) => {
-        return { ...thematic, url: `/thematique/${thematic.slug}` };
-      }),
-    };
-  };
-
-  /**
-   * Retrieve the formatted posts.
-   *
-   * @param {Article[]} posts - An array of articles.
-   * @returns {Post[]} An array of formatted posts.
-   */
-  const getPosts = (posts: Article[]): Post[] => {
-    return posts.map((post) => {
-      return {
-        ...post,
-        cover: post.meta.cover,
-        excerpt: post.intro,
-        meta: getPostMeta(post.meta),
-        url: `/article/${post.slug}`,
-      };
-    });
-  };
-
-  /**
-   * Retrieve the posts list from raw data.
-   *
-   * @param {EdgesResponse<RawArticle>[]} rawData - The raw data.
-   * @returns {Post[]} An array of posts.
-   */
-  const getPostsList = (rawData: EdgesResponse<RawArticle>[]): Post[] => {
-    const articlesList: RawArticle[] = [];
-    rawData.forEach((articleData) =>
-      articleData.edges.forEach((edge) => {
-        articlesList.push(edge.node);
-      })
-    );
-
-    return getPosts(
-      articlesList.map((article) => getArticleFromRawData(article))
-    );
-  };
-
   const {
     data,
     error,
diff --git a/src/pages/blog/page/[number].tsx b/src/pages/blog/page/[number].tsx
index e8c93f7..8f50ddd 100644
--- a/src/pages/blog/page/[number].tsx
+++ b/src/pages/blog/page/[number].tsx
@@ -1,10 +1,9 @@
-import PostsList, { type Post } from '@components/organisms/layout/posts-list';
+import PostsList from '@components/organisms/layout/posts-list';
 import LinksListWidget from '@components/organisms/widgets/links-list-widget';
 import { getLayout } from '@components/templates/layout/layout';
 import PageLayout from '@components/templates/page/page-layout';
 import { type EdgesResponse } from '@services/graphql/api';
 import {
-  getArticleFromRawData,
   getArticles,
   getArticlesEndCursor,
   getTotalArticles,
@@ -14,11 +13,7 @@ import {
   getTotalThematics,
 } from '@services/graphql/thematics';
 import { getTopicsPreview, getTotalTopics } from '@services/graphql/topics';
-import {
-  type Article,
-  type Meta,
-  type NextPageWithLayout,
-} from '@ts/types/app';
+import { type NextPageWithLayout } from '@ts/types/app';
 import {
   type RawArticle,
   type RawThematicPreview,
@@ -29,6 +24,7 @@ import { loadTranslation, type Messages } from '@utils/helpers/i18n';
 import {
   getLinksListItems,
   getPageLinkFromRawData,
+  getPostsList,
 } from '@utils/helpers/pages';
 import useBreadcrumb from '@utils/hooks/use-breadcrumb';
 import useRedirection from '@utils/hooks/use-redirection';
@@ -131,62 +127,6 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({
     '@graph': [webpageSchema, blogSchema],
   };
 
-  /**
-   * Retrieve the formatted meta.
-   *
-   * @param {Meta<'article'>} meta - The article meta.
-   * @returns {Post['meta']} The formatted meta.
-   */
-  const getPostMeta = (meta: Meta<'article'>): Post['meta'] => {
-    const { commentsCount, dates, thematics, wordsCount } = meta;
-
-    return {
-      commentsCount,
-      dates,
-      readingTime: { wordsCount: wordsCount || 0, onlyMinutes: true },
-      thematics: thematics?.map((thematic) => {
-        return { ...thematic, url: `/thematique/${thematic.slug}` };
-      }),
-    };
-  };
-
-  /**
-   * Retrieve the formatted posts.
-   *
-   * @param {Article[]} posts - An array of articles.
-   * @returns {Post[]} An array of formatted posts.
-   */
-  const getPosts = (posts: Article[]): Post[] => {
-    return posts.map((post) => {
-      return {
-        ...post,
-        cover: post.meta.cover,
-        excerpt: post.intro,
-        meta: getPostMeta(post.meta),
-        url: `/article/${post.slug}`,
-      };
-    });
-  };
-
-  /**
-   * Retrieve the posts list from raw data.
-   *
-   * @param {EdgesResponse<RawArticle>[]} rawData - The raw data.
-   * @returns {Post[]} An array of posts.
-   */
-  const getPostsList = (rawData: EdgesResponse<RawArticle>[]): Post[] => {
-    const articlesList: RawArticle[] = [];
-    rawData.forEach((articleData) =>
-      articleData.edges.forEach((edge) => {
-        articlesList.push(edge.node);
-      })
-    );
-
-    return getPosts(
-      articlesList.map((article) => getArticleFromRawData(article))
-    );
-  };
-
   const thematicsListTitle = intl.formatMessage({
     defaultMessage: 'Thematics',
     description: 'BlogPage: thematics list widget title',
diff --git a/src/pages/projets/[slug].tsx b/src/pages/projets/[slug].tsx
index e9b36fa..27c715d 100644
--- a/src/pages/projets/[slug].tsx
+++ b/src/pages/projets/[slug].tsx
@@ -1,14 +1,18 @@
 import Link from '@components/atoms/links/link';
-import SocialLink, { SocialWebsite } from '@components/atoms/links/social-link';
+import SocialLink, {
+  type 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 Gallery from '@components/organisms/images/gallery';
-import Overview, { OverviewMeta } from '@components/organisms/layout/overview';
+import Overview, {
+  type OverviewMeta,
+} from '@components/organisms/layout/overview';
 import Sharing from '@components/organisms/widgets/sharing';
 import { getLayout } from '@components/templates/layout/layout';
 import PageLayout, {
-  PageLayoutProps,
+  type PageLayoutProps,
 } from '@components/templates/page/page-layout';
 import styles from '@styles/pages/project.module.scss';
 import {
@@ -16,11 +20,11 @@ import {
   type ProjectPreview,
   type Repos,
 } from '@ts/types/app';
-import { loadTranslation, Messages } from '@utils/helpers/i18n';
+import { loadTranslation, type 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 useGithubApi, { type RepoData } from '@utils/hooks/use-github-api';
 import useSettings from '@utils/hooks/use-settings';
 import { MDXComponents, NestedMDXComponents } from 'mdx/types';
 import { GetStaticPaths, GetStaticProps } from 'next';
diff --git a/src/pages/projets/index.tsx b/src/pages/projets/index.tsx
index d500b6b..9ca289a 100644
--- a/src/pages/projets/index.tsx
+++ b/src/pages/projets/index.tsx
@@ -1,13 +1,13 @@
 import Link from '@components/atoms/links/link';
 import CardsList, {
-  CardsListItem,
+  type CardsListItem,
 } from '@components/organisms/layout/cards-list';
 import { getLayout } from '@components/templates/layout/layout';
 import PageLayout from '@components/templates/page/page-layout';
 import PageContent, { meta } from '@content/pages/projects.mdx';
 import styles from '@styles/pages/projects.module.scss';
 import { type NextPageWithLayout, type ProjectCard } from '@ts/types/app';
-import { loadTranslation, Messages } from '@utils/helpers/i18n';
+import { loadTranslation, type 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';
diff --git a/src/pages/recherche/index.tsx b/src/pages/recherche/index.tsx
index 8895015..c69d931 100644
--- a/src/pages/recherche/index.tsx
+++ b/src/pages/recherche/index.tsx
@@ -1,34 +1,26 @@
 import Notice from '@components/atoms/layout/notice';
 import Spinner from '@components/atoms/loaders/spinner';
-import PostsList, { type Post } from '@components/organisms/layout/posts-list';
+import PostsList from '@components/organisms/layout/posts-list';
 import LinksListWidget from '@components/organisms/widgets/links-list-widget';
 import { getLayout } from '@components/templates/layout/layout';
 import PageLayout from '@components/templates/page/page-layout';
-import { type EdgesResponse } from '@services/graphql/api';
-import {
-  getArticleFromRawData,
-  getArticles,
-  getTotalArticles,
-} from '@services/graphql/articles';
+import { getArticles, getTotalArticles } from '@services/graphql/articles';
 import {
   getThematicsPreview,
   getTotalThematics,
 } from '@services/graphql/thematics';
 import { getTopicsPreview, getTotalTopics } from '@services/graphql/topics';
+import { type NextPageWithLayout } from '@ts/types/app';
 import {
-  type Article,
-  type Meta,
-  type NextPageWithLayout,
-} from '@ts/types/app';
-import {
-  RawThematicPreview,
-  RawTopicPreview,
   type RawArticle,
+  type RawThematicPreview,
+  type RawTopicPreview,
 } from '@ts/types/raw-data';
 import { loadTranslation, type Messages } from '@utils/helpers/i18n';
 import {
   getLinksListItems,
   getPageLinkFromRawData,
+  getPostsList,
 } from '@utils/helpers/pages';
 import useBreadcrumb from '@utils/hooks/use-breadcrumb';
 import useDataFromAPI from '@utils/hooks/use-data-from-api';
@@ -145,62 +137,6 @@ const SearchPage: NextPageWithLayout<SearchPageProps> = ({
     getTotalArticles(query.s as string)
   );
 
-  /**
-   * Retrieve the formatted meta.
-   *
-   * @param {Meta<'article'>} meta - The article meta.
-   * @returns {Post['meta']} The formatted meta.
-   */
-  const getPostMeta = (meta: Meta<'article'>): Post['meta'] => {
-    const { commentsCount, dates, thematics, wordsCount } = meta;
-
-    return {
-      commentsCount,
-      dates,
-      readingTime: { wordsCount: wordsCount || 0, onlyMinutes: true },
-      thematics: thematics?.map((thematic) => {
-        return { ...thematic, url: `/thematique/${thematic.slug}` };
-      }),
-    };
-  };
-
-  /**
-   * Retrieve the formatted posts.
-   *
-   * @param {Article[]} posts - An array of articles.
-   * @returns {Post[]} An array of formatted posts.
-   */
-  const getPosts = (posts: Article[]): Post[] => {
-    return posts.map((post) => {
-      return {
-        ...post,
-        cover: post.meta.cover,
-        excerpt: post.intro,
-        meta: getPostMeta(post.meta),
-        url: `/article/${post.slug}`,
-      };
-    });
-  };
-
-  /**
-   * Retrieve the posts list from raw data.
-   *
-   * @param {EdgesResponse<RawArticle>[]} rawData - The raw data.
-   * @returns {Post[]} An array of posts.
-   */
-  const getPostsList = (rawData: EdgesResponse<RawArticle>[]): Post[] => {
-    const articlesList: RawArticle[] = [];
-    rawData.forEach((articleData) =>
-      articleData.edges.forEach((edge) => {
-        articlesList.push(edge.node);
-      })
-    );
-
-    return getPosts(
-      articlesList.map((article) => getArticleFromRawData(article))
-    );
-  };
-
   /**
    * Load more posts handler.
    */
diff --git a/src/pages/sujet/[slug].tsx b/src/pages/sujet/[slug].tsx
index b4845eb..95843cd 100644
--- a/src/pages/sujet/[slug].tsx
+++ b/src/pages/sujet/[slug].tsx
@@ -1,6 +1,6 @@
 import Heading from '@components/atoms/headings/heading';
 import ResponsiveImage from '@components/molecules/images/responsive-image';
-import PostsList, { type Post } from '@components/organisms/layout/posts-list';
+import PostsList from '@components/organisms/layout/posts-list';
 import LinksListWidget from '@components/organisms/widgets/links-list-widget';
 import { getLayout } from '@components/templates/layout/layout';
 import PageLayout, {
@@ -14,7 +14,6 @@ import {
 } from '@services/graphql/topics';
 import styles from '@styles/pages/topic.module.scss';
 import {
-  type Article,
   type NextPageWithLayout,
   type PageLink,
   type Topic,
@@ -23,7 +22,7 @@ import { loadTranslation, type Messages } from '@utils/helpers/i18n';
 import {
   getLinksListItems,
   getPageLinkFromRawData,
-  getPostMeta,
+  getPostsWithUrl,
 } from '@utils/helpers/pages';
 import useBreadcrumb from '@utils/hooks/use-breadcrumb';
 import useSettings from '@utils/hooks/use-settings';
@@ -109,27 +108,6 @@ const TopicPage: NextPageWithLayout<TopicPageProps> = ({
     '@graph': [webpageSchema, articleSchema],
   };
 
-  const getPosts = (array: Article[]): Post[] => {
-    return array.map((article) => {
-      const {
-        intro: articleIntro,
-        meta: articleMeta,
-        slug: articleSlug,
-        ...remainingData
-      } = article;
-
-      const { cover: articleCover, ...remainingMeta } = articleMeta;
-
-      return {
-        cover: articleCover,
-        excerpt: articleIntro,
-        meta: getPostMeta(remainingMeta),
-        url: `/article/${articleSlug}`,
-        ...remainingData,
-      };
-    });
-  };
-
   const topicsListTitle = intl.formatMessage({
     defaultMessage: 'Other topics',
     description: 'TopicPage: other topics list widget title',
@@ -206,7 +184,7 @@ const TopicPage: NextPageWithLayout<TopicPageProps> = ({
             </Heading>
             <PostsList
               baseUrl="/sujet/page/"
-              posts={getPosts(articles)}
+              posts={getPostsWithUrl(articles)}
               total={articles.length}
               titleLevel={3}
               byYear={true}
diff --git a/src/pages/thematique/[slug].tsx b/src/pages/thematique/[slug].tsx
index ce4eccf..a91483e 100644
--- a/src/pages/thematique/[slug].tsx
+++ b/src/pages/thematique/[slug].tsx
@@ -1,5 +1,5 @@
 import Heading from '@components/atoms/headings/heading';
-import PostsList, { type Post } from '@components/organisms/layout/posts-list';
+import PostsList from '@components/organisms/layout/posts-list';
 import LinksListWidget from '@components/organisms/widgets/links-list-widget';
 import { getLayout } from '@components/templates/layout/layout';
 import PageLayout, {
@@ -12,7 +12,6 @@ import {
   getTotalThematics,
 } from '@services/graphql/thematics';
 import {
-  type Article,
   type NextPageWithLayout,
   type PageLink,
   type Thematic,
@@ -21,7 +20,7 @@ import { loadTranslation, type Messages } from '@utils/helpers/i18n';
 import {
   getLinksListItems,
   getPageLinkFromRawData,
-  getPostMeta,
+  getPostsWithUrl,
 } from '@utils/helpers/pages';
 import useBreadcrumb from '@utils/hooks/use-breadcrumb';
 import useSettings from '@utils/hooks/use-settings';
@@ -99,27 +98,6 @@ const ThematicPage: NextPageWithLayout<ThematicPageProps> = ({
     '@graph': [webpageSchema, articleSchema],
   };
 
-  const getPosts = (array: Article[]): Post[] => {
-    return array.map((article) => {
-      const {
-        intro: articleIntro,
-        meta: articleMeta,
-        slug: articleSlug,
-        ...remainingData
-      } = article;
-
-      const { cover, ...remainingMeta } = articleMeta;
-
-      return {
-        cover,
-        excerpt: articleIntro,
-        meta: getPostMeta(remainingMeta),
-        url: `/article/${articleSlug}`,
-        ...remainingData,
-      };
-    });
-  };
-
   const thematicsListTitle = intl.formatMessage({
     defaultMessage: 'Other thematics',
     description: 'ThematicPage: other thematics list widget title',
@@ -187,7 +165,7 @@ const ThematicPage: NextPageWithLayout<ThematicPageProps> = ({
             </Heading>
             <PostsList
               baseUrl="/thematique/page/"
-              posts={getPosts(articles)}
+              posts={getPostsWithUrl(articles)}
               total={articles.length}
               titleLevel={3}
               byYear={true}
diff --git a/src/services/graphql/articles.query.ts b/src/services/graphql/articles.query.ts
index 7bd0901..3e1f575 100644
--- a/src/services/graphql/articles.query.ts
+++ b/src/services/graphql/articles.query.ts
@@ -56,7 +56,6 @@ export const articleBySlugQuery = `query PostBy($slug: ID!) {
       }
     }
     info {
-      readingTime
       wordsCount
     }
     modified
@@ -90,11 +89,6 @@ export const articlesQuery = `query Articles($after: String = "", $first: Int =
             }
           }
         }
-        author {
-          node {
-            name
-          }
-        }
         commentCount
         contentParts {
           beforeMore
@@ -113,7 +107,6 @@ export const articlesQuery = `query Articles($after: String = "", $first: Int =
           }
         }
         info {
-          readingTime
           wordsCount
         }
         modified
diff --git a/src/services/graphql/articles.ts b/src/services/graphql/articles.ts
index f130872..f0ba4e4 100644
--- a/src/services/graphql/articles.ts
+++ b/src/services/graphql/articles.ts
@@ -71,13 +71,12 @@ export const getArticleFromRawData = (data: RawArticle): Article => {
     id: databaseId,
     intro: contentParts.beforeMore,
     meta: {
-      author: getAuthorFromRawData(author.node, 'page'),
+      author: author && getAuthorFromRawData(author.node, 'page'),
       commentsCount: commentCount || 0,
       cover: featuredImage?.node
         ? getImageFromRawData(featuredImage.node)
         : undefined,
       dates: { publication: date, update: modified },
-      readingTime: info.readingTime,
       seo: {
         description: seo?.metaDesc || '',
         title: seo?.title || '',
diff --git a/src/services/graphql/thematics.query.ts b/src/services/graphql/thematics.query.ts
index 3b3ebd6..5a82133 100644
--- a/src/services/graphql/thematics.query.ts
+++ b/src/services/graphql/thematics.query.ts
@@ -15,13 +15,6 @@ export const thematicBySlugQuery = `query ThematicBy($slug: ID!) {
               }
             }
           }
-          author {
-            node {
-              gravatarUrl
-              name
-              url
-            }
-          }
           commentCount
           contentParts {
             beforeMore
@@ -40,7 +33,6 @@ export const thematicBySlugQuery = `query ThematicBy($slug: ID!) {
             }
           }
           info {
-            readingTime
             wordsCount
           }
           modified
@@ -67,7 +59,6 @@ export const thematicBySlugQuery = `query ThematicBy($slug: ID!) {
       }
     }
     info {
-      readingTime
       wordsCount
     }
     modified
diff --git a/src/services/graphql/thematics.ts b/src/services/graphql/thematics.ts
index e526db8..fd9d9f5 100644
--- a/src/services/graphql/thematics.ts
+++ b/src/services/graphql/thematics.ts
@@ -117,7 +117,6 @@ export const getThematicFromRawData = (data: RawThematic): Thematic => {
         ? getImageFromRawData(featuredImage.node)
         : undefined,
       dates: { publication: date, update: modified },
-      readingTime: info.readingTime,
       seo: {
         description: seo?.metaDesc || '',
         title: seo?.title || '',
diff --git a/src/services/graphql/topics.query.ts b/src/services/graphql/topics.query.ts
index 4574256..57b2569 100644
--- a/src/services/graphql/topics.query.ts
+++ b/src/services/graphql/topics.query.ts
@@ -16,13 +16,6 @@ export const topicBySlugQuery = `query TopicBy($slug: ID!) {
               }
             }
           }
-          author {
-            node {
-              gravatarUrl
-              name
-              url
-            }
-          }
           commentCount
           contentParts {
             beforeMore
@@ -41,7 +34,6 @@ export const topicBySlugQuery = `query TopicBy($slug: ID!) {
             }
           }
           info {
-            readingTime
             wordsCount
           }
           modified
@@ -68,7 +60,6 @@ export const topicBySlugQuery = `query TopicBy($slug: ID!) {
       }
     }
     info {
-      readingTime
       wordsCount
     }
     modified
diff --git a/src/services/graphql/topics.ts b/src/services/graphql/topics.ts
index 3c92442..277580f 100644
--- a/src/services/graphql/topics.ts
+++ b/src/services/graphql/topics.ts
@@ -119,7 +119,6 @@ export const getTopicFromRawData = (data: RawTopic): Topic => {
         : undefined,
       dates: { publication: date, update: modified },
       website: acfTopics.officialWebsite,
-      readingTime: info.readingTime,
       seo: {
         description: seo?.metaDesc || '',
         title: seo?.title || '',
diff --git a/src/ts/types/app.ts b/src/ts/types/app.ts
index feff5a5..cb5429b 100644
--- a/src/ts/types/app.ts
+++ b/src/ts/types/app.ts
@@ -44,7 +44,7 @@ export type Comment = {
   content: string;
   id: number;
   meta: CommentMeta;
-  parentId: number;
+  parentId?: number;
   replies: Comment[];
 };
 
@@ -80,7 +80,6 @@ export type Meta<T extends PageKind> = {
   cover?: Image;
   dates: Dates;
   license?: T extends 'project' ? string : never;
-  readingTime?: number;
   repos?: T extends 'project' ? Repos : never;
   seo: SEO;
   tagline?: T extends 'project' ? string : never;
@@ -88,7 +87,7 @@ export type Meta<T extends PageKind> = {
   thematics?: T extends 'article' | 'topic' ? PageLink[] : never;
   topics?: T extends 'article' | 'thematic' ? PageLink[] : never;
   website?: T extends 'topic' ? string : never;
-  wordsCount?: number;
+  wordsCount: number;
 };
 
 export type Page<T extends PageKind> = {
diff --git a/src/ts/types/mdx.ts b/src/ts/types/mdx.ts
index 6b72f21..16538c1 100644
--- a/src/ts/types/mdx.ts
+++ b/src/ts/types/mdx.ts
@@ -11,10 +11,7 @@ export type MDXPageMeta = Pick<Meta<'page'>, 'cover' | 'dates' | 'seo'> & {
   title: string;
 };
 
-export type MDXProjectMeta = Omit<
-  Meta<'project'>,
-  'readingTime' | 'wordsCount'
-> & {
+export type MDXProjectMeta = Exclude<Meta<'project'>, 'wordsCount'> & {
   intro: string;
   title: string;
 };
diff --git a/src/ts/types/raw-data.ts b/src/ts/types/raw-data.ts
index ba6d596..dc3db90 100644
--- a/src/ts/types/raw-data.ts
+++ b/src/ts/types/raw-data.ts
@@ -25,7 +25,6 @@ export type ContentParts = {
 };
 
 export type Info = {
-  readingTime: number;
   wordsCount: number;
 };
 
@@ -66,7 +65,7 @@ export type RawArticlePreview = Pick<
 >;
 
 export type RawPage = {
-  author: NodeResponse<RawAuthor<'page'>>;
+  author?: NodeResponse<RawAuthor<'page'>>;
   contentParts: ContentParts;
   databaseId: number;
   date: string;
diff --git a/src/utils/helpers/pages.ts b/src/utils/helpers/pages.ts
index 62337db..1a388e1 100644
--- a/src/utils/helpers/pages.ts
+++ b/src/utils/helpers/pages.ts
@@ -1,7 +1,10 @@
 import { type Post } from '@components/organisms/layout/posts-list';
 import { type LinksListItems } from '@components/organisms/widgets/links-list-widget';
-import { type Meta, type PageLink } from '@ts/types/app';
+import { type EdgesResponse } from '@services/graphql/api';
+import { getArticleFromRawData } from '@services/graphql/articles';
+import { type Article, type PageLink } from '@ts/types/app';
 import {
+  type RawArticle,
   type RawThematicPreview,
   type RawTopicPreview,
 } from '@ts/types/raw-data';
@@ -52,23 +55,35 @@ export const getLinksListItems = (
 };
 
 /**
- * Retrieve the formatted meta.
+ * Retrieve the posts list with the article URL.
  *
- * @param {Meta<'article'>} meta - The article meta.
- * @returns {Post['meta']} The formatted meta.
+ * @param {Article[]} posts - An array of articles.
+ * @returns {Post[]} An array of posts with full article URL.
  */
-export const getPostMeta = (meta: Meta<'article'>): Post['meta'] => {
-  const { commentsCount, dates, thematics, topics, wordsCount } = meta;
+export const getPostsWithUrl = (posts: Article[]): Post[] => {
+  return posts.map((post) => {
+    return {
+      ...post,
+      url: `/article/${post.slug}`,
+    };
+  });
+};
 
-  return {
-    commentsCount,
-    dates,
-    readingTime: { wordsCount: wordsCount || 0, onlyMinutes: true },
-    thematics: thematics?.map((thematic) => {
-      return { ...thematic, url: `/thematique/${thematic.slug}` };
-    }),
-    topics: topics?.map((topic) => {
-      return { ...topic, url: `/sujet/${topic.slug}` };
-    }),
-  };
+/**
+ * Retrieve the posts list from raw data.
+ *
+ * @param {EdgesResponse<RawArticle>[]} rawData - The raw data.
+ * @returns {Post[]} An array of posts.
+ */
+export const getPostsList = (rawData: EdgesResponse<RawArticle>[]): Post[] => {
+  const articlesList: RawArticle[] = [];
+  rawData.forEach((articleData) =>
+    articleData.edges.forEach((edge) => {
+      articlesList.push(edge.node);
+    })
+  );
+
+  return getPostsWithUrl(
+    articlesList.map((article) => getArticleFromRawData(article))
+  );
 };
-- 
cgit v1.2.3

