diff options
Diffstat (limited to 'src')
| -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 | 
16 files changed, 600 insertions, 65 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> +    </>    );  }; 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, | 
