# presentation/renderers/page.renderer.ts

> 171 lines of code and 37 definitions.

Tree: Site tree
Language: typescript
Layer: product
Canonical: https://banes-lab.com/anatomy/tree#file-presentation-renderers-page-renderer-ts
Source text: https://banes-lab.com/assets/sources/source.992927d6f2f3cd85f8e6d169fecc046945cc4ed34188a43531663c11233a70da.generated.txt

## Definitions

- `fill` (lexical_declaration, line 150)
- `shareOf` (lexical_declaration, line 82)
- `applyHead` (lexical_declaration, line 156, exported)
- `headOf` (lexical_declaration, line 131, exported)
- `renderPage` (lexical_declaration, line 65, exported)
- `pageHead` (lexical_declaration, line 90)
- `tabHead` (lexical_declaration, line 100, exported)
- `missingPage` (lexical_declaration, line 54)
- `renderStaticPage` (lexical_declaration, line 73, exported)
- `TITLE_SLOTS` (lexical_declaration, line 33)
- `IMAGE_SLOTS` (lexical_declaration, line 34)
- `IMAGE_ALT_SLOTS` (lexical_declaration, line 35)
- `DESCRIPTION_SLOTS` (lexical_declaration, line 36)
- `URL_SLOTS` (lexical_declaration, line 41)
- `KIND_SLOTS` (lexical_declaration, line 42)
- `ROBOTS_SLOTS` (lexical_declaration, line 43)
- `CANONICAL_SLOT` (lexical_declaration, line 44)
- `MARKDOWN_ALTERNATE_SLOT` (lexical_declaration, line 45)
- `JSON_ALTERNATE_SLOT` (lexical_declaration, line 46)
- `SCHEMA_SLOT` (lexical_declaration, line 47)
- `SCHEMA_INDENT` (lexical_declaration, line 48)
- `tabText` (lexical_declaration, line 49)
- `[section]` (lexical_declaration, line 50)
- `definition` (lexical_declaration, line 66, exported)
- `content` (lexical_declaration, line 67, exported)
- `shell` (lexical_declaration, line 68, exported)
- `share` (lexical_declaration, line 83)
- `title` (lexical_declaration, line 91)
- `kind` (lexical_declaration, line 96)
- `secondaryTab` (lexical_declaration, line 119, exported)
- `requested` (lexical_declaration, line 123, exported)
- `[first]` (lexical_declaration, line 124, exported)
- `subject` (lexical_declaration, line 133, exported)
- `tab` (lexical_declaration, line 146, exported)
- `alternates` (lexical_declaration, line 166, exported)
- `link` (lexical_declaration, line 171, exported)
- `slot` (lexical_declaration, line 178, exported)

## Uses

- [core/assets/link.assets.ts](https://banes-lab.com/source/tree/core/assets/link.assets.ts.md)
- [core/converters/text.converter.ts](https://banes-lab.com/source/tree/core/converters/text.converter.ts.md)
- [core/factories/element.factory.ts](https://banes-lab.com/source/tree/core/factories/element.factory.ts.md)
- [presentation/components/clipboard.component.ts](https://banes-lab.com/source/tree/presentation/components/clipboard.component.ts.md)
- [presentation/components/link.component.ts](https://banes-lab.com/source/tree/presentation/components/link.component.ts.md)
- [presentation/renderers/schema.renderer.ts](https://banes-lab.com/source/tree/presentation/renderers/schema.renderer.ts.md)
- [presentation/renderers/site.renderer.ts](https://banes-lab.com/source/tree/presentation/renderers/site.renderer.ts.md)

## Used by

- [presentation/widgets/tab.widget.ts](https://banes-lab.com/source/tree/presentation/widgets/tab.widget.ts.md)
- [runtime/coordinators/route.coordinator.ts](https://banes-lab.com/source/tree/runtime/coordinators/route.coordinator.ts.md)

## Source

```typescript
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 } from "#types/page.types";
import { SITE_URL, markdownTwinPath, pagePath, payloadTwinPath, tabFromPath, tabLink } from "#assets/link.assets";
import { COPY_PAGE_TITLE } from "#configuration/strings/clipboard.strings";
import { HOME_PAGE } from "#core/ids/page.ids";
import { MISSING_SHARE } from "#configuration/strings/report.strings";
import { SHARE_IMAGES } from "#assets/share.generated";
import type { ShareImage } from "#types/card.types";
import type { Tab } from "#types/document.types";
import { createCopyButton } from "#presentation/components/clipboard.component";
import { createElement } from "#core/factories/element.factory";
import { createPageLink } from "#presentation/components/link.component";
import { exportText } from "#core/converters/text.converter";
import { loadedPage } from "#domain/registries/page.registry";
import { renderSiteFooter } from "#presentation/renderers/site.renderer";
import { schemaOf } from "#presentation/renderers/schema.renderer";
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 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 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",
    });
};

export const renderPage = function renderPage(page: string): readonly Element[] {
    const definition = loadedPage(page);
    const content = definition === undefined ? missingPage() : definition.render();
    const shell = createElement("div", { children: [content], className: "page-content" });
    shell.append(createCopyButton(() => exportText(shell), "copy-button large copy-all", COPY_PAGE_TITLE));
    return [shell, renderSiteFooter(definition?.license)];
};

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 {
    const share = SHARE_IMAGES.get(page);
    if (share === undefined) {
        throw new Error(MISSING_SHARE + page);
    }
    return 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: shareOf(HOME_PAGE),
            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 alternates: readonly (readonly [string, string])[] = [
        [MARKDOWN_ALTERNATE_SLOT, markdownTwinPath(head.path)],
        [JSON_ALTERNATE_SLOT, 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);
    }
};
```
