diff options
| -rw-r--r-- | .env.example | 1 | ||||
| -rw-r--r-- | package.json | 1 | ||||
| -rw-r--r-- | src/components/Branding/Branding.tsx | 63 | ||||
| -rw-r--r-- | src/components/Breadcrumb/Breadcrumb.tsx | 55 | ||||
| -rw-r--r-- | src/components/Comment/Comment.tsx | 42 | ||||
| -rw-r--r-- | src/components/Layouts/Layout.tsx | 24 | ||||
| -rw-r--r-- | src/components/PostPreview/PostPreview.tsx | 104 | ||||
| -rw-r--r-- | src/config/website.ts | 1 | ||||
| -rw-r--r-- | src/pages/article/[slug].tsx | 67 | ||||
| -rw-r--r-- | src/pages/blog/index.tsx | 38 | ||||
| -rw-r--r-- | src/pages/contact.tsx | 40 | ||||
| -rw-r--r-- | src/pages/cv.tsx | 48 | ||||
| -rw-r--r-- | src/pages/index.tsx | 30 | ||||
| -rw-r--r-- | src/pages/mentions-legales.tsx | 50 | ||||
| -rw-r--r-- | src/pages/sujet/[slug].tsx | 52 | ||||
| -rw-r--r-- | src/pages/thematique/[slug].tsx | 47 | ||||
| -rw-r--r-- | src/ts/types/articles.ts | 2 | ||||
| -rw-r--r-- | src/utils/helpers/format.ts | 2 | ||||
| -rw-r--r-- | yarn.lock | 5 |
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, @@ -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" |
