import { readdirSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { DOC_SLUGS, docExcerpt, docSubtitle, docTitle, readDoc } from "../src/lib/docs";
import {
buildMetadata,
composeTitle,
PAGE_META,
pageMetadata,
SITE_NAME,
SITE_OG_IMAGE,
siteTitle,
specMetadata,
TITLE_MAX,
} from "../src/lib/page-meta";
import { allSpecs } from "../src/lib/specs";
/**
* Guards every page's own title.
*
* Pages exported `{ title, description, alternates }` and nothing else.
* Next.js shallow-merges metadata, so a page without an `openGraph` block
* inherited the root layout's -- meaning og:title, og:description and og:url
* were the HOMEPAGE's on every page of the site. Reddit, Slack, X and Facebook
* read og:title, so every spec we posted was announced as "LogicSRC — Open
* Coordination Standards for Humans & AI Agents" regardless of which spec the
* link pointed at. The
tag was correct throughout, which is why
* looking at the page never caught it.
*
* These tests fail if a page stops carrying its own social title, if a new
* route reaches the sitemap without one, or if two pages share a title.
*/
const APP_DIR = resolve(process.cwd(), "src/app");
type Meta = ReturnType;
function titleOf(metadata: Meta): string {
return String(metadata.title);
}
function ogOf(metadata: Meta): { title?: unknown; description?: unknown; url?: unknown; images?: unknown } {
return (metadata.openGraph ?? {}) as {
title?: unknown;
description?: unknown;
url?: unknown;
images?: unknown;
};
}
function twitterOf(metadata: Meta): { title?: unknown; images?: unknown } {
return (metadata.twitter ?? {}) as { title?: unknown; images?: unknown };
}
/** The sitemap's own static routes, read from source so the two cannot drift. */
function sitemapStaticPaths(): string[] {
const source = readFileSync(resolve(APP_DIR, "sitemap.ts"), "utf8");
// The array literal, not its type: the Route type mentions
// `Sitemap[number]["changeFrequency"]`, so a naive search for "];" stops
// inside it and the block comes back empty.
const start = source.indexOf("= [", source.indexOf("const STATIC_ROUTES"));
const end = source.indexOf("\n];", start);
return [...source.slice(start, end).matchAll(/path:\s*"([^"]+)"/g)].map((m) => m[1]);
}
describe("the site's own pages each have a title", () => {
const sitemapPaths = sitemapStaticPaths();
it("reads the sitemap's static routes", () => {
expect(sitemapPaths.length).toBeGreaterThan(5);
expect(sitemapPaths).toContain("/");
});
it("every static route in the sitemap has a PAGE_META entry", () => {
const missing = sitemapPaths.filter((path) => !PAGE_META[path]);
expect(
missing,
`Add these paths to PAGE_META in src/lib/page-meta.ts, or they ship with the homepage's social card: ${missing.join(", ")}`
).toEqual([]);
});
it("no PAGE_META entry is for a path nothing serves", () => {
// /hire-us is an SPA section rather than a sitemap-listed page in its own
// right, so it is allowed to appear here without a sitemap entry.
const allowed = new Set([...sitemapPaths, "/hire-us"]);
const orphans = Object.keys(PAGE_META).filter((path) => !allowed.has(path));
expect(orphans, `These PAGE_META paths are not served: ${orphans.join(", ")}`).toEqual([]);
});
it("titles and descriptions are unique per page", () => {
const seen = new Map();
for (const [path, meta] of Object.entries(PAGE_META)) {
const title = siteTitle(meta.title);
const earlier = seen.get(title);
expect(earlier, `"${title}" is used by both ${earlier} and ${path}`).toBeUndefined();
seen.set(title, path);
}
const descriptions = Object.values(PAGE_META).map((m) => m.description);
expect(new Set(descriptions).size).toBe(descriptions.length);
});
it("only the homepage is titled with the homepage title", () => {
const home = siteTitle(PAGE_META["/"].title);
const sharing = Object.entries(PAGE_META)
.filter(([path, meta]) => path !== "/" && siteTitle(meta.title) === home)
.map(([path]) => path);
expect(sharing).toEqual([]);
});
it("every title names the site, says something, and fits a link preview", () => {
for (const [path, meta] of Object.entries(PAGE_META)) {
const title = siteTitle(meta.title);
expect(title, path).toContain(SITE_NAME);
// "Docs · LogicSRC" and "Blog · LogicSRC" are what this change replaced:
// technically distinct, useless as a headline. Require a real subject.
expect(title.replace(` · ${SITE_NAME}`, "").trim().split(/\s+/).length, path).toBeGreaterThan(1);
expect(title.length, `${path} title is ${title.length} chars`).toBeLessThanOrEqual(TITLE_MAX);
expect(meta.description.length, path).toBeGreaterThan(40);
}
});
});
describe("spec landing pages are titled from the registry", () => {
const landings = allSpecs().filter((spec) => spec.landing);
it("there are spec landing pages to check", () => {
expect(landings.length).toBeGreaterThan(20);
});
it("each one gets its own title, description, og:url and image", () => {
const titles = new Map();
for (const spec of landings) {
const path = spec.landing as string;
const metadata = specMetadata(path);
const title = titleOf(metadata);
expect(title, path).toContain(spec.name);
expect(title.length, `${path} title is ${title.length} chars: ${title}`).toBeLessThanOrEqual(TITLE_MAX);
const earlier = titles.get(title);
expect(earlier, `"${title}" is used by both ${earlier} and ${path}`).toBeUndefined();
titles.set(title, path);
expect(ogOf(metadata).title, path).toBe(title);
expect(twitterOf(metadata).title, path).toBe(title);
expect(ogOf(metadata).url, path).toBe(`https://logicsrc.com${path}`);
expect((metadata.alternates as { canonical?: unknown }).canonical, path).toBe(path);
expect(ogOf(metadata).images, path).toEqual([SITE_OG_IMAGE]);
}
});
it("no spec landing title is the homepage's", () => {
const home = siteTitle(PAGE_META["/"].title);
for (const spec of landings) {
expect(titleOf(specMetadata(spec.landing as string)), spec.slug).not.toBe(home);
}
});
it("a page's own description is kept when it passes one", () => {
const spec = landings[0];
const metadata = specMetadata(spec.landing as string, "A longer description written on the page.");
expect(metadata.description).toBe("A longer description written on the page.");
expect(ogOf(metadata).description).toBe("A longer description written on the page.");
});
it("a spec page with no registry entry fails loudly rather than falling back", () => {
expect(() => specMetadata("/not-a-spec")).toThrow(/lib\/specs\.ts/);
expect(() => pageMetadata("/not-a-page")).toThrow(/PAGE_META/);
});
it("composeTitle trims a long subject line at a clause boundary", () => {
const long = composeTitle(
"OpenObject",
"A bucket you can mount: keyed objects encrypted by their owner, kept on paid disks at a stated redundancy, verified, repaired and read back by path"
);
expect(siteTitle(long).length).toBeLessThanOrEqual(TITLE_MAX);
expect(long.startsWith("OpenObject: A bucket you can mount")).toBe(true);
expect(long.endsWith(",")).toBe(false);
});
});
describe("content routes carry their own social title too", () => {
it("a doc, report or post gets openGraph and twitter titles and an image", () => {
const metadata = buildMetadata({
title: "OpenStream: A lossless byte-stream relay envelope",
description: "How one program hands another the exact bytes of a stream.",
path: "/docs/openstream",
type: "article",
});
expect(titleOf(metadata)).toBe(
"OpenStream: A lossless byte-stream relay envelope · LogicSRC"
);
expect(ogOf(metadata).title).toBe(titleOf(metadata));
expect(twitterOf(metadata).title).toBe(titleOf(metadata));
expect(ogOf(metadata).url).toBe("https://logicsrc.com/docs/openstream");
expect(ogOf(metadata).images).toEqual([SITE_OG_IMAGE]);
});
/**
* Next.js only supplies the file-convention app/opengraph-image.tsx card to
* routes that declare no `openGraph`. Every route declares one now, so the
* card has to be named explicitly or every page loses its social image --
* which is what already happened to blog posts, the one route that used to
* declare openGraph. They shipped with no og:image at all.
*/
it("a post with a featured image uses it instead of the site card", () => {
const metadata = buildMetadata({
title: "A post with art",
description: "It has its own image.",
path: "/blog/art",
type: "article",
images: ["https://cdn.example.com/art.png"],
});
expect(ogOf(metadata).images).toEqual(["https://cdn.example.com/art.png"]);
});
it("a syndicated post points og:url at its canonical original", () => {
const metadata = buildMetadata({
title: "A guest post",
description: "Published first somewhere else.",
path: "/blog/guest",
canonical: "https://example.com/guest",
type: "article",
});
expect(ogOf(metadata).url).toBe("https://example.com/guest");
});
});
describe("no page declares raw metadata", () => {
function pageFiles(dir: string, out: string[] = []): string[] {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const path = resolve(dir, entry.name);
if (entry.isDirectory()) pageFiles(path, out);
else if (entry.name === "page.tsx" || entry.name === "layout.tsx") out.push(path);
}
return out;
}
it("every page and layout builds its metadata through lib/page-meta", () => {
const offenders: string[] = [];
for (const file of pageFiles(APP_DIR)) {
const source = readFileSync(file, "utf8");
const declares = /export\s+(const\s+metadata|async\s+function\s+generateMetadata)/.test(source);
if (!declares) continue;
if (!source.includes('from "@/lib/page-meta"')) offenders.push(file.replace(`${APP_DIR}/`, ""));
}
expect(
offenders,
`These routes declare metadata without lib/page-meta, so they inherit the layout's openGraph title: ${offenders.join(", ")}`
).toEqual([]);
});
});
describe("doc pages are titled by their subject, not just their name", () => {
it("every published doc has a subject line", () => {
const missing = DOC_SLUGS.filter((slug) => docSubtitle(slug).length < 10);
expect(
missing,
`These docs have no subject line: give the spec a registry entry in lib/specs.ts, or the guide an entry in GUIDE_SUBTITLES: ${missing.join(", ")}`
).toEqual([]);
});
it("doc titles are distinct, name the doc, and fit a link preview", () => {
const titles = new Map();
for (const slug of DOC_SLUGS) {
const md = readDoc(slug);
expect(md, `docs/${slug}.md should be readable`).toBeTruthy();
const name = docTitle(md as string, slug);
const title = siteTitle(composeTitle(name, docSubtitle(slug)));
const earlier = titles.get(title);
expect(earlier, `"${title}" is used by both ${earlier} and ${slug}`).toBeUndefined();
titles.set(title, slug);
expect(title).toContain(name);
expect(title.length, `${slug} title is ${title.length} chars: ${title}`).toBeLessThanOrEqual(TITLE_MAX);
}
});
it("no doc description is a Status:/Slug: header or a bare list lead-in", () => {
for (const slug of DOC_SLUGS) {
const excerpt = docExcerpt(readDoc(slug) as string);
expect(excerpt, slug).not.toMatch(/^(status|slug|version)\b\s*:/i);
expect(excerpt, slug).not.toMatch(/:$/);
// Either a real paragraph, or nothing -- in which case the page and the
// docs index both fall back to the doc's subject line.
if (excerpt) expect(excerpt.length, slug).toBeGreaterThan(40);
}
});
it("every doc has a usable description one way or the other", () => {
for (const slug of DOC_SLUGS) {
const md = readDoc(slug) as string;
const name = docTitle(md, slug);
const description =
docExcerpt(md) || `${name}: ${docSubtitle(slug)}. Part of the LogicSRC open-standards surface.`;
expect(description.length, slug).toBeGreaterThan(40);
}
});
});