diff options
Diffstat (limited to 'src/utils/helpers/schema-org.ts')
| -rw-r--r-- | src/utils/helpers/schema-org.ts | 561 |
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], }; }; |
