aboutsummaryrefslogtreecommitdiffstats
path: root/src/utils/helpers/schema-org.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/utils/helpers/schema-org.ts')
-rw-r--r--src/utils/helpers/schema-org.ts561
1 files changed, 399 insertions, 162 deletions
diff --git a/src/utils/helpers/schema-org.ts b/src/utils/helpers/schema-org.ts
index 633c35a..7710aba 100644
--- a/src/utils/helpers/schema-org.ts
+++ b/src/utils/helpers/schema-org.ts
@@ -1,261 +1,498 @@
import type {
AboutPage,
- Article,
Blog,
BlogPosting,
+ BreadcrumbList,
Comment as CommentSchema,
ContactPage,
+ Duration,
Graph,
+ ListItem,
+ Person,
+ SearchAction,
+ SearchResultsPage,
WebPage,
+ WebSite,
} from 'schema-dts';
-import type { Dates, SingleComment } from '../../types';
import { CONFIG } from '../config';
-import { ROUTES } from '../constants';
+import {
+ ARTICLE_ID,
+ AUTHOR_ID,
+ COMMENTS_SECTION_ID,
+ COMMENT_ID_PREFIX,
+ ROUTES,
+} from '../constants';
import { trimTrailingChars } from './strings';
const host = trimTrailingChars(CONFIG.url, '/');
-export type GetBlogSchemaProps = {
- /**
- * True if the page is part of the blog.
- */
- isSinglePage: boolean;
+/**
+ * Retrieve a Person schema in JSON-LD format for the website owner.
+ *
+ * @returns {Person} A Person graph.
+ */
+export const getAuthorGraph = (): Person => {
+ return {
+ '@type': 'Person',
+ '@id': `${host}#${AUTHOR_ID}`,
+ givenName: CONFIG.name.split(' ')[0],
+ image: `${host}/armand-philippot.jpg`,
+ jobTitle: CONFIG.baseline,
+ knowsLanguage: [
+ {
+ '@type': 'Language',
+ name: 'French',
+ alternateName: 'fr',
+ },
+ {
+ '@type': 'Language',
+ name: 'English',
+ alternateName: 'en',
+ },
+ {
+ '@type': 'Language',
+ name: 'Spanish',
+ alternateName: 'es',
+ },
+ ],
+ nationality: {
+ '@type': 'Country',
+ name: 'France',
+ },
+ name: CONFIG.name,
+ url: host,
+ };
+};
+
+export type WebSiteData = {
/**
- * The page locale.
+ * A description of the website.
*/
- locale: string;
+ description: string;
/**
- * The page slug with a leading slash.
+ * The website title.
*/
- slug: string;
+ title: string;
+};
+
+export type CustomSearchAction = SearchAction & {
+ 'query-input': string;
};
/**
- * Retrieve the JSON for Blog schema.
+ * Retrieve the Website schema in JSON-LD format.
*
- * @param props - The page data.
- * @returns {Blog} The JSON for Blog schema.
+ * @param {WebSiteData} data - The website data.
+ * @returns {Website} A Website graph.
*/
-export const getBlogSchema = ({
- isSinglePage,
- locale,
- slug,
-}: GetBlogSchemaProps): Blog => {
+export const getWebSiteGraph = ({
+ description,
+ title,
+}: WebSiteData): WebSite => {
+ const searchAction: CustomSearchAction = {
+ '@type': 'SearchAction',
+ query: 'required',
+ 'query-input': 'required name=query',
+ target: `${host}${ROUTES.SEARCH}?s={query}`,
+ };
+
return {
- '@id': `${host}/#blog`,
- '@type': 'Blog',
- author: { '@id': `${host}/#branding` },
- creator: { '@id': `${host}/#branding` },
- editor: { '@id': `${host}/#branding` },
- blogPost: isSinglePage ? { '@id': `${host}/#article` } : undefined,
- inLanguage: locale,
- isPartOf: isSinglePage
- ? {
- '@id': `${host}/${slug}`,
- }
- : undefined,
+ '@type': 'WebSite',
+ '@id': host,
+ potentialAction: searchAction,
+ url: host,
+ author: { '@id': `${host}#${AUTHOR_ID}` },
+ copyrightHolder: { '@id': `${host}#${AUTHOR_ID}` },
+ copyrightYear: Number(CONFIG.copyright.startYear),
+ creator: { '@id': `${host}#${AUTHOR_ID}` },
+ description,
+ editor: { '@id': `${host}#${AUTHOR_ID}` },
+ image: `${host}/icon.svg`,
+ inLanguage: [
+ {
+ '@type': 'Language',
+ name: 'French',
+ alternateName: 'fr',
+ },
+ ],
+ isAccessibleForFree: true,
license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- mainEntityOfPage: isSinglePage ? undefined : { '@id': `${host}/${slug}` },
+ name: title,
+ publisher: { '@id': `${host}#${AUTHOR_ID}` },
+ thumbnailUrl: `${host}/icon.svg`,
};
};
+export type BreadcrumbItemData = {
+ label: string;
+ position: number;
+ slug: string;
+};
+
/**
- * Retrieve the JSON for Comment schema.
+ * Retrieve the BreadcrumbItem schema in JSON-LD format.
*
- * @param props - The comments.
- * @returns {CommentSchema[]} The JSON for Comment schema.
+ * @param {BreadcrumbItemData} data - The item data.
+ * @returns {ListItem} A ListItem graph.
*/
-export const getCommentsSchema = (comments: SingleComment[]): CommentSchema[] =>
- comments.map((comment) => {
- return {
- '@context': 'https://schema.org',
- '@id': `${CONFIG.url}/#comment-${comment.id}`,
- '@type': 'Comment',
- parentItem: comment.parentId
- ? { '@id': `${CONFIG.url}/#comment-${comment.parentId}` }
- : undefined,
- about: { '@type': 'Article', '@id': `${CONFIG.url}/#article` },
- author: {
- '@type': 'Person',
- name: comment.meta.author.name,
- image: comment.meta.author.avatar?.src,
- url: comment.meta.author.website,
- },
- creator: {
- '@type': 'Person',
- name: comment.meta.author.name,
- image: comment.meta.author.avatar?.src,
- url: comment.meta.author.website,
- },
- dateCreated: comment.meta.date,
- datePublished: comment.meta.date,
- text: comment.content,
- };
- });
-
-export type SinglePageSchemaReturn = {
- about: AboutPage;
- contact: ContactPage;
- page: Article;
- post: BlogPosting;
+export const getBreadcrumbItemGraph = ({
+ label,
+ position,
+ slug,
+}: BreadcrumbItemData): ListItem => {
+ return {
+ '@type': 'ListItem',
+ item: {
+ '@id': slug === ROUTES.HOME ? host : `${host}${slug}`,
+ name: label,
+ },
+ position,
+ };
};
-export type SinglePageSchemaKind = keyof SinglePageSchemaReturn;
-
-export type GetSinglePageSchemaProps<T extends SinglePageSchemaKind> = {
+type WebContentsDates = {
/**
- * The number of comments.
+ * A date value in ISO 8601 date format.
*/
- commentsCount?: number;
+ publication?: string;
/**
- * The page content.
+ * A date value in ISO 8601 date format..
*/
- content?: string;
+ update?: string;
+};
+
+type WebContentsData = {
/**
- * The url of the cover.
+ * The year during which the claimed copyright was first asserted.
*/
- cover?: string;
+ copyrightYear?: number;
/**
- * The page dates.
+ * The URL of the creative work cover.
*/
- dates: Dates;
+ cover?: string;
/**
- * The page description.
+ * A description of the contents.
*/
description: string;
/**
- * The page id.
- */
- id: string;
- /**
- * The page kind.
+ * The publication date and maybe the update date.
*/
- kind: T;
+ dates?: WebContentsDates;
/**
- * The page locale.
+ * Approximate time it usually takes to work through the contents.
*/
- locale: string;
+ readingTime?: Duration;
/**
- * The page slug with a leading slash.
+ * The page slug.
*/
slug: string;
/**
- * The page title.
+ * The contents title.
*/
title: string;
};
+export type WebPageData = WebContentsData & {
+ /**
+ * The breadcrumbs schema.
+ */
+ breadcrumb?: BreadcrumbList;
+};
+
/**
- * Retrieve the JSON schema depending on the page kind.
+ * Retrieve the WebPage schema in JSON-LD format.
*
- * @param props - The page data.
- * @returns {SinglePageSchemaReturn[T]} - Either AboutPage, ContactPage, Article or BlogPosting schema.
+ * @param {WebPageData} data - The page data.
+ * @returns {WebPage} A WebPage graph.
*/
-export const getSinglePageSchema = <T extends SinglePageSchemaKind>({
- commentsCount,
- content,
+export const getWebPageGraph = ({
+ breadcrumb,
+ copyrightYear,
cover,
dates,
description,
- id,
- kind,
- locale,
- title,
+ readingTime,
slug,
-}: GetSinglePageSchemaProps<T>): SinglePageSchemaReturn[T] => {
- const publicationDate = new Date(dates.publication);
- const updateDate = dates.update ? new Date(dates.update) : undefined;
- const singlePageSchemaType = {
- about: 'AboutPage',
- contact: 'ContactPage',
- page: 'Article',
- post: 'BlogPosting',
+ title,
+}: WebPageData): WebPage => {
+ return {
+ '@id': `${host}${slug}`,
+ '@type': 'WebPage',
+ author: { '@id': `${host}#${AUTHOR_ID}` },
+ breadcrumb,
+ copyrightHolder: { '@id': `${host}#${AUTHOR_ID}` },
+ copyrightYear,
+ dateCreated: dates?.publication,
+ dateModified: dates?.update,
+ datePublished: dates?.publication,
+ description,
+ editor: { '@id': `${host}#${AUTHOR_ID}` },
+ headline: title,
+ inLanguage: [
+ {
+ '@type': 'Language',
+ name: 'French',
+ alternateName: 'fr',
+ },
+ ],
+ isAccessibleForFree: true,
+ isPartOf: { '@id': host },
+ license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ lastReviewed: dates?.update,
+ name: title,
+ publisher: { '@id': `${host}#${AUTHOR_ID}` },
+ reviewedBy: { '@id': `${host}#${AUTHOR_ID}` },
+ timeRequired: readingTime,
+ thumbnailUrl: cover,
+ url: `${host}${slug}`,
};
+};
+/**
+ * Retrieve the AboutPage schema in JSON-LD format.
+ *
+ * @param {WebPageData} data - The page data.
+ * @returns {AboutPage} A AboutPage graph.
+ */
+export const getAboutPageGraph = (data: WebPageData): AboutPage => {
return {
- '@id': `${host}/#${id}`,
- '@type': singlePageSchemaType[kind],
- name: title,
+ ...getWebPageGraph(data),
+ '@type': 'AboutPage',
+ };
+};
+
+/**
+ * Retrieve the ContactPage schema in JSON-LD format.
+ *
+ * @param {WebPageData} data - The page data.
+ * @returns {ContactPage} A ContactPage graph.
+ */
+export const getContactPageGraph = (data: WebPageData): ContactPage => {
+ return {
+ ...getWebPageGraph(data),
+ '@type': 'ContactPage',
+ };
+};
+
+/**
+ * Retrieve the SearchResultsPage schema in JSON-LD format.
+ *
+ * @param {WebPageData} data - The page data.
+ * @returns {SearchResultsPage} A SearchResultsPage graph.
+ */
+export const getSearchResultsPageGraph = (
+ data: WebPageData
+): SearchResultsPage => {
+ return {
+ ...getWebPageGraph(data),
+ '@type': 'SearchResultsPage',
+ };
+};
+
+export type BlogData = WebContentsData & {
+ posts?: Blog['blogPost'];
+};
+
+/**
+ * Retrieve the Blog schema in JSON-LD format.
+ *
+ * @param {BlogData} data - The blog data.
+ * @returns {Blog} A Blog graph.
+ */
+export const getBlogGraph = ({
+ copyrightYear,
+ cover,
+ dates,
+ description,
+ posts,
+ readingTime,
+ slug,
+ title,
+}: BlogData): Blog => {
+ return {
+ '@type': 'Blog',
+ '@id': `${host}${slug}`,
+ author: { '@id': `${host}#${AUTHOR_ID}` },
+ blogPost: posts,
+ copyrightHolder: { '@id': `${host}#${AUTHOR_ID}` },
+ copyrightYear,
+ dateCreated: dates?.publication,
+ dateModified: dates?.update,
+ datePublished: dates?.publication,
description,
- articleBody: content,
- author: { '@id': `${host}/#branding` },
- commentCount: commentsCount,
- copyrightYear: publicationDate.getFullYear(),
- creator: { '@id': `${host}/#branding` },
- dateCreated: publicationDate.toISOString(),
- dateModified: updateDate?.toISOString(),
- datePublished: publicationDate.toISOString(),
- editor: { '@id': `${host}/#branding` },
+ editor: { '@id': `${host}#${AUTHOR_ID}` },
headline: title,
- image: cover,
- inLanguage: locale,
+ inLanguage: [
+ {
+ '@type': 'Language',
+ name: 'French',
+ alternateName: 'fr',
+ },
+ ],
+ isAccessibleForFree: true,
+ isPartOf: { '@id': host },
license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ name: title,
+ publisher: { '@id': `${host}#${AUTHOR_ID}` },
+ timeRequired: readingTime,
thumbnailUrl: cover,
- isPartOf:
- kind === 'post'
- ? {
- '@id': `${host}/${ROUTES.BLOG}`,
- }
- : undefined,
- mainEntityOfPage: { '@id': `${host}/${slug}` },
- } as SinglePageSchemaReturn[T];
+ url: `${host}${slug}`,
+ };
};
-export type GetWebPageSchemaProps = {
+export type BlogPostingData = WebContentsData & {
/**
- * The page description.
+ * The author of the article.
*/
- description: string;
+ author?: Person;
/**
- * The page locale.
+ * The article body.
*/
- locale: string;
+ body?: string;
/**
- * The page slug.
+ * The comments on this creative work.
*/
- slug: string;
+ comment?: CommentSchema[];
/**
- * The page title.
+ * The number of comments on this creative work.
*/
- title: string;
+ commentCount?: number;
/**
- * The page last update.
+ * A comma separated list of keywords.
*/
- updateDate?: string;
+ keywords?: string;
+ /**
+ * The number of words in the article.
+ */
+ wordCount?: number;
};
/**
- * Retrieve the JSON for WebPage schema.
+ * Retrieve the BlogPosting schema in JSON-LD format.
*
- * @param props - The page data.
- * @returns {WebPage} The JSON for WebPage schema.
+ * @param {BlogPostingData} data - The blog posting data.
+ * @returns {BlogPosting} A BlogPosting graph.
*/
-export const getWebPageSchema = ({
+export const getBlogPostingGraph = ({
+ author,
+ body,
+ comment,
+ commentCount,
+ copyrightYear,
+ cover,
+ dates,
description,
- locale,
+ keywords,
+ readingTime,
slug,
title,
- updateDate,
-}: GetWebPageSchemaProps): WebPage => {
+ wordCount,
+}: BlogPostingData): BlogPosting => {
return {
- '@id': `${host}/${slug}`,
- '@type': 'WebPage',
- breadcrumb: { '@id': `${host}/#breadcrumb` },
- lastReviewed: updateDate,
- name: title,
+ '@type': 'BlogPosting',
+ '@id': `${host}${slug}#${ARTICLE_ID}`,
+ articleBody: body,
+ author: author ?? { '@id': `${host}#${AUTHOR_ID}` },
+ comment,
+ commentCount,
+ copyrightHolder: author ?? { '@id': `${host}#${AUTHOR_ID}` },
+ copyrightYear,
+ dateCreated: dates?.publication,
+ dateModified: dates?.update,
+ datePublished: dates?.publication,
description,
- inLanguage: locale,
- reviewedBy: { '@id': `${host}/#branding` },
- url: `${host}/${slug}`,
- isPartOf: {
- '@id': `${host}`,
- },
+ discussionUrl: comment
+ ? `${host}${slug}#${COMMENTS_SECTION_ID}`
+ : undefined,
+ editor: author ?? { '@id': `${host}#${AUTHOR_ID}` },
+ headline: title,
+ image: cover,
+ inLanguage: [
+ {
+ '@type': 'Language',
+ name: 'French',
+ alternateName: 'fr',
+ },
+ ],
+ isAccessibleForFree: true,
+ isPartOf: { '@id': `${host}${ROUTES.BLOG}#${ARTICLE_ID}` },
+ keywords,
+ license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ mainEntityOfPage: { '@id': `${host}${slug}` },
+ name: title,
+ publisher: { '@id': `${host}#${AUTHOR_ID}` },
+ timeRequired: readingTime,
+ thumbnailUrl: cover,
+ url: `${host}${slug}`,
+ wordCount,
+ };
+};
+
+export type CommentData = {
+ /**
+ * The slug of the commented article.
+ */
+ articleSlug: string;
+ /**
+ * The author of the comment.
+ */
+ author: Person;
+ /**
+ * The comment body.
+ */
+ body: string;
+ /**
+ * The comment id.
+ */
+ id: string;
+ /**
+ * The id of the parent.
+ */
+ parentId?: string;
+ /**
+ * A date value in ISO 8601 date format.
+ */
+ publishedAt: string;
+};
+
+/**
+ * Retrieve the Comment schema in JSON-LD format.
+ *
+ * @param {CommentData} data - The comment data.
+ * @returns {CommentSchema} A Comment graph.
+ */
+export const getCommentGraph = ({
+ articleSlug,
+ author,
+ body,
+ id,
+ parentId,
+ publishedAt,
+}: CommentData): CommentSchema => {
+ return {
+ '@id': `${host}${articleSlug}#${COMMENT_ID_PREFIX}${id}`,
+ '@type': 'Comment',
+ about: { '@id': `${host}/${articleSlug}#${ARTICLE_ID}` },
+ author,
+ creator: author,
+ dateCreated: publishedAt,
+ datePublished: publishedAt,
+ parentItem: parentId
+ ? { '@id': `${host}${articleSlug}#${COMMENT_ID_PREFIX}${parentId}` }
+ : { '@id': `${host}/${articleSlug}#${ARTICLE_ID}` },
+ text: body,
};
};
-export const getSchemaJson = (graphs: Graph['@graph']): Graph => {
+/**
+ * Retrieve a schema in JSON-LD format from the given graphs.
+ *
+ * @param {Graph['@graph']} graphs - The schema graphs.
+ * @returns {CommentSchema} The schema in JSON-LD format.
+ */
+export const getSchemaFrom = (graphs: Graph['@graph']): Graph => {
return {
'@context': 'https://schema.org',
- '@graph': graphs,
+ '@graph': [getAuthorGraph(), ...graphs],
};
};