export { renderMetadata as render }; export interface Meta { /** Required for all pages. `{content}` */ title: string; /** Recommended for all pages. `` */ description?: string | null; /** Automatically added for static renders from the 'pages' folders. */ canonical?: string | null; /** Add ``. Object keys are interpretted as * mime types if they contain a slash, otherwise seen as an alternative language. */ alternates?: Alternate[] | Record; /** Automatically generate both OpenGraph and Twitter meta tags */ embed?: AutoEmbed | null; /** Add a robots tag for `noindex` and `nofollow` */ denyRobots?: boolean | null; /** Add 'og:*' meta tags */ openGraph?: OpenGraph | null; /** Add 'twitter:*' meta tags */ twitter?: Twitter | null; /** * Refer to an oEmbed file. See https://oembed.com * TODO: support passing the oEmbed file directly here. */ oEmbed?: string; /** * '#sitegen/meta' intentionally excludes a lot of exotic tags. * Add these manually using JSX syntax: * * extra: [ * , * ], */ extra?: render.Node; /** Adds `` */ authors?: string[]; /** Credit sitegen by setting this to true aka "clover sitegen 3; paperclover.net/sitegen" */ generator?: string | true | null; /** Adds `` */ keywords?: string[]; /** URL to a manifest; https://developer.mozilla.org/en-US/docs/Web/Manifest */ manifest?: string | null; /** Adds `` */ publisher?: string | null; /** https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/meta/name/referrer */ referrer?: Referrer | null; /** Adds `` */ themeColor?: string | { dark: string; light: string } | null; /** Defaults to `width=device-width, initial-scale=1.0` for mobile compatibility. */ viewport?: string; } export type Alternate = { type: string; url: string } | { lang: string; url: string; }; export interface AutoEmbed { /* Defaults to the page title. */ title?: string | null; /* Defaults to the page description. */ description?: string | null; /* Provide to add an embed image. */ thumbnail?: string | null; /** @default "banner", which applies twitter:card = "summary_large_image" */ thumbnailSize?: "banner" | "icon"; /* Ignored if not passed */ siteTitle?: string | null; } /** See https://ogp.me for extra rules. */ export interface OpenGraph { /** The title of your object as it should appear within the graph */ title?: string; /** A one to two sentence description of your object. */ description?: string | null; /** The type of your object, e.g., "video.movie". Depending on the type you specify, other properties may also be required */ type?: string; /** An image URL which should represent your object within the graph */ image?: OpenGraphField; /** The canonical URL of your object that will be used as its permanent ID in the graph, e.g., "https://www.imdb.com/title/tt0117500/" */ url?: string; /** A URL to an audio file to accompany this object */ audio?: OpenGraphField; /** The word that appears before this object's title in a sentence. An enum of (a, an, the, "", auto). If auto is chosen, the consumer of your data should choose between "a" or "an". Default is "" (blank) */ determiner?: string; /** The locale these tags are marked up in. Of the format language_TERRITORY. Default is en_US */ locale?: string; /** An array of other locales this page is available in */ "locale:alternate"?: string[]; /** If your object is part of a larger web site, the name which should be displayed for the overall site. e.g., "IMDb" */ site_name?: string; /** A URL to a video file that complements this object */ video?: OpenGraphField; [field: string]: OpenGraphField; } /** * When passing an array, the property is duplicated. * When passing an object, the fields are emitted as namespaced with ':'. */ type OpenGraphField = | string | { [field: string]: OpenGraphField } | Array | (null | undefined); /** Twitter uses various OpenGraph fields if these are not specified. */ export interface Twitter { card: string; title?: string; description?: string | null; url?: string; image?: string; player?: string; /** Same logic as Open Graph */ [field: string]: OpenGraphField; } export interface Template extends Omit { base: URL; titleTemplate?: (title: string) => string; } export interface Alternates { canonical: string; types: { [mime: string]: AlternateType }; } export interface AlternateType { url: string; title: string; } export type Referrer = | "no-referrer" | "origin" | "no-referrer-when-downgrade" | "origin-when-cross-origin" | "same-origin" | "strict-origin" | "strict-origin-when-cross-origin"; /* Convert a metadata definition into text. */ function renderMetadata(meta: Meta, template: Template): string { const { titleTemplate, base } = template; const resolve = (str: string) => new URL(str, base).href; const title = titleTemplate ? titleTemplate(meta.title) : meta.title; const description = meta.description ?? null; const canonical = meta.canonical ? resolve(meta.canonical) : null; const denyRobots = Boolean(meta.denyRobots || template.denyRobots); const authors = meta.authors ?? template.authors ?? null; let generator = meta.generator ?? template.generator ?? null; const keywords = meta.keywords ?? template.keywords ?? null; const manifest = meta.manifest ?? template.manifest ?? null; const publisher = meta.publisher ?? template.publisher ?? null; const referrer = meta.referrer ?? template.referrer ?? null; const themeColor = meta.themeColor ?? template.themeColor ?? null; const viewport = meta.viewport ?? template.viewport ?? "width=device-width, initial-scale=1.0"; const embed = meta.embed ?? template.embed ?? null; let openGraph = meta.openGraph ?? template.openGraph ?? null; let twitter = meta.twitter ?? template.twitter ?? null; if (embed) { const { thumbnail, thumbnailSize, siteTitle } = embed; openGraph = { type: "website", title: embed.title ?? title, description: embed.description ?? description, ...openGraph, }; twitter = { card: (thumbnailSize ?? (thumbnail ? "banner" : "icon")) === "banner" ? "summary_large_image" : "summary", ...twitter, }; if (thumbnail) { openGraph.image = embed.thumbnail; } if (siteTitle) { openGraph.site_name = siteTitle; } if (canonical) { openGraph.url = canonical; } } let out = `${esc(title)}`; if (description) { out += ``; } for (const author of authors ?? []) { out += ``; } if (keywords) { out += ``; } if (generator) { if (generator === true) { generator = "clover sitegen 3; paperclover.net/sitegen"; } out += ``; } if (publisher) { out += ``; } if (referrer) { out += ``; } if (themeColor) { if (typeof themeColor === "string") { out += ``; } else { out += '`; out += '`; } } if (denyRobots) { out += ``; } if (description) { out += ``; } if (canonical) { out += ``; } if (manifest) { out += ``; } if (openGraph) out += renderOpenGraph("og:", openGraph); if (twitter) out += renderOpenGraph("twitter:", twitter); if (meta.extra) out += render.sync(meta.extra); if (template.extra) out += render.sync(template.extra); return out; } function renderOpenGraph(prefix: string, value: OpenGraphField): string { if (!value) return ""; if (typeof value === "string") { return ``; } if (Array.isArray(value)) { return value .map((item) => renderOpenGraph(prefix, item)) .join(""); } return Object.entries(value) .map(([key, item]) => renderOpenGraph(`${prefix}:${key}`, item)) .join(""); } function attr(value: string) { return render.quoteIfNeeded(esc(value)); } import { escapeHtmlContent as esc } from "#engine/render"; import * as render from "#engine/render";