presentation/renderers/page.renderer.ts

presentation/renderers/page.renderer.ts is a file in Bane's Lab Site. 196 lines of code and 38 definitions.

import { ANATOMY_PAGE, HOME_PAGE, ONTOLOGY_PAGE } from "#core/ids/page.ids";
import { MISSING_PAGE_MESSAGE, MISSING_PAGE_TITLE, SITE_NAME, TITLE_SEPARATOR } from "#configuration/strings/page.strings";
import {
    OPEN_GRAPH_ARTICLE,
    OPEN_GRAPH_WEBSITE,
    ROBOTS_INDEX,
    ROBOTS_NOINDEX,
} from "#configuration/constants/document.constants";
import type {
    PageDates,
    PageDefinition,
    PageHead,
    RecordSubject,
    SectionSubject,
    SourceSubject,
} from "#types/page.types";
import { SITE_URL, markdownTwinPath, pagePath, payloadTwinPath, tabFromPath, tabLink } from "#core/assets/link.assets";
import { recordSchemaOf, schemaOf, sectionSchemaOf, sourceSchemaOf } from "#presentation/renderers/schema.renderer";
import { HOME_THESIS } from "#configuration/strings/home.strings";
import { MISSING_SHARE } from "#configuration/strings/report.strings";
import { SHARE_IMAGES } from "#core/generated/share.generated";
import type { ShareImage } from "#types/card.types";
import type { Tab } from "#types/document.types";
import { summarizeMarkup } from "#core/converters/markup.converter";

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 NONCE_SLOT = 'meta[property="csp-nonce"]';
const CONTENT_ATTRIBUTE = "content";
const MARKDOWN_ALTERNATE_SLOT = 'link[rel="alternate"][type="text/markdown"]';
const JSON_ALTERNATE_SLOT = 'link[rel="alternate"][type="application/json"]';
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 shareOf = function shareOf(page: string): ShareImage {
    const share = SHARE_IMAGES.get(page);
    if (share === undefined) {
        throw new Error(MISSING_SHARE + page);
    }
    return share;
};

const pageHead = function pageHead(definition: PageDefinition, dates?: PageDates): PageHead {
    const title =
        definition.id === HOME_PAGE
            ? SITE_NAME + TITLE_SEPARATOR + HOME_THESIS
            : definition.title + TITLE_SEPARATOR + SITE_NAME;
    const banner = shareOf(definition.id);
    const subject = {
        banner,
        ...(dates === undefined ? {} : { dates }),
        definition,
        description: definition.description,
        path: pagePath(definition.id),
        title,
    };
    const kind = definition.id === HOME_PAGE ? OPEN_GRAPH_WEBSITE : OPEN_GRAPH_ARTICLE;
    return { ...subject, kind, robots: ROBOTS_INDEX, schema: schemaOf(subject) };
};

export const tabHead = function tabHead(definition: PageDefinition, tab: Tab, dates?: PageDates): PageHead {
    const subject = {
        banner: shareOf(definition.id),
        ...(dates === undefined ? {} : { dates }),
        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: subject.banner,
        description: subject.description,
        kind: OPEN_GRAPH_ARTICLE,
        path: subject.path,
        robots: ROBOTS_INDEX,
        schema: schemaOf(subject),
        title: subject.title,
    };
};

export const sourceHead = function sourceHead(subject: SourceSubject): PageHead {
    return {
        alternates: subject.alternates,
        banner: shareOf(ANATOMY_PAGE),
        description: subject.description,
        kind: OPEN_GRAPH_ARTICLE,
        path: subject.path,
        robots: ROBOTS_INDEX,
        schema: sourceSchemaOf(subject),
        title: subject.title,
    };
};

export const sectionHead = function sectionHead(subject: SectionSubject): PageHead {
    return {
        alternates: subject.alternates,
        banner: shareOf(subject.seed.page),
        description: subject.description,
        kind: OPEN_GRAPH_ARTICLE,
        path: subject.path,
        robots: ROBOTS_INDEX,
        schema: sectionSchemaOf(subject),
        title: subject.title,
    };
};

export const recordHead = function recordHead(subject: RecordSubject): PageHead {
    return {
        alternates: subject.alternates,
        banner: shareOf(ONTOLOGY_PAGE),
        description: subject.description,
        kind: OPEN_GRAPH_ARTICLE,
        path: subject.path,
        robots: ROBOTS_INDEX,
        schema: recordSchemaOf(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),
    dates?: PageDates,
): PageHead {
    if (definition === undefined) {
        const subject = {
            description: MISSING_PAGE_MESSAGE,
            path: pagePath(page),
            title: MISSING_PAGE_TITLE + TITLE_SEPARATOR + SITE_NAME,
        };
        return {
            ...subject,
            banner: shareOf(HOME_PAGE),
            kind: OPEN_GRAPH_WEBSITE,
            robots: ROBOTS_NOINDEX,
            schema: schemaOf(subject),
        };
    }
    const tab = secondaryTab(definition, pathname);
    return tab === undefined ? pageHead(definition, dates) : tabHead(definition, tab, dates);
};

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 holdsHead = function holdsHead(root: Document, path: string): boolean {
    return root.querySelector(`${CANONICAL_SLOT}[href="${SITE_URL + path}"]`) !== null;
};

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);
    root.querySelector(NONCE_SLOT)?.setAttribute(CONTENT_ATTRIBUTE, "");
    const alternates: readonly (readonly [string, string])[] = [
        [MARKDOWN_ALTERNATE_SLOT, head.alternates?.markdown ?? markdownTwinPath(head.path)],
        [JSON_ALTERNATE_SLOT, head.alternates?.json ?? payloadTwinPath(head.path)],
    ];
    for (const [selector, path] of alternates) {
        const link = root.querySelector(selector);
        if (head.robots === ROBOTS_INDEX) {
            link?.setAttribute("href", SITE_URL + path);
        } else {
            link?.removeAttribute("href");
        }
    }
    const slot = root.querySelector(SCHEMA_SLOT);
    if (slot !== null) {
        slot.textContent = JSON.stringify(head.schema, null, SCHEMA_INDENT);
    }
};