import { COPYRIGHT_PREFIX, COPYRIGHT_SUFFIX } from "#configuration/strings/company.strings"; import { MISSING_PAGE_LINK, MISSING_PAGE_MESSAGE, MISSING_PAGE_TITLE, SITE_NAME, SITE_TAGLINE, TITLE_SEPARATOR, } from "#configuration/strings/page.strings"; import { OPEN_GRAPH_ARTICLE, OPEN_GRAPH_WEBSITE, ROBOTS_INDEX, ROBOTS_NOINDEX, STATIC_RENDER_ATTRIBUTE, } from "#configuration/constants/document.constants"; import type { PageDefinition, PageHead, ShareImage } from "#types/page.types"; import { SITE_URL, pagePath, tabFromPath, tabLink } from "#assets/link.assets"; import { getPage, loadedPage } from "#domain/registries/page.registry"; import { HOME_PAGE } from "#core/ids/page.ids"; import { SITE_BANNER } from "#assets/image.assets"; import type { Tab } from "#types/document.types"; import { createElement } from "#core/factories/element.factory"; import { createPageLink } from "#presentation/components/link.component"; import { schemaOf } from "#presentation/renderers/schema.renderer"; import { summarizeMarkup } from "#core/converters/markup.converter"; const SITE_SHARE: ShareImage = { alt: SITE_NAME, source: SITE_BANNER }; const TITLE_SLOTS = ['meta[property="og:title"]', 'meta[name="twitter:title"]']; const IMAGE_SLOTS = ['meta[property="og:image"]', 'meta[name="twitter:image"]']; const IMAGE_ALT_SLOTS = ['meta[property="og:image:alt"]', 'meta[name="twitter:image:alt"]']; const DESCRIPTION_SLOTS = [ 'meta[name="description"]', 'meta[property="og:description"]', 'meta[name="twitter:description"]', ]; const URL_SLOTS = ['meta[property="og:url"]']; const KIND_SLOTS = ['meta[property="og:type"]']; const ROBOTS_SLOTS = ['meta[name="robots"]']; const CANONICAL_SLOT = 'link[rel="canonical"]'; const SCHEMA_SLOT = 'script[type="application/ld+json"]'; const SCHEMA_INDENT = 2; const tabText = function tabText(tab: Tab): string { const [section] = tab.sections; return section?.intro ?? section?.subsections[0]?.content ?? ""; }; const missingPage = function missingPage(): Element { return createElement("div", { children: [ createElement("h1", { text: MISSING_PAGE_TITLE }), createElement("p", { text: MISSING_PAGE_MESSAGE }), createPageLink(HOME_PAGE, "footer-link", [MISSING_PAGE_LINK]), ], className: "missing-page", }); }; const siteFooter = function siteFooter(): Element { const year = String(new Date().getFullYear()); return createElement("footer", { className: "site-footer", text: COPYRIGHT_PREFIX + year + COPYRIGHT_SUFFIX }); }; export const renderPage = function renderPage(page: string): readonly Element[] { const definition = loadedPage(page); const content = definition === undefined ? missingPage() : definition.render(); return [createElement("div", { children: [content], className: "page-content" }), siteFooter()]; }; export const renderStaticPage = function renderStaticPage(page: string): readonly Element[] { document.documentElement.setAttribute(STATIC_RENDER_ATTRIBUTE, ""); try { return renderPage(page); } finally { document.documentElement.removeAttribute(STATIC_RENDER_ATTRIBUTE); } }; const shareOf = function shareOf(page: string): ShareImage { return getPage(page)?.banner ?? SITE_SHARE; }; const pageHead = function pageHead(definition: PageDefinition): PageHead { const title = definition.id === HOME_PAGE ? SITE_NAME + TITLE_SEPARATOR + SITE_TAGLINE : definition.title + TITLE_SEPARATOR + SITE_NAME; const subject = { definition, description: definition.description, path: pagePath(definition.id), title }; const kind = definition.id === HOME_PAGE ? OPEN_GRAPH_WEBSITE : OPEN_GRAPH_ARTICLE; return { ...subject, banner: shareOf(definition.id), kind, robots: ROBOTS_INDEX, schema: schemaOf(subject) }; }; export const tabHead = function tabHead(definition: PageDefinition, tab: Tab): PageHead { const subject = { definition, description: summarizeMarkup(tabText(tab), definition.description), path: tabLink(definition.id, tab.id), tab, title: tab.label + TITLE_SEPARATOR + definition.title + TITLE_SEPARATOR + SITE_NAME, }; return { banner: shareOf(definition.id), description: subject.description, kind: OPEN_GRAPH_ARTICLE, path: subject.path, robots: ROBOTS_INDEX, schema: schemaOf(subject), title: subject.title, }; }; export const secondaryTab = function secondaryTab(definition: PageDefinition, pathname: string): Tab | undefined { if (definition.content.kind !== "tabbed") { return undefined; } const requested = tabFromPath(pathname); const [first] = definition.content.tabs; if (requested === null || requested === first?.id) { return undefined; } return definition.content.tabs.find((tab) => tab.id === requested); }; export const headOf = function headOf(page: string, definition?: PageDefinition, pathname = pagePath(page)): PageHead { if (definition === undefined) { const subject = { description: MISSING_PAGE_MESSAGE, path: pagePath(page), title: MISSING_PAGE_TITLE + TITLE_SEPARATOR + SITE_NAME, }; return { ...subject, banner: SITE_SHARE, kind: OPEN_GRAPH_WEBSITE, robots: ROBOTS_NOINDEX, schema: schemaOf(subject), }; } const tab = secondaryTab(definition, pathname); return tab === undefined ? pageHead(definition) : tabHead(definition, tab); }; const fill = function fill(root: Document, selectors: readonly string[], value: string): void { for (const selector of selectors) { root.querySelector(selector)?.setAttribute("content", value); } }; export const applyHead = function applyHead(root: Document, head: PageHead): void { root.title = head.title; fill(root, TITLE_SLOTS, head.title); fill(root, DESCRIPTION_SLOTS, head.description); fill(root, URL_SLOTS, SITE_URL + head.path); fill(root, IMAGE_SLOTS, SITE_URL + head.banner.source); fill(root, IMAGE_ALT_SLOTS, head.banner.alt); fill(root, KIND_SLOTS, head.kind); fill(root, ROBOTS_SLOTS, head.robots); root.querySelector(CANONICAL_SLOT)?.setAttribute("href", SITE_URL + head.path); const slot = root.querySelector(SCHEMA_SLOT); if (slot !== null) { slot.textContent = JSON.stringify(head.schema, null, SCHEMA_INDENT); } };