# core/formatters/site.formatter.ts

> 181 lines of code and 37 definitions.

Tree: Bane's Lab Build Scripts
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/build-scripts#file-build-scripts-core-formatters-site-formatter-ts
Source text: https://banes-lab.com/source/build-scripts/core/formatters/site.formatter.ts.txt

Listed in [core/formatters](https://banes-lab.com/api/source/build-scripts/core/formatters.md), after [core/formatters/server.formatter.ts](https://banes-lab.com/source/build-scripts/core/formatters/server.formatter.ts.md) and before [core/formatters/source.formatter.ts](https://banes-lab.com/source/build-scripts/core/formatters/source.formatter.ts.md).

## Definitions

- `renderRoutePayload` (lexical_declaration, line 71, exported)
- `renderLlms` (lexical_declaration, line 158, exported)
- `urlEntry` (lexical_declaration, line 75)
- `renderSitemapParts` (lexical_declaration, line 106, exported)
- `renderRobots` (lexical_declaration, line 137, exported)
- `escapeXml` (lexical_declaration, line 46, exported)
- `renderPayload` (lexical_declaration, line 58, exported)
- `renderSitemap` (lexical_declaration, line 80, exported)
- `renderSitemapIndex` (lexical_declaration, line 91, exported)
- `renderUrlSet` (lexical_declaration, line 102, exported)
- `SLASH` (lexical_declaration, line 30)
- `JSON_INDENT` (lexical_declaration, line 31)
- `SITEMAP_OPEN` (lexical_declaration, line 32)
- `SITEMAP_CLOSE` (lexical_declaration, line 34)
- `SITEMAP_INDEX_OPEN` (lexical_declaration, line 35)
- `SITEMAP_INDEX_CLOSE` (lexical_declaration, line 37)
- `XML_ESCAPES` (lexical_declaration, line 38)
- `out` (lexical_declaration, line 47, exported)
- `isoDate` (lexical_declaration, line 54, exported)
- `payload` (lexical_declaration, line 59, exported)
- `stamp` (lexical_declaration, line 82, exported)
- `entries` (lexical_declaration, line 95, exported)
- `lastmod` (lexical_declaration, line 96, exported)
- `count` (lexical_declaration, line 110, exported)
- `listed` (lexical_declaration, line 112, exported)
- `body` (lexical_declaration, line 113, exported)
- `partRoute` (lexical_declaration, line 114, exported)
- `bytes` (lexical_declaration, line 115, exported)
- `DAY_MILLISECONDS` (lexical_declaration, line 123)
- `renderSecurityTxt` (lexical_declaration, line 125, exported)
- `expires` (lexical_declaration, line 126, exported)
- `{ author, consent, site }` (lexical_declaration, line 138, exported)
- `agents` (lexical_declaration, line 139, exported)
- `search` (lexical_declaration, line 140, exported)
- `links` (lexical_declaration, line 159, exported)
- `texts` (lexical_declaration, line 162, exported)
- `payloads` (lexical_declaration, line 163, exported)

## Contained in

- [core/formatters](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-formatters.md)

## Uses

- [configuration/strings/page.strings.ts](https://banes-lab.com/source/build-scripts/configuration/strings/page.strings.ts.md)
- [configuration/strings/site.strings.ts](https://banes-lab.com/source/build-scripts/configuration/strings/site.strings.ts.md)
- [core/selectors/site.selector.ts](https://banes-lab.com/source/build-scripts/core/selectors/site.selector.ts.md)

## Used by

- [core/converters/route.converter.ts](https://banes-lab.com/source/build-scripts/core/converters/route.converter.ts.md)
- [core/coordinators/payload.coordinator.ts](https://banes-lab.com/source/build-scripts/core/coordinators/payload.coordinator.ts.md)
- [core/persistence/page.persistence.ts](https://banes-lab.com/source/build-scripts/core/persistence/page.persistence.ts.md)

## Linked from

- [configuration/strings](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-configuration-strings.md)
- [core/converters](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-converters.md)
- [core/coordinators](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-coordinators.md)
- [core/formatters](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-formatters.md)
- [core/persistence](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-persistence.md)
- [core/selectors](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-selectors.md)

## Source

```typescript
import {
    AI_AGENTS,
    FULL_TEXT_ROUTE,
    SEARCH_AGENTS,
    SEARCH_CRAWL_RULES,
    SECURITY_KEY_FILE,
    SECURITY_TXT_DAYS,
    SECURITY_TXT_FIELDS,
    SECURITY_TXT_FILE,
    SECURITY_TXT_LANGUAGES,
    SITEMAP_BYTE_CAP,
    SITEMAP_URL_CAP,
} from "#configuration/constants/site.constants";
import type { DiscoveredRoute, Discovery, SitemapPart } from "#types/site.types";
import {
    LLMS_FULL_TEXT_LABEL,
    LLMS_JSON_HEADING,
    LLMS_JSON_INTRO,
    LLMS_MARKDOWN_HEADING,
    LLMS_MARKDOWN_INTRO,
    LLMS_PAGES_HEADING,
    LLMS_USE_HEADING,
    trainingConsent,
} from "#configuration/strings/site.strings";
import { fullTextParts, routeContentOf } from "#core/selectors/site.selector";
import { jsonPath, markdownPathOf, sitemapPartRoute } from "#core/resolvers/route.resolver";
import { oversizeSitemap, unstampedRoute } from "#configuration/strings/page.strings";
import { Buffer } from "node:buffer";

const SLASH = "/";
const JSON_INDENT = 2;
const SITEMAP_OPEN =
    '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
const SITEMAP_CLOSE = "</urlset>\n";
const SITEMAP_INDEX_OPEN =
    '<?xml version="1.0" encoding="UTF-8"?>\n<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
const SITEMAP_INDEX_CLOSE = "</sitemapindex>\n";
const XML_ESCAPES: ReadonlyMap<string, string> = new Map([
    ["&", "&amp;"],
    ["<", "&lt;"],
    [">", "&gt;"],
    ['"', "&quot;"],
    ["'", "&apos;"],
]);

export const escapeXml = function escapeXml(value: string): string {
    let out = "";
    for (const char of value) {
        out += XML_ESCAPES.get(char) ?? char;
    }
    return out;
};

export const isoDate = function isoDate(moment: Date): string {
    return moment.toISOString().slice(0, "YYYY-MM-DD".length);
};

export const renderPayload = function renderPayload(site: string, route: DiscoveredRoute, content: unknown): string {
    const payload = {
        content,
        description: route.description,
        id: route.page,
        label: route.label,
        tab: route.tab,
        title: route.title,
        url: site + route.path,
    };
    return `${JSON.stringify(payload, null, JSON_INDENT)}\n`;
};

export const renderRoutePayload = function renderRoutePayload(discovery: Discovery, route: DiscoveredRoute): string {
    return renderPayload(discovery.site, route, routeContentOf(discovery, route));
};

const urlEntry = function urlEntry(location: string, stamp: string | null): string {
    const lastmod = stamp === null ? "" : `\n    <lastmod>${stamp}</lastmod>`;
    return `  <url>\n    <loc>${escapeXml(location)}</loc>${lastmod}\n  </url>`;
};

export const renderSitemap = function renderSitemap(discovery: Discovery, stamps: ReadonlyMap<string, string>): string {
    const entries = discovery.routes.map((route) => {
        const stamp = stamps.get(route.path);
        if (stamp === undefined) {
            throw new Error(unstampedRoute(route.path));
        }
        return urlEntry(discovery.site + route.path, stamp);
    });
    return [SITEMAP_OPEN, ...entries, SITEMAP_CLOSE].join("\n");
};

export const renderSitemapIndex = function renderSitemapIndex(
    site: string,
    sitemaps: readonly (readonly [string, string | null])[],
): string {
    const entries = sitemaps.map(([path, stamp]) => {
        const lastmod = stamp === null ? "" : `\n    <lastmod>${stamp}</lastmod>`;
        return `  <sitemap>\n    <loc>${escapeXml(site + path)}</loc>${lastmod}\n  </sitemap>`;
    });
    return [SITEMAP_INDEX_OPEN, ...entries, SITEMAP_INDEX_CLOSE].join("\n");
};

export const renderUrlSet = function renderUrlSet(locations: readonly string[]): string {
    return [SITEMAP_OPEN, ...locations.map((location) => urlEntry(location, null)), SITEMAP_CLOSE].join("\n");
};

export const renderSitemapParts = function renderSitemapParts(
    route: string,
    locations: readonly string[],
): readonly SitemapPart[] {
    const count = Math.max(1, Math.ceil(locations.length / SITEMAP_URL_CAP));
    return Array.from({ length: count }, (_, part) => {
        const listed = locations.slice(part * SITEMAP_URL_CAP, (part + 1) * SITEMAP_URL_CAP);
        const body = renderUrlSet(listed);
        const partRoute = sitemapPartRoute(route, part);
        const bytes = Buffer.byteLength(body);
        if (bytes > SITEMAP_BYTE_CAP) {
            throw new Error(oversizeSitemap(partRoute, listed.length, bytes));
        }
        return { body, route: partRoute };
    });
};

const DAY_MILLISECONDS = 86_400_000;

export const renderSecurityTxt = function renderSecurityTxt(site: string, contact: string, now: Date): string {
    const expires = new Date(now.getTime() + SECURITY_TXT_DAYS * DAY_MILLISECONDS);
    return [
        `${SECURITY_TXT_FIELDS.contact}: mailto:${contact}`,
        `${SECURITY_TXT_FIELDS.encryption}: ${site}${SLASH}${SECURITY_KEY_FILE}`,
        `${SECURITY_TXT_FIELDS.expires}: ${expires.toISOString()}`,
        `${SECURITY_TXT_FIELDS.languages}: ${SECURITY_TXT_LANGUAGES}`,
        `${SECURITY_TXT_FIELDS.canonical}: ${site}${SLASH}${SECURITY_TXT_FILE}`,
        "",
    ].join("\n");
};

export const renderRobots = function renderRobots(discovery: Discovery, sitemapPaths: readonly string[]): string {
    const { author, consent, site } = discovery;
    const agents = AI_AGENTS.flatMap((agent) => [`User-agent: ${agent}`, "Allow: /", ""]);
    const search = [
        ...SEARCH_AGENTS.map((agent) => `User-agent: ${agent}`),
        ...SEARCH_CRAWL_RULES.map((rule) => `${rule.directive}: ${rule.path}`),
        "",
    ];
    return [
        `# ${trainingConsent(consent, author, site)}`,
        "",
        "User-agent: *",
        "Allow: /",
        "",
        ...search,
        ...agents,
        ...sitemapPaths.map((path) => `Sitemap: ${site}${path}`),
        "",
    ].join("\n");
};

export const renderLlms = function renderLlms(discovery: Discovery, appendix: readonly string[]): string {
    const links = discovery.routes.map(
        (route) => `- [${route.label}](${discovery.site}${route.path}): ${route.description}`,
    );
    const texts = discovery.routes.map((route) => `- [${route.label}](${discovery.site}${markdownPathOf(route.path)})`);
    const payloads = discovery.routes.map((route) => `- [${route.label}](${discovery.site}${jsonPath(route)})`);
    return [
        `# ${discovery.name}`,
        "",
        `> ${discovery.summary}`,
        "",
        LLMS_PAGES_HEADING,
        "",
        ...links,
        "",
        LLMS_MARKDOWN_HEADING,
        "",
        LLMS_MARKDOWN_INTRO,
        "",
        `- [${LLMS_FULL_TEXT_LABEL}](${discovery.site}${SLASH}${FULL_TEXT_ROUTE})`,
        ...fullTextParts(discovery).map(
            (part) => `- [${LLMS_FULL_TEXT_LABEL}: ${part.label}](${discovery.site}${SLASH}${part.file})`,
        ),
        ...texts,
        "",
        LLMS_JSON_HEADING,
        "",
        LLMS_JSON_INTRO,
        "",
        ...payloads,
        "",
        ...appendix,
        LLMS_USE_HEADING,
        "",
        trainingConsent(discovery.consent, discovery.author, discovery.site),
        "",
    ].join("\n");
};
```
