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