schema.mjs
137 lines and 15 functions in DrDustinEdwards/prelum, at commit 547db6a on 2026-10-25. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| jsonLd | 15 to 17 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| thing | 27 to 29 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| graph | 37 to 42 | 1 | 20% | OkCRAP 1.5 | No data: mutation testing has not run on this repo yet |
| <anonymous:40> | 40 to 40 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:40> | 40 to 40 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| website | 50 to 61 | 2 | 8% | WatchCRAP 5.1 | No data: mutation testing has not run on this repo yet |
| article | 82 to 96 | 3 | 100% | OkCRAP 3 | No data: mutation testing has not run on this repo yet |
| breadcrumbs | 102 to 111 | 1 | 25% | OkCRAP 1.4 | No data: mutation testing has not run on this repo yet |
| <anonymous:104> | 104 to 109 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| itemList | 118 to 129 | 1 | 17% | OkCRAP 1.6 | No data: mutation testing has not run on this repo yet |
| <anonymous:122> | 122 to 127 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| faqPage | 132 to 140 | 1 | 25% | OkCRAP 1.4 | No data: mutation testing has not run on this repo yet |
| <anonymous:134> | 134 to 138 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| compact | 143 to 149 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:146> | 146 to 146 | 5 | 100% | OkCRAP 5 | No data: mutation testing has not run on this repo yet |
Imports
It imports head.mjs. No module of this repo imports it.
Source
import { absoluteUrl, articleDates } from "./head.mjs";
const CONTEXT = "https://schema.org";
/**
* @typedef {Record<string, unknown>} SchemaNode
*/
/**
* The descriptor React Router's `meta` serializes into a `<script type="application/ld+json">`; `headHtml` writes the same.
*
* @param {SchemaNode} node
* @returns {{ "script:ld+json": SchemaNode }}
*/
export function jsonLd(node) {
return { "script:ld+json": node };
}
/**
* Any schema.org type, with the context added and every empty field dropped: `undefined`, `null`, `""` and `[]` would each
* claim a value the page does not have. Person, Organization, Book, Event and the rest are this, with the site's own fields.
*
* @param {string} type
* @param {SchemaNode} fields
* @returns {SchemaNode}
*/
export function thing(type, fields) {
return { "@context": CONTEXT, "@type": type, ...compact(fields) };
}
/**
* Several nodes as one `@graph`, joined by their `@id`s, with the context written once.
*
* @param {SchemaNode[]} nodes
* @returns {SchemaNode}
*/
export function graph(nodes) {
return {
"@context": CONTEXT,
"@graph": nodes.map((node) => Object.fromEntries(Object.entries(node).filter(([key]) => key !== "@context"))),
};
}
/**
* The site itself. There is no SearchAction unless `searchUrl` is given: one is a promise that the template is a working search.
*
* @param {{ origin: string, name: string, description?: string, searchUrl?: string, publisher?: SchemaNode }} site
* `searchUrl` holds `{search_term_string}` where the query goes
*/
export function website({ origin, name, description, searchUrl, publisher }) {
return thing("WebSite", {
"@id": `${origin}/#website`,
url: origin,
name,
description,
publisher,
potentialAction: searchUrl
? { "@type": "SearchAction", target: searchUrl, "query-input": "required name=search_term_string" }
: undefined,
});
}
/**
* @typedef {object} ArticleInput
* @property {string} url
* @property {string} headline
* @property {string | null} [description]
* @property {string | string[]} [image]
* @property {Date | number | string | null} [published]
* @property {Date | number | string | null} [modified]
* @property {string[]} [tags]
* @property {SchemaNode} [author]
* @property {SchemaNode} [publisher]
* @property {string} [type] `Article` unless a subtype fits better (`ScholarlyArticle`, `TechArticle`, `BlogPosting`)
*/
/**
* An article, with its dates from the same `articleDates` the Open Graph tags use.
*
* @param {ArticleInput} article
*/
export function article({ url, headline, description, image, published, modified, tags, author, publisher, type = "Article" }) {
const dates = articleDates({ published, modified });
return thing(type, {
headline,
description,
image,
datePublished: dates.published,
dateModified: dates.modified,
keywords: tags && tags.length > 0 ? tags.join(", ") : undefined,
mainEntityOfPage: { "@type": "WebPage", "@id": url },
url,
author,
publisher,
});
}
/**
* @param {string} origin
* @param {Array<{ name: string, path: string }>} trail from the home page down
*/
export function breadcrumbs(origin, trail) {
return thing("BreadcrumbList", {
itemListElement: trail.map((crumb, i) => ({
"@type": "ListItem",
position: i + 1,
name: crumb.name,
item: absoluteUrl(origin, crumb.path),
})),
});
}
/**
* @param {string} origin
* @param {string} name
* @param {Array<{ name: string, path: string }>} items
*/
export function itemList(origin, name, items) {
return thing("ItemList", {
name,
numberOfItems: items.length,
itemListElement: items.map((item, i) => ({
"@type": "ListItem",
position: i + 1,
name: item.name,
url: absoluteUrl(origin, item.path),
})),
});
}
/** @param {Array<{ question: string, answer: string }>} faqs */
export function faqPage(faqs) {
return thing("FAQPage", {
mainEntity: faqs.map((faq) => ({
"@type": "Question",
name: faq.question,
acceptedAnswer: { "@type": "Answer", text: faq.answer },
})),
});
}
/** @param {SchemaNode} fields */
function compact(fields) {
return Object.fromEntries(
Object.entries(fields).filter(
([, value]) => value !== undefined && value !== null && value !== "" && !(Array.isArray(value) && value.length === 0),
),
);
}