Build Mac AppDocsMenu

Modules

Long-form content: the FAQ registry, and where guides went

Guides and comparisons are now built in, as the blog (docs/blog.md): MDX posts with kind: guide or kind: comparison, held to the rules below by site/lib/blog. What follows is the older typed-registry pattern the FAQ still uses, kept because it explains the registry the blog's surfaces share.

The template ships one long-form page, /faq, registered in site/lib/content/index.ts. A source project grew an explainer section and competitor comparisons on the same pattern and got most of its search traffic from them. The pattern is what matters; here it is.

One registry, every surface

Every long-form page is an entry in CONTENT (site/lib/content), typed by ContentPage in types.ts. site/app/sitemap.ts, site/app/llms.txt/route.ts and site/app/llms-full.txt/route.ts read that array, so a page cannot be in the sitemap and missing from llms.txt. The one hand-maintained twin is ROUTES in site/scripts/assert-metadata.sh, deliberately, so the check does not compute its expectation from the code it checks.

Content is typed data, not Markdown

A guide is an object: title (≤ 60 characters), description (≤ 155, no price), hand-written published and updated dates, sources, and sections each carrying a JSX body and a required plain-text mirror. The mirror is what llms-full.txt and the JSON-LD emit; required because an optional mirror is a mirror that drifts. MDX was rejected for exactly that: it has no plain-text twin, and a test cannot walk it for title length and dates the way it walks objects.

Add to ContentPage for a guide:

export type GuideSection = { id: string; heading: string; body: ReactNode; text: string };
export type Guide = ContentPage & {
  kind: "guide";
  definition: string;          // the one-sentence answer, quoted in llms.txt
  sections: readonly GuideSection[];
  related: readonly string[];  // paths worth reading next
};

and render it with site/components/ArticleShell.tsx, which already handles the trail, byline, date and sources.

Comparisons: facts, sources, dates — no verdicts

A comparison page is a table of checkable facts about another product, each with a source a reader can open and the date it was checked. It is noindex and out of the sitemap until at least MIN_INDEXABLE_ITEMS (site/lib/seo.ts) comparisons have been checked, and each one only goes live once its checkedOn date is set. Competitor names appear only in the comparison data — a test can walk the source for them (site/lib/test/walk.ts), the inverse of the brand test.

Dates

updated moves when the prose changes, by hand, in the same commit — never when a shared component is restyled, and never from a clock. It is printed in the byline, emitted as dateModified, and reported as the sitemap's lastModified: one constant, three surfaces.

This page is docs/modules/content.md in the repository, copied 2026-09-25.