summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-01-19 13:56:34 +0100
committerArmand Philippot <git@armandphilippot.com>2022-01-19 14:22:28 +0100
commita26b775b7bbf1abd3e99c8bf9ce4c7522d3a0adc (patch)
tree7f041845fa64d00f20f949d1cba14fec3eca3435
parent813084fc23113ae2f594bf6ef1cf53bd003c9479 (diff)
chore: add structured data using schema.org and JSON-LD
I also added the featured image on single article.
-rw-r--r--.env.example1
-rw-r--r--package.json1
-rw-r--r--src/components/Branding/Branding.tsx63
-rw-r--r--src/components/Breadcrumb/Breadcrumb.tsx55
-rw-r--r--src/components/Comment/Comment.tsx42
-rw-r--r--src/components/Layouts/Layout.tsx24
-rw-r--r--src/components/PostPreview/PostPreview.tsx104
-rw-r--r--src/config/website.ts1
-rw-r--r--src/pages/article/[slug].tsx67
-rw-r--r--src/pages/blog/index.tsx38
-rw-r--r--src/pages/contact.tsx40
-rw-r--r--src/pages/cv.tsx48
-rw-r--r--src/pages/index.tsx30
-rw-r--r--src/pages/mentions-legales.tsx50
-rw-r--r--src/pages/sujet/[slug].tsx52
-rw-r--r--src/pages/thematique/[slug].tsx47
-rw-r--r--src/ts/types/articles.ts2
-rw-r--r--src/utils/helpers/format.ts2
-rw-r--r--yarn.lock5
19 files changed, 607 insertions, 65 deletions
diff --git a/.env.example b/.env.example
index efeb49d..fddbd69 100644
--- a/.env.example
+++ b/.env.example
@@ -7,6 +7,7 @@ AUTHOR_EMAIL="your@email.com"
AUTHOR_URL="https://www.yourWebsite.com/"
FEED_DESCRIPTION="What you want..."
+NEXT_PUBLIC_FRONTEND_URL="$FRONTEND_URL"
NEXT_PUBLIC_GRAPHQL_API="$BACKEND_URL$GRAPHQL_ENDPOINT"
# Use this only in development mode. It prevents "unable to verify the first
diff --git a/package.json b/package.json
index 033527a..3ce04a9 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
"prismjs": "^1.25.0",
"react": "17.0.2",
"react-dom": "17.0.2",
+ "schema-dts": "^1.0.0",
"swr": "^1.1.1"
},
"devDependencies": {
diff --git a/src/components/Branding/Branding.tsx b/src/components/Branding/Branding.tsx
index 9421314..5e2cf6a 100644
--- a/src/components/Branding/Branding.tsx
+++ b/src/components/Branding/Branding.tsx
@@ -6,36 +6,57 @@ import photo from '@assets/images/armand-philippot.jpg';
import Logo from '@assets/images/armand-philippot-logo.svg';
import { config } from '@config/website';
import styles from './Branding.module.scss';
+import Head from 'next/head';
+import { Person, WithContext } from 'schema-dts';
type BrandingReturn = ({ isHome }: { isHome: boolean }) => ReactElement;
const Branding: BrandingReturn = ({ isHome = false }) => {
const TitleTag = isHome ? 'h1' : 'p';
+ const schemaJsonLd: WithContext<Person> = {
+ '@context': 'https://schema.org',
+ '@type': 'Person',
+ '@id': `${config.url}/#branding`,
+ name: config.name,
+ url: config.url,
+ jobTitle: config.baseline,
+ image: photo.src,
+ subjectOf: { '@id': `${config.url}` },
+ };
+
return (
- <div className={styles.wrapper}>
- <div className={styles.logo}>
- <div className={styles.logo__front}>
- <Image
- src={photo}
- alt={t({
- message: `${config.name} picture`,
- comment: 'Branding logo.',
- })}
- layout="responsive"
- />
- </div>
- <div className={styles.logo__back}>
- <Logo />
+ <>
+ <Head>
+ <script
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ ></script>
+ </Head>
+ <div id="branding" className={styles.wrapper}>
+ <div className={styles.logo}>
+ <div className={styles.logo__front}>
+ <Image
+ src={photo}
+ alt={t({
+ message: `${config.name} picture`,
+ comment: 'Branding logo.',
+ })}
+ layout="responsive"
+ />
+ </div>
+ <div className={styles.logo__back}>
+ <Logo />
+ </div>
</div>
+ <TitleTag className={styles.name}>
+ <Link href="/">
+ <a className={styles.link}>{config.name}</a>
+ </Link>
+ </TitleTag>
+ <p className={styles.job}>{config.baseline}</p>
</div>
- <TitleTag className={styles.name}>
- <Link href="/">
- <a className={styles.link}>{config.name}</a>
- </Link>
- </TitleTag>
- <p className={styles.job}>{config.baseline}</p>
- </div>
+ </>
);
};
diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx
index 77e7c08..0b9977e 100644
--- a/src/components/Breadcrumb/Breadcrumb.tsx
+++ b/src/components/Breadcrumb/Breadcrumb.tsx
@@ -1,7 +1,9 @@
+import { config } from '@config/website';
import { t } from '@lingui/macro';
import Head from 'next/head';
import Link from 'next/link';
import { useRouter } from 'next/router';
+import { BreadcrumbList, WithContext } from 'schema-dts';
import styles from './Breadcrumb.module.scss';
const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => {
@@ -15,9 +17,6 @@ const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => {
const getItems = () => {
return (
<>
- <Head>
- <script type="application/ld+json">{}</script>
- </Head>
<li className={styles.item}>
<Link href="/">
<a>{t`Home`}</a>
@@ -32,14 +31,62 @@ const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => {
</li>
</>
)}
+ <li className="screen-reader-text">{pageTitle}</li>
</>
);
};
+ const getElementsSchema = () => {
+ const items = [];
+ const homepage: BreadcrumbList['itemListElement'] = {
+ '@type': 'ListItem',
+ position: 1,
+ name: t`Home`,
+ item: config.url,
+ };
+
+ items.push(homepage);
+
+ if (isArticle || isThematic || isSubject) {
+ const blog: BreadcrumbList['itemListElement'] = {
+ '@type': 'ListItem',
+ position: 2,
+ name: t`Blog`,
+ item: `${config.url}/blog`,
+ };
+
+ items.push(blog);
+ }
+
+ const currentPage: BreadcrumbList['itemListElement'] = {
+ '@type': 'ListItem',
+ position: items.length + 1,
+ name: pageTitle,
+ item: `${config.url}${router.asPath}`,
+ };
+
+ items.push(currentPage);
+
+ return items;
+ };
+
+ const schemaJsonLd: WithContext<BreadcrumbList> = {
+ '@context': 'https://schema.org',
+ '@type': 'BreadcrumbList',
+ '@id': `${config.url}/#breadcrumb`,
+ itemListElement: getElementsSchema(),
+ };
+
return (
<>
+ <Head>
+ <script
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ ></script>
+ </Head>
{!isHome && (
- <nav className={styles.wrapper}>
+ <nav id="breadcrumb" className={styles.wrapper}>
<span className="screen-reader-text">{t`You are here:`}</span>
<ol className={styles.list}>{getItems()}</ol>
</nav>
diff --git a/src/components/Comment/Comment.tsx b/src/components/Comment/Comment.tsx
index e0a65f3..11300fc 100644
--- a/src/components/Comment/Comment.tsx
+++ b/src/components/Comment/Comment.tsx
@@ -1,11 +1,14 @@
import { Button } from '@components/Buttons';
import CommentForm from '@components/CommentForm/CommentForm';
+import { config } from '@config/website';
import { t } from '@lingui/macro';
import { Comment as CommentData } from '@ts/types/comments';
+import Head from 'next/head';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useEffect, useRef, useState } from 'react';
+import { Comment as CommentSchema, WithContext } from 'schema-dts';
import styles from './Comment.module.scss';
const Comment = ({
@@ -117,10 +120,43 @@ const Comment = ({
return <p>{t`This comment is awaiting moderation.`}</p>;
};
+ const schemaJsonLd: WithContext<CommentSchema> = {
+ '@context': 'https://schema.org',
+ '@id': `${config.url}/#comment-${comment.commentId}`,
+ '@type': 'Comment',
+ parentItem: isNested
+ ? { '@id': `${config.url}/#comment-${comment.parentDatabaseId}` }
+ : undefined,
+ about: { '@type': 'Article', '@id': `${config.url}/#article` },
+ author: {
+ '@type': 'Person',
+ name: comment.author.name,
+ image: comment.author.gravatarUrl,
+ url: comment.author.url,
+ },
+ creator: {
+ '@type': 'Person',
+ name: comment.author.name,
+ image: comment.author.gravatarUrl,
+ url: comment.author.url,
+ },
+ dateCreated: comment.date,
+ datePublished: comment.date,
+ text: comment.content,
+ };
+
return (
- <li className={styles.item}>
- {comment.approved ? getApprovedComment() : getCommentStatus()}
- </li>
+ <>
+ <Head>
+ <script
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ ></script>
+ </Head>
+ <li className={styles.item}>
+ {comment.approved ? getApprovedComment() : getCommentStatus()}
+ </li>
+ </>
);
};
diff --git a/src/components/Layouts/Layout.tsx b/src/components/Layouts/Layout.tsx
index f5116f8..2e7d255 100644
--- a/src/components/Layouts/Layout.tsx
+++ b/src/components/Layouts/Layout.tsx
@@ -7,6 +7,7 @@ import { t } from '@lingui/macro';
import Head from 'next/head';
import { config } from '@config/website';
import { useRouter } from 'next/router';
+import { WebSite, WithContext } from 'schema-dts';
const Layout = ({
children,
@@ -22,6 +23,25 @@ const Layout = ({
ref.current?.focus();
}, [asPath]);
+ const schemaJsonLd: WithContext<WebSite> = {
+ '@context': 'https://schema.org',
+ '@id': `${config.url}`,
+ '@type': 'WebSite',
+ name: config.name,
+ description: config.baseline,
+ url: config.url,
+ author: { '@id': `${config.url}/#branding` },
+ copyrightYear: Number(config.copyright.startYear),
+ creator: { '@id': `${config.url}/#branding` },
+ editor: { '@id': `${config.url}/#branding` },
+ inLanguage: config.defaultLocale,
+ potentialAction: {
+ '@type': 'SearchAction',
+ target: `${config.url}/recherche?s={query}`,
+ query: 'required',
+ },
+ };
+
return (
<>
<Head>
@@ -43,6 +63,10 @@ const Layout = ({
type="application/feed+json"
title={`${config.name}'s RSS feed`}
/>
+ <script
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ ></script>
</Head>
<span ref={ref} tabIndex={-1} />
<a href="#main" className="screen-reader-text">{t`Skip to content`}</a>
diff --git a/src/components/PostPreview/PostPreview.tsx b/src/components/PostPreview/PostPreview.tsx
index 3bf7bdb..2a0bcf1 100644
--- a/src/components/PostPreview/PostPreview.tsx
+++ b/src/components/PostPreview/PostPreview.tsx
@@ -7,6 +7,9 @@ import Image from 'next/image';
import { ButtonLink } from '@components/Buttons';
import { ArrowIcon } from '@components/Icons';
import { TitleLevel } from '@ts/types/app';
+import { BlogPosting, WithContext } from 'schema-dts';
+import Head from 'next/head';
+import { config } from '@config/website';
const PostPreview = ({
post,
@@ -24,41 +27,74 @@ const PostPreview = ({
thematics: post.thematics,
};
+ const publicationDate = new Date(post.dates.publication);
+ const updateDate = new Date(post.dates.update);
+
+ const schemaJsonLd: WithContext<BlogPosting> = {
+ '@context': 'https://schema.org',
+ '@type': 'BlogPosting',
+ name: post.title,
+ description: post.intro,
+ articleBody: post.intro,
+ author: { '@id': `${config.url}/#branding` },
+ commentCount: post.commentCount ? post.commentCount : 0,
+ copyrightYear: publicationDate.getFullYear(),
+ creator: { '@id': `${config.url}/#branding` },
+ dateCreated: publicationDate.toISOString(),
+ dateModified: updateDate.toISOString(),
+ datePublished: publicationDate.toISOString(),
+ editor: { '@id': `${config.url}/#branding` },
+ image: post.featuredImage?.sourceUrl,
+ inLanguage: config.defaultLocale,
+ isBasedOn: `${config.url}/article/${post.slug}`,
+ isPartOf: { '@id': `${config.url}/blog` },
+ license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ thumbnailUrl: post.featuredImage?.sourceUrl,
+ };
+
return (
- <article className={styles.wrapper}>
- {post.featuredImage && Object.keys(post.featuredImage).length > 0 && (
- <div className={styles.cover}>
- <Image
- src={post.featuredImage.sourceUrl}
- alt={post.featuredImage.altText}
- layout="fill"
- objectFit="contain"
- />
- </div>
- )}
- <header className={styles.header}>
- <TitleTag className={styles.title}>
- <Link href={`/article/${post.slug}`}>
- <a>{post.title}</a>
- </Link>
- </TitleTag>
- </header>
- <div
- className={styles.body}
- dangerouslySetInnerHTML={{ __html: post.intro }}
- ></div>
- <footer className={styles.footer}>
- <ButtonLink target={`/article/${post.slug}`} position="left">
- {t`Read more`}
- <span className="screen-reader-text">
- {' '}
- {t({ message: `about ${post.title}`, comment: 'Post title' })}
- </span>
- <ArrowIcon />
- </ButtonLink>
- </footer>
- <PostMeta meta={meta} />
- </article>
+ <>
+ <Head>
+ <script
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ ></script>
+ </Head>
+ <article className={styles.wrapper}>
+ {post.featuredImage && Object.keys(post.featuredImage).length > 0 && (
+ <div className={styles.cover}>
+ <Image
+ src={post.featuredImage.sourceUrl}
+ alt={post.featuredImage.altText}
+ layout="fill"
+ objectFit="contain"
+ />
+ </div>
+ )}
+ <header className={styles.header}>
+ <TitleTag className={styles.title}>
+ <Link href={`/article/${post.slug}`}>
+ <a>{post.title}</a>
+ </Link>
+ </TitleTag>
+ </header>
+ <div
+ className={styles.body}
+ dangerouslySetInnerHTML={{ __html: post.intro }}
+ ></div>
+ <footer className={styles.footer}>
+ <ButtonLink target={`/article/${post.slug}`} position="left">
+ {t`Read more`}
+ <span className="screen-reader-text">
+ {' '}
+ {t({ message: `about ${post.title}`, comment: 'Post title' })}
+ </span>
+ <ArrowIcon />
+ </ButtonLink>
+ </footer>
+ <PostMeta meta={meta} />
+ </article>
+ </>
);
};
diff --git a/src/config/website.ts b/src/config/website.ts
index a1e238e..d8721c5 100644
--- a/src/config/website.ts
+++ b/src/config/website.ts
@@ -9,4 +9,5 @@ export const config = {
},
defaultLocale: 'fr',
postsPerPage: 10,
+ url: process.env.NEXT_PUBLIC_FRONTEND_URL,
};
diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx
index e519c27..8c345b7 100644
--- a/src/pages/article/[slug].tsx
+++ b/src/pages/article/[slug].tsx
@@ -18,6 +18,7 @@ import { useEffect } from 'react';
import styles from '@styles/pages/Page.module.scss';
import { Sharing, ToC } from '@components/Widgets';
import Sidebar from '@components/Sidebar/Sidebar';
+import { Blog, BlogPosting, Graph, WebPage } from 'schema-dts';
const SingleArticle: NextPageWithLayout<ArticleProps> = ({ post }) => {
const {
@@ -26,6 +27,7 @@ const SingleArticle: NextPageWithLayout<ArticleProps> = ({ post }) => {
content,
databaseId,
dates,
+ featuredImage,
intro,
seo,
subjects,
@@ -52,13 +54,74 @@ const SingleArticle: NextPageWithLayout<ArticleProps> = ({ post }) => {
translateCopyButton(locale);
}, [locale]);
+ const webpageSchema: WebPage = {
+ '@id': `${config.url}${router.asPath}`,
+ '@type': 'WebPage',
+ breadcrumb: { '@id': `${config.url}/#breadcrumb` },
+ lastReviewed: dates.update,
+ name: seo.title,
+ description: seo.metaDesc,
+ reviewedBy: { '@id': `${config.url}/#branding` },
+ url: `${config.url}${router.asPath}`,
+ isPartOf: {
+ '@id': `${config.url}`,
+ },
+ };
+
+ const blogSchema: Blog = {
+ '@id': `${config.url}/#blog`,
+ '@type': 'Blog',
+ blogPost: { '@id': `${config.url}/#article` },
+ isPartOf: {
+ '@id': `${config.url}${router.asPath}`,
+ },
+ license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ };
+
+ const publicationDate = new Date(dates.publication);
+ const updateDate = new Date(dates.update);
+
+ const blogPostSchema: BlogPosting = {
+ '@id': `${config.url}/#article`,
+ '@type': 'BlogPosting',
+ name: title,
+ description: intro,
+ articleBody: content,
+ author: { '@id': `${config.url}/#branding` },
+ commentCount: comments.length,
+ copyrightYear: publicationDate.getFullYear(),
+ creator: { '@id': `${config.url}/#branding` },
+ dateCreated: publicationDate.toISOString(),
+ dateModified: updateDate.toISOString(),
+ datePublished: publicationDate.toISOString(),
+ discussionUrl: `${config.url}${router.asPath}/#comments`,
+ editor: { '@id': `${config.url}/#branding` },
+ image: featuredImage?.sourceUrl,
+ inLanguage: config.defaultLocale,
+ isPartOf: {
+ '@id': `${config.url}/blog`,
+ },
+ license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ mainEntityOfPage: { '@id': `${config.url}${router.asPath}` },
+ thumbnailUrl: featuredImage?.sourceUrl,
+ };
+
+ const schemaJsonLd: Graph = {
+ '@context': 'https://schema.org',
+ '@graph': [webpageSchema, blogSchema, blogPostSchema],
+ };
+
return (
<>
<Head>
<title>{seo.title}</title>
<meta name="description" content={seo.metaDesc} />
+ <script
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ ></script>
</Head>
- <article className={styles.article}>
+ <article id="article" className={styles.article}>
<PostHeader intro={intro} meta={meta} title={title} />
<Sidebar position="left">
<ToC />
@@ -73,7 +136,7 @@ const SingleArticle: NextPageWithLayout<ArticleProps> = ({ post }) => {
</Sidebar>
<section id="comments" className={styles.comments}>
<CommentsList articleId={databaseId} comments={comments} />
- <CommentForm articleId={post.databaseId} />
+ <CommentForm articleId={databaseId} />
</section>
</article>
</>
diff --git a/src/pages/blog/index.tsx b/src/pages/blog/index.tsx
index 48fab1c..765a93b 100644
--- a/src/pages/blog/index.tsx
+++ b/src/pages/blog/index.tsx
@@ -17,9 +17,12 @@ import Sidebar from '@components/Sidebar/Sidebar';
import styles from '@styles/pages/Page.module.scss';
import { useRef } from 'react';
import Spinner from '@components/Spinner/Spinner';
+import { Blog as BlogSchema, Graph, WebPage } from 'schema-dts';
+import { useRouter } from 'next/router';
const Blog: NextPageWithLayout<BlogPageProps> = ({ fallback }) => {
const lastPostRef = useRef<HTMLSpanElement>(null);
+ const router = useRouter();
const getKey = (pageIndex: number, previousData: PostsListData) => {
if (previousData && !previousData.posts) return null;
@@ -59,13 +62,48 @@ const Blog: NextPageWithLayout<BlogPageProps> = ({ fallback }) => {
return <PostsList ref={lastPostRef} data={data} showYears={true} />;
};
+ const webpageSchema: WebPage = {
+ '@id': `${config.url}${router.asPath}`,
+ '@type': 'WebPage',
+ breadcrumb: { '@id': `${config.url}/#breadcrumb` },
+ name: seo.blog.title,
+ description: seo.blog.description,
+ inLanguage: config.defaultLocale,
+ reviewedBy: { '@id': `${config.url}/#branding` },
+ url: `${config.url}`,
+ isPartOf: {
+ '@id': `${config.url}`,
+ },
+ };
+
+ const blogSchema: BlogSchema = {
+ '@id': `${config.url}/#blog`,
+ '@type': 'Blog',
+ author: { '@id': `${config.url}/#branding` },
+ creator: { '@id': `${config.url}/#branding` },
+ editor: { '@id': `${config.url}/#branding` },
+ inLanguage: config.defaultLocale,
+ license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ mainEntityOfPage: { '@id': `${config.url}${router.asPath}` },
+ };
+
+ const schemaJsonLd: Graph = {
+ '@context': 'https://schema.org',
+ '@graph': [webpageSchema, blogSchema],
+ };
+
return (
<>
<Head>
<title>{seo.blog.title}</title>
<meta name="description" content={seo.blog.description} />
+ <script
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ ></script>
</Head>
<article
+ id="blog"
className={`${styles.article} ${styles['article--no-comments']}`}
>
<PostHeader title={t`Blog`} />
diff --git a/src/pages/contact.tsx b/src/pages/contact.tsx
index bafa5e9..ba462c0 100644
--- a/src/pages/contact.tsx
+++ b/src/pages/contact.tsx
@@ -13,6 +13,9 @@ import PostHeader from '@components/PostHeader/PostHeader';
import styles from '@styles/pages/Page.module.scss';
import { SocialMedia } from '@components/Widgets';
import Sidebar from '@components/Sidebar/Sidebar';
+import { ContactPage as ContactPageSchema, Graph, WebPage } from 'schema-dts';
+import { config } from '@config/website';
+import { useRouter } from 'next/router';
const ContactPage: NextPageWithLayout = () => {
const [name, setName] = useState('');
@@ -20,6 +23,7 @@ const ContactPage: NextPageWithLayout = () => {
const [subject, setSubject] = useState('');
const [message, setMessage] = useState('');
const [status, setStatus] = useState('');
+ const router = useRouter();
const resetForm = () => {
setName('');
@@ -55,13 +59,49 @@ const ContactPage: NextPageWithLayout = () => {
const title = t`Contact`;
const intro = t`Please fill the form to contact me.`;
+ const webpageSchema: WebPage = {
+ '@id': `${config.url}${router.asPath}`,
+ '@type': 'WebPage',
+ breadcrumb: { '@id': `${config.url}/#breadcrumb` },
+ name: seo.contact.title,
+ description: seo.contact.description,
+ reviewedBy: { '@id': `${config.url}/#branding` },
+ url: `${config.url}${router.asPath}`,
+ isPartOf: {
+ '@id': `${config.url}`,
+ },
+ };
+
+ const contactSchema: ContactPageSchema = {
+ '@id': `${config.url}/#contact`,
+ '@type': 'ContactPage',
+ name: title,
+ description: intro,
+ author: { '@id': `${config.url}/#branding` },
+ creator: { '@id': `${config.url}/#branding` },
+ editor: { '@id': `${config.url}/#branding` },
+ inLanguage: config.defaultLocale,
+ license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ mainEntityOfPage: { '@id': `${config.url}${router.asPath}` },
+ };
+
+ const schemaJsonLd: Graph = {
+ '@context': 'https://schema.org',
+ '@graph': [webpageSchema, contactSchema],
+ };
+
return (
<>
<Head>
<title>{seo.contact.title}</title>
<meta name="description" content={seo.contact.description} />
+ <script
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ ></script>
</Head>
<article
+ id="contact"
className={`${styles.article} ${styles['article--no-comments']}`}
>
<PostHeader title={title} intro={intro} />
diff --git a/src/pages/cv.tsx b/src/pages/cv.tsx
index 01eab4c..78e9a6e 100644
--- a/src/pages/cv.tsx
+++ b/src/pages/cv.tsx
@@ -11,8 +11,12 @@ import styles from '@styles/pages/Page.module.scss';
import { CVPreview, SocialMedia, ToC } from '@components/Widgets';
import { t } from '@lingui/macro';
import Sidebar from '@components/Sidebar/Sidebar';
+import { AboutPage, Graph, WebPage } from 'schema-dts';
+import { config } from '@config/website';
+import { useRouter } from 'next/router';
const CV: NextPageWithLayout = () => {
+ const router = useRouter();
const dates = {
publication: meta.publishedOn,
update: meta.updatedOn,
@@ -22,13 +26,57 @@ const CV: NextPageWithLayout = () => {
dates,
};
+ const webpageSchema: WebPage = {
+ '@id': `${config.url}${router.asPath}`,
+ '@type': 'WebPage',
+ breadcrumb: { '@id': `${config.url}/#breadcrumb` },
+ name: seo.cv.title,
+ description: seo.cv.description,
+ reviewedBy: { '@id': `${config.url}/#branding` },
+ url: `${config.url}${router.asPath}`,
+ isPartOf: {
+ '@id': `${config.url}`,
+ },
+ };
+
+ const publicationDate = new Date(dates.publication);
+ const updateDate = new Date(dates.update);
+
+ const cvSchema: AboutPage = {
+ '@id': `${config.url}/#cv`,
+ '@type': 'AboutPage',
+ name: `${config.name} CV`,
+ description: intro,
+ author: { '@id': `${config.url}/#branding` },
+ creator: { '@id': `${config.url}/#branding` },
+ dateCreated: publicationDate.toISOString(),
+ dateModified: updateDate.toISOString(),
+ datePublished: publicationDate.toISOString(),
+ editor: { '@id': `${config.url}/#branding` },
+ image,
+ inLanguage: config.defaultLocale,
+ license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ thumbnailUrl: image,
+ mainEntityOfPage: { '@id': `${config.url}${router.asPath}` },
+ };
+
+ const schemaJsonLd: Graph = {
+ '@context': 'https://schema.org',
+ '@graph': [webpageSchema, cvSchema],
+ };
+
return (
<>
<Head>
<title>{seo.cv.title}</title>
<meta name="description" content={seo.cv.description} />
+ <script
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ ></script>
</Head>
<article
+ id="cv"
className={`${styles.article} ${styles['article--no-comments']}`}
>
<PostHeader intro={intro} meta={pageMeta} title={meta.title} />
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index 3664ae1..f59602f 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -11,6 +11,8 @@ import styles from '@styles/pages/Home.module.scss';
import { t } from '@lingui/macro';
import FeedIcon from '@assets/images/icon-feed.svg';
import { ContactIcon } from '@components/Icons';
+import { Graph, WebPage } from 'schema-dts';
+import { config } from '@config/website';
const Home: NextPageWithLayout = () => {
const CodingLinks = () => {
@@ -90,13 +92,39 @@ const Home: NextPageWithLayout = () => {
MoreLinks: MoreLinks,
};
+ const webpageSchema: WebPage = {
+ '@id': `${config.url}/#home`,
+ '@type': 'WebPage',
+ breadcrumb: { '@id': `${config.url}/#breadcrumb` },
+ name: seo.legalNotice.title,
+ description: seo.legalNotice.description,
+ author: { '@id': `${config.url}/#branding` },
+ creator: { '@id': `${config.url}/#branding` },
+ editor: { '@id': `${config.url}/#branding` },
+ inLanguage: config.defaultLocale,
+ license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ reviewedBy: { '@id': `${config.url}/#branding` },
+ url: `${config.url}`,
+ };
+
+ const schemaJsonLd: Graph = {
+ '@context': 'https://schema.org',
+ '@graph': [webpageSchema],
+ };
+
return (
<>
<Head>
<title>{seo.homepage.title}</title>
<meta name="description" content={seo.homepage.description} />
+ <script
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ ></script>
</Head>
- <HomePageContent components={components} />
+ <div id="home">
+ <HomePageContent components={components} />
+ </div>
</>
);
};
diff --git a/src/pages/mentions-legales.tsx b/src/pages/mentions-legales.tsx
index fcaef06..81d8e98 100644
--- a/src/pages/mentions-legales.tsx
+++ b/src/pages/mentions-legales.tsx
@@ -13,8 +13,13 @@ import { ArticleMeta } from '@ts/types/articles';
import styles from '@styles/pages/Page.module.scss';
import { ToC } from '@components/Widgets';
import Sidebar from '@components/Sidebar/Sidebar';
+import { Article, Graph, WebPage } from 'schema-dts';
+import { config } from '@config/website';
+import { useRouter } from 'next/router';
+import { t } from '@lingui/macro';
const LegalNotice: NextPageWithLayout = () => {
+ const router = useRouter();
const dates = {
publication: meta.publishedOn,
update: meta.updatedOn,
@@ -24,13 +29,58 @@ const LegalNotice: NextPageWithLayout = () => {
dates,
};
+ const publicationDate = new Date(dates.publication);
+ const updateDate = new Date(dates.update);
+
+ const webpageSchema: WebPage = {
+ '@id': `${config.url}${router.asPath}`,
+ '@type': 'WebPage',
+ breadcrumb: { '@id': `${config.url}/#breadcrumb` },
+ name: seo.legalNotice.title,
+ description: seo.legalNotice.description,
+ inLanguage: config.defaultLocale,
+ license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ reviewedBy: { '@id': `${config.url}/#branding` },
+ url: `${config.url}${router.asPath}`,
+ isPartOf: {
+ '@id': `${config.url}`,
+ },
+ };
+
+ const articleSchema: Article = {
+ '@id': `${config.url}/#legal-notice`,
+ '@type': 'Article',
+ name: t`Legal notice`,
+ description: intro,
+ author: { '@id': `${config.url}/#branding` },
+ copyrightYear: publicationDate.getFullYear(),
+ creator: { '@id': `${config.url}/#branding` },
+ dateCreated: publicationDate.toISOString(),
+ dateModified: updateDate.toISOString(),
+ datePublished: publicationDate.toISOString(),
+ editor: { '@id': `${config.url}/#branding` },
+ inLanguage: config.defaultLocale,
+ license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ mainEntityOfPage: { '@id': `${config.url}${router.asPath}` },
+ };
+
+ const schemaJsonLd: Graph = {
+ '@context': 'https://schema.org',
+ '@graph': [webpageSchema, articleSchema],
+ };
+
return (
<>
<Head>
<title>{seo.legalNotice.title}</title>
<meta name="description" content={seo.legalNotice.description} />
+ <script
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ ></script>
</Head>
<article
+ id="legal-notice"
className={`${styles.article} ${styles['article--no-comments']}`}
>
<PostHeader intro={intro} meta={pageMeta} title={meta.title} />
diff --git a/src/pages/sujet/[slug].tsx b/src/pages/sujet/[slug].tsx
index b373041..97c76c0 100644
--- a/src/pages/sujet/[slug].tsx
+++ b/src/pages/sujet/[slug].tsx
@@ -17,9 +17,13 @@ import { RelatedThematics, ToC, TopicsList } from '@components/Widgets';
import { useRef } from 'react';
import Head from 'next/head';
import Sidebar from '@components/Sidebar/Sidebar';
+import { Article as Article, Blog, Graph, WebPage } from 'schema-dts';
+import { config } from '@config/website';
+import { useRouter } from 'next/router';
const Subject: NextPageWithLayout<SubjectProps> = ({ subject }) => {
const relatedThematics = useRef<ThematicPreview[]>([]);
+ const router = useRouter();
const updateRelatedThematics = (newThematics: ThematicPreview[]) => {
newThematics.forEach((thematic) => {
@@ -49,13 +53,61 @@ const Subject: NextPageWithLayout<SubjectProps> = ({ subject }) => {
website: subject.officialWebsite,
};
+ const webpageSchema: WebPage = {
+ '@id': `${config.url}${router.asPath}`,
+ '@type': 'WebPage',
+ breadcrumb: { '@id': `${config.url}/#breadcrumb` },
+ name: subject.seo.title,
+ description: subject.seo.metaDesc,
+ inLanguage: config.defaultLocale,
+ reviewedBy: { '@id': `${config.url}/#branding` },
+ url: `${config.url}`,
+ isPartOf: {
+ '@id': `${config.url}`,
+ },
+ };
+
+ const publicationDate = new Date(subject.dates.publication);
+ const updateDate = new Date(subject.dates.update);
+
+ const articleSchema: Article = {
+ '@id': `${config.url}/subject`,
+ '@type': 'Article',
+ name: subject.title,
+ description: subject.intro,
+ author: { '@id': `${config.url}/#branding` },
+ copyrightYear: publicationDate.getFullYear(),
+ creator: { '@id': `${config.url}/#branding` },
+ dateCreated: publicationDate.toISOString(),
+ dateModified: updateDate.toISOString(),
+ datePublished: publicationDate.toISOString(),
+ editor: { '@id': `${config.url}/#branding` },
+ thumbnailUrl: subject.featuredImage?.sourceUrl,
+ image: subject.featuredImage?.sourceUrl,
+ inLanguage: config.defaultLocale,
+ isPartOf: { '@id': `${config.url}/blog` },
+ license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ mainEntityOfPage: { '@id': `${config.url}${router.asPath}` },
+ subjectOf: { '@id': `${config.url}/blog` },
+ };
+
+ const schemaJsonLd: Graph = {
+ '@context': 'https://schema.org',
+ '@graph': [webpageSchema, articleSchema],
+ };
+
return (
<>
<Head>
<title>{subject.seo.title}</title>
<meta name="description" content={subject.seo.metaDesc} />
+ <script
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ ></script>
</Head>
<article
+ id="subject"
className={`${styles.article} ${styles['article--no-comments']}`}
>
<PostHeader
diff --git a/src/pages/thematique/[slug].tsx b/src/pages/thematique/[slug].tsx
index 4eee656..660a207 100644
--- a/src/pages/thematique/[slug].tsx
+++ b/src/pages/thematique/[slug].tsx
@@ -17,9 +17,13 @@ import { useRef } from 'react';
import { ArticleMeta } from '@ts/types/articles';
import Head from 'next/head';
import Sidebar from '@components/Sidebar/Sidebar';
+import { Article, Blog, Graph, WebPage } from 'schema-dts';
+import { config } from '@config/website';
+import { useRouter } from 'next/router';
const Thematic: NextPageWithLayout<ThematicProps> = ({ thematic }) => {
const relatedSubjects = useRef<SubjectPreview[]>([]);
+ const router = useRouter();
const updateRelatedSubjects = (newSubjects: SubjectPreview[]) => {
newSubjects.forEach((subject) => {
@@ -48,13 +52,56 @@ const Thematic: NextPageWithLayout<ThematicProps> = ({ thematic }) => {
dates: thematic.dates,
};
+ const webpageSchema: WebPage = {
+ '@id': `${config.url}${router.asPath}`,
+ '@type': 'WebPage',
+ breadcrumb: { '@id': `${config.url}/#breadcrumb` },
+ name: thematic.seo.title,
+ description: thematic.seo.metaDesc,
+ inLanguage: config.defaultLocale,
+ reviewedBy: { '@id': `${config.url}/#branding` },
+ url: `${config.url}`,
+ };
+
+ const publicationDate = new Date(thematic.dates.publication);
+ const updateDate = new Date(thematic.dates.update);
+
+ const articleSchema: Article = {
+ '@id': `${config.url}/thematic`,
+ '@type': 'Article',
+ name: thematic.title,
+ description: thematic.intro,
+ author: { '@id': `${config.url}/#branding` },
+ copyrightYear: publicationDate.getFullYear(),
+ creator: { '@id': `${config.url}/#branding` },
+ dateCreated: publicationDate.toISOString(),
+ dateModified: updateDate.toISOString(),
+ datePublished: publicationDate.toISOString(),
+ editor: { '@id': `${config.url}/#branding` },
+ inLanguage: config.defaultLocale,
+ isPartOf: { '@id': `${config.url}/blog` },
+ license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ mainEntityOfPage: { '@id': `${config.url}${router.asPath}` },
+ subjectOf: { '@id': `${config.url}/blog` },
+ };
+
+ const schemaJsonLd: Graph = {
+ '@context': 'https://schema.org',
+ '@graph': [webpageSchema, articleSchema],
+ };
+
return (
<>
<Head>
<title>{thematic.seo.title}</title>
<meta name="description" content={thematic.seo.metaDesc} />
+ <script
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ ></script>
</Head>
<article
+ id="thematic"
className={`${styles.article} ${styles['article--no-comments']}`}
>
<PostHeader intro={thematic.intro} meta={meta} title={thematic.title} />
diff --git a/src/ts/types/articles.ts b/src/ts/types/articles.ts
index 01a4c38..1fb3ec5 100644
--- a/src/ts/types/articles.ts
+++ b/src/ts/types/articles.ts
@@ -40,6 +40,7 @@ export type Article = {
content: string;
databaseId: number;
dates: Dates;
+ featuredImage: Cover;
id: string;
intro: string;
seo: SEO;
@@ -57,6 +58,7 @@ export type RawArticle = Pick<
comments: CommentsNode;
contentParts: ContentParts;
date: string;
+ featuredImage: RawCover;
modified: string;
};
diff --git a/src/utils/helpers/format.ts b/src/utils/helpers/format.ts
index b79daef..374df76 100644
--- a/src/utils/helpers/format.ts
+++ b/src/utils/helpers/format.ts
@@ -223,6 +223,7 @@ export const getFormattedPost = (rawPost: RawArticle): Article => {
contentParts,
databaseId,
date,
+ featuredImage,
id,
modified,
seo,
@@ -247,6 +248,7 @@ export const getFormattedPost = (rawPost: RawArticle): Article => {
content: contentParts.afterMore,
databaseId,
dates,
+ featuredImage: featuredImage ? featuredImage.node : null,
id,
intro: contentParts.beforeMore,
seo,
diff --git a/yarn.lock b/yarn.lock
index 2b5d02b..8293cb1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7692,6 +7692,11 @@ scheduler@^0.20.2:
loose-envify "^1.1.0"
object-assign "^4.1.1"
+schema-dts@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/schema-dts/-/schema-dts-1.0.0.tgz#60e8a0f2cef5e644c44c843b03d35b37a4423c01"
+ integrity sha512-9t8gnY3RW2CbpuvA0pIpcaHFXkJTeNnWR4uaWI+PiYSfpuEeMw+2Q0Gac6YTnQb1B8TR6/+G71gQWuSE7dq6Zw==
+
"semver@2 || 3 || 4 || 5":
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"