# test.build/core/persistence/page.persistence.test.ts

> 312 lines of code and 0 definitions.

Tree: Codebase Testing
Language: typescript
Canonical: https://banes-lab.com/anatomy/testing#file-testing-test-build-core-persistence-page-persistence-test-ts
Source text: https://banes-lab.com/source/testing/test.build/core/persistence/page.persistence.test.ts.txt

Listed in [test.build/core/persistence](https://banes-lab.com/api/source/testing/test.build/core/persistence.md), after [test.build/core/persistence/journal.persistence.test.ts](https://banes-lab.com/source/testing/test.build/core/persistence/journal.persistence.test.ts.md) and before [test.build/core/persistence/route.persistence.test.ts](https://banes-lab.com/source/testing/test.build/core/persistence/route.persistence.test.ts.md).

## Contained in

- [test.build/core/persistence](https://banes-lab.com/anatomy/testing/folder-testing-test-build-core-persistence.md)

## Source

```typescript
import { DISCOVERY, SOURCE } from "../converters/site.fixture.ts";
import { afterEach, describe, expect, it } from "vitest";
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import {
    writeDiscovery,
    writePages,
    writeRecordPages,
    writeSectionPages,
    writeSitemapIndex,
    writeSitemapParts,
    writeSourcePages,
    writeText,
} from "@banes-lab/build-scripts/core/persistence/page.persistence.ts";
import { JSDOM } from "jsdom";
import { join } from "node:path";
import { stampLedger } from "@banes-lab/build-scripts/core/persistence/route.persistence.ts";
import { tmpdir } from "node:os";
import { undatedRoute } from "@banes-lab/build-scripts/configuration/strings/route.strings.ts";
import { writeVerbatim } from "@govlab/canonical-write";

const TEMPLATE = '<!doctype html><html><head><title></title></head><body><main id="app"></main></body></html>';

const scratch: string[] = [];

const folder = function folder(): string {
    const dir = mkdtempSync(join(tmpdir(), "pages-"));
    scratch.push(dir);
    return dir;
};

afterEach(() => {
    for (const dir of scratch.splice(0)) {
        rmSync(dir, { force: true, recursive: true });
    }
});

describe("writeText", () => {
    it("writes a text file below the output folder, creating its folders", () => {
        const dir = folder();
        writeText(dir, "a/b/c.txt", "hello");
        expect(readFileSync(join(dir, "a", "b", "c.txt"), "utf8")).toBe("hello");
    });
});

describe("writePages", () => {
    const STAMP = { lastmod: "2026-10-07", published: "2026-09-30" };
    const DATED = new Map(["/", "/terms", "/terms/guide"].map((path) => [path, STAMP]));

    const pageSource = function pageSource(): {
        readonly dated: ReadonlyMap<string, string>;
        readonly source: Parameters<typeof writePages>[1];
    } {
        const dated = new Map<string, string>();
        const source: Parameters<typeof writePages>[1] = {
            ...SOURCE,
            main: "app",
            renderer: {
                ...SOURCE.renderer,
                applyHead: () => {},
                headOf: (page, definition, pathname, dates) => {
                    if (dates !== undefined && pathname !== undefined) {
                        dated.set(pathname, `${dates.published} ${dates.modified}`);
                    }
                    return SOURCE.renderer.headOf(page, definition, pathname);
                },
                renderStaticPage: () => [],
            },
            visit: () => {},
        };
        return { dated, source };
    };

    it("writes one dated page file per route plus the undated 404 page, and drops a stale precompressed sibling", async () => {
        const dir = folder();
        writeVerbatim(join(dir, "terms.html.gz"), "stale");
        const { dated, source } = pageSource();
        await writePages(dir, source, DISCOVERY, DATED, TEMPLATE);
        const pages = ["index.html", "terms.html", "terms/guide.html", "404.html"];
        expect(pages.every((file) => existsSync(join(dir, file)))).toBe(true);
        expect(existsSync(join(dir, "terms.html.gz"))).toBe(false);
        expect(Object.fromEntries(dated)).toStrictEqual({
            "/": "2026-09-30 2026-10-07",
            "/terms": "2026-09-30 2026-10-07",
            "/terms/guide": "2026-09-30 2026-10-07",
        });
    });

    it("stops on a route the ledger holds no stamp for", async () => {
        const undated = new Map([["/", STAMP]]);
        await expect(writePages(folder(), pageSource().source, DISCOVERY, undated, TEMPLATE)).rejects.toThrow(
            undatedRoute("/terms"),
        );
    });
});

describe("writeSourcePages", () => {
    it("writes one page per source route with its head and body, and lists every page in the source sitemap", () => {
        const dir = folder();
        const { document } = new JSDOM("").window;
        const subject = {
            alternates: { json: "/json/source/tree/a.ts", markdown: "/source/tree/a.ts.md" },
            description: "a.ts is a file in Site.",
            language: "typescript",
            license: null,
            name: "a.ts",
            path: "/anatomy/tree/file-a-ts",
            repository: null,
            tabPath: "/anatomy/tree",
            tabTitle: "Site",
            title: "Site — a.ts — Bane's Lab",
            trail: [{ label: "Site", path: "/anatomy/tree" }],
        };
        const titles: string[] = [];
        writeSourcePages(
            dir,
            {
                body: {
                    renderSourceBody: (route) => {
                        const element = document.createElement("article");
                        element.textContent = route.subject.name;
                        return element;
                    },
                },
                main: "app",
                renderer: {
                    applyHead: (root, head) => {
                        root.title = head.title;
                    },
                    sourceHead: (held) => {
                        titles.push(held.title);
                        return { description: held.description, path: held.path, robots: "index", title: held.title };
                    },
                },
                site: "https://example.test",
            },
            [
                { children: [], parent: { label: "Site", path: "/anatomy/tree" }, subject, text: null },
                {
                    children: [],
                    parent: { label: "Site", path: "/anatomy/tree" },
                    subject: { ...subject, name: "core", path: "/anatomy/tree/folder-core", title: "core" },
                    text: null,
                },
            ],
            TEMPLATE,
        );
        const html = readFileSync(join(dir, "anatomy/tree/file-a-ts.html"), "utf8");
        expect(html).toContain(`<title>${subject.title}</title>`);
        expect(html).toContain("<article>a.ts</article>");
        expect(titles).toStrictEqual([subject.title, "core"]);
        expect(existsSync(join(dir, "anatomy/tree/folder-core.html"))).toBe(true);
        const sitemap = readFileSync(join(dir, "sitemap-sources.xml"), "utf8");
        expect(sitemap).toContain("https://example.test/anatomy/tree/file-a-ts");
        expect(sitemap).not.toContain("folder-core");
    });
});

describe("writeRecordPages", () => {
    it("writes one page per record with its head and body, and lists every record in the record sitemap", () => {
        const dir = folder();
        const { document } = new JSDOM("").window;
        const record = {
            aliases: [],
            alternates: { json: "/json/records/lexicon/cache", markdown: "/records/lexicon/cache.md" },
            code: null,
            collection: { label: "Lexicon", path: "/ontology/lexicon" },
            description: "A store that answers again without the work.",
            facts: [],
            kind: "term",
            layer: null,
            name: "Cache",
            path: "/records/lexicon/cache",
            placement: null,
            relations: [],
            section: "core",
            summary: null,
            title: "Cache — Lexicon — Bane's Lab",
        };
        const sectioned: string[] = [];
        const parts = writeRecordPages(
            dir,
            {
                body: {
                    renderRecordBody: (subject) => {
                        const element = document.createElement("article");
                        element.textContent = subject.name;
                        return element;
                    },
                },
                main: "app",
                renderer: {
                    applyHead: (root, head) => {
                        root.title = head.title;
                    },
                    recordHead: (held) => ({
                        description: held.description,
                        path: held.path,
                        robots: "index",
                        title: held.title,
                    }),
                    renderStaticSection: (page, path, id) => {
                        sectioned.push(`${page} ${path} ${id}`);
                        const rows = document.createElement("section");
                        rows.textContent = "rows";
                        return rows;
                    },
                },
                site: "https://example.test",
            },
            [record],
            TEMPLATE,
        );
        const html = readFileSync(join(dir, "records/lexicon/cache.html"), "utf8");
        expect(html).toContain(`<title>${record.title}</title>`);
        expect(html).toContain("<article>Cache<section>rows</section></article>");
        expect(sectioned).toStrictEqual(["ontology /ontology/lexicon core"]);
        expect(parts).toStrictEqual(["/sitemap-records.xml"]);
        expect(readFileSync(join(dir, "sitemap-records.xml"), "utf8")).toContain("https://example.test/records/lexicon/cache");
    });
});

describe("writeSectionPages", () => {
    it("writes one page per section with its head and body, and lists every section in the section sitemap", () => {
        const dir = folder();
        const { document } = new JSDOM("").window;
        const seed = {
            intro: null,
            kind: null,
            name: "Core Vocabulary",
            page: "ontology",
            part: 1,
            parts: 1,
            records: 2,
            rows: [0, 2] as const,
            section: "core-vocabulary",
            tab: "lexicon",
            tabPath: "/ontology/lexicon",
            tabTitle: "Lexicon",
        };
        const subject = {
            alternates: { json: "/json/ontology/lexicon/core-vocabulary", markdown: "/ontology/lexicon/core-vocabulary.md" },
            description: "The 2 records of the Core Vocabulary section in Lexicon.",
            name: seed.name,
            parts: [],
            path: "/ontology/lexicon/core-vocabulary",
            seed,
            title: "Core Vocabulary — Lexicon — Bane's Lab",
        };
        const parts = writeSectionPages(
            dir,
            {
                main: "app",
                renderer: {
                    applyHead: (root, head) => {
                        root.title = head.title;
                    },
                    renderSectionPage: (held) => {
                        const element = document.createElement("article");
                        element.textContent = held.name;
                        return element;
                    },
                    sectionHead: (held) => ({
                        description: held.description,
                        path: held.path,
                        robots: "index",
                        title: held.title,
                    }),
                },
                site: "https://example.test",
            },
            [subject],
            TEMPLATE,
        );
        const html = readFileSync(join(dir, "ontology/lexicon/core-vocabulary.html"), "utf8");
        expect(html).toContain(`<title>${subject.title}</title>`);
        expect(html).toContain("<article>Core Vocabulary</article>");
        expect(parts).toStrictEqual(["/sitemap-sections.xml"]);
        expect(readFileSync(join(dir, "sitemap-sections.xml"), "utf8")).toContain(
            "https://example.test/ontology/lexicon/core-vocabulary",
        );
    });
});

describe("writeDiscovery", () => {
    it("writes each route's payload and Markdown alternate, the full text, the page sitemap, robots and llms from the stamped ledger", async () => {
        const dir = folder();
        const ledger = join(folder(), "sitemap.generated.json");
        const stamped = await stampLedger(DISCOVERY, new Date("2026-09-19T14:00:00Z"), ledger);
        const [route, stamp] = writeDiscovery(dir, DISCOVERY, stamped);
        const written = [
            "json/terms.json",
            "json/terms/guide.json",
            "terms.md",
            "index.md",
            "llms-full.txt",
            "llms-full/terms.txt",
            "sitemap-pages.xml",
            "robots.txt",
            "llms.txt",
        ];
        expect(written.every((file) => existsSync(join(dir, file)))).toBe(true);
        expect(route).toBe("/sitemap-pages.xml");
        expect(stamp).toBe("2026-09-19");
        expect(readFileSync(join(dir, "sitemap-pages.xml"), "utf8")).toContain("<lastmod>2026-09-19</lastmod>");
        expect(readFileSync(join(dir, "llms.txt"), "utf8")).toContain("/llms-full/terms.txt");
        expect(readFileSync(join(dir, "robots.txt"), "utf8")).not.toContain("sitemap-catalog.xml");
        const stored: unknown = JSON.parse(readFileSync(ledger, "utf8"));
        expect(Object.keys(stored ?? {})).toContain("/terms/guide");
    });
});

describe("writeSitemapParts and writeSitemapIndex", () => {
    it("writes each part of a sitemap and an index naming every part", () => {
        const dir = folder();
        const routes = writeSitemapParts(dir, "/sitemap-catalog.xml", ["https://example.test/a.md"]);
        expect(routes).toStrictEqual(["/sitemap-catalog.xml"]);
        writeSitemapIndex(dir, "https://example.test", [
            ["/sitemap-pages.xml", "2026-10-05"],
            ...routes.map((held): readonly [string, null] => [held, null]),
        ]);
        const index = readFileSync(join(dir, "sitemap.xml"), "utf8");
        expect(index).toContain("https://example.test/sitemap-catalog.xml");
        expect(index).toContain("<lastmod>2026-10-05</lastmod>");
        expect(readFileSync(join(dir, "sitemap-catalog.xml"), "utf8")).toContain("https://example.test/a.md");
    });
});
```
