summaryrefslogtreecommitdiffstats
path: root/src/components
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 /src/components
parent813084fc23113ae2f594bf6ef1cf53bd003c9479 (diff)
chore: add structured data using schema.org and JSON-LD
I also added the featured image on single article.
Diffstat (limited to 'src/components')
-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
5 files changed, 226 insertions, 62 deletions
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>
+ </>
);
};