// The crawler pair: `/sitemap.xml` and `/robots.txt` (server/blog.ts). // // Both are anonymous discovery surfaces, so both are scoped like `/feed.xml` // and not like the admin's own vault: published notes only, EXCLUDE_TAGS and // the languageFilter applied. Before these routes existed, both paths fell // through to the SPA catch-all and answered a crawler with a 200 and a page of // HTML — a "sitemap" that was neither XML nor a map. import assert from "node:assert/strict"; import { after, before, beforeEach, describe, it } from "node:test"; import { renderFeed, renderRobots, renderSitemap } from "../server/blog.ts"; import { initIndexer } from "../server/indexer.ts"; import { initSite } from "../server/site.ts"; import { patchSettings } from "../server/settings.ts"; import { initVault } from "../server/vault.ts"; import { makeDir, makeVault, note, removeVault } from "./helpers/vault.ts"; const ORIGIN = "https://astrolabe.example"; const data = makeDir(); const root = makeVault({ "Newest.md": note({ publish: "true", date: "2026-05-04" }, "The newest published post.\n"), "Middle.md": note({ publish: "true", date: "2025-01-02" }, "An older published post.\n"), "Folder/Nested post.md": note({ publish: "true", date: "2024-07-07" }, "A post in a folder.\n"), "مقالة.md": note({ publish: "true", date: "2024-03-03" }, "فقرة عربية كاملة تكفي لتصنيف اللغة.\n"), "About.md": note({ publish: "true", page: "true", date: "2023-02-02" }, "Who writes here.\n"), "Draft.md": note({ date: "2026-06-06" }, "Unpublished — nobody may learn this exists.\n"), }); /** Every `` in a rendered sitemap, in document order. */ function locs(xml: string): string[] { return [...xml.matchAll(/([^<]*)<\/loc>/g)].map((m) => m[1]); } /** The `` paired with a given ``, or null when it carries none. */ function lastmod(xml: string, loc: string): string | null { const at = xml.indexOf(`${loc}`); assert.notEqual(at, -1, `no for ${loc}`); const block = xml.slice(at, xml.indexOf("", at)); return block.match(/([^<]*)<\/lastmod>/)?.[1] ?? null; } const OFF = { mode: "off", lang: null, fallbackFrom: null } as const; before(async () => { initSite({ ASTROLABE_DATA: data, SITE_URL: ORIGIN }); initVault(root); await initIndexer(); }); after(() => { removeVault(root); removeVault(data); }); beforeEach(() => { patchSettings({ publicLayout: null }); }); describe("sitemap.xml", () => { it("is a well-formed urlset with the front door first", () => { const xml = renderSitemap(ORIGIN, OFF); assert.ok(xml.startsWith('\n')); assert.ok(xml.includes('')); assert.ok(xml.trimEnd().endsWith("")); assert.equal(locs(xml)[0], `${ORIGIN}/`); // Every opened is a closed — the cheap structural check that // catches a half-built entry before a crawler does. assert.equal((xml.match(//g) ?? []).length, (xml.match(/<\/url>/g) ?? []).length); }); it("names every visitor-visible note and nothing else", () => { const found = locs(renderSitemap(ORIGIN, OFF)); assert.ok(found.includes(`${ORIGIN}/Newest`)); assert.ok(found.includes(`${ORIGIN}/Middle`)); assert.ok(found.includes(`${ORIGIN}/Folder/Nested%20post`)); // AN UNPUBLISHED NOTE IS NOT A URL. A sitemap that names one hands a // crawler the one thing the whole visitor surface withholds: existence. assert.ok(!found.some((u) => u.includes("Draft"))); assert.equal(found.length, 6); // home + 5 published notes }); it("keeps static pages, which the feed drops", () => { patchSettings({ publicLayout: "designed" }); const page = `${ORIGIN}/About`; // The feed is a timeline and an About page is not an article… assert.ok(!renderFeed(ORIGIN, OFF).includes("" + page + "")); // …but the sitemap is the list of URLs this site serves, and /About is one. assert.ok(locs(renderSitemap(ORIGIN, OFF)).includes(page)); }); it("takes lastmod from the note's own date, to the second", () => { const xml = renderSitemap(ORIGIN, OFF); assert.equal(lastmod(xml, `${ORIGIN}/Middle`), "2025-01-02T00:00:00Z"); // The front door borrows the newest post's date rather than claiming // "now" on every fetch. assert.equal(lastmod(xml, `${ORIGIN}/`), lastmod(xml, `${ORIGIN}/Newest`)); assert.equal(lastmod(xml, `${ORIGIN}/`), "2026-05-04T00:00:00Z"); }); it("percent-encodes paths and XML-escapes the result", () => { const found = locs(renderSitemap(ORIGIN, OFF)); assert.ok(found.includes(`${ORIGIN}/%D9%85%D9%82%D8%A7%D9%84%D8%A9`)); assert.ok(!found.some((u) => /[<>&"']/.test(u))); }); it("is language-scoped exactly like the feed", () => { const arabic = locs(renderSitemap(ORIGIN, { mode: "ar", lang: "ar", fallbackFrom: null })); assert.deepEqual(arabic, [`${ORIGIN}/`, `${ORIGIN}/%D9%85%D9%82%D8%A7%D9%84%D8%A9`]); const english = locs(renderSitemap(ORIGIN, { mode: "en", lang: "en", fallbackFrom: null })); assert.ok(english.includes(`${ORIGIN}/Newest`)); assert.ok(!english.includes(`${ORIGIN}/%D9%85%D9%82%D8%A7%D9%84%D8%A9`)); }); it("says so when the filter fell back to every language", () => { const xml = renderSitemap(ORIGIN, { mode: "ar", lang: null, fallbackFrom: "ar" }); assert.match(xml, /