Skip to content

Programmatic SEO

Markdown

Programmatic SEO builds pages out of the listings you already have: /best-{category}-for-{tag}/ for every category and tag pair with enough listings, /{name}-alternatives/ for every listing, and, off until you enable it, /free-{category}-{nouns}/. Two switches gate it, features.pseo and pseo.enabled, both true by default; the copy is pseo.templates in site.config.ts, which the demo fills with two entries and a fresh config leaves empty.

  1. Add or edit an entry in pseo.templates. The id picks the pattern; the copy is yours.

    site.config.ts
    pseo: {
    enabled: true,
    maxPages: 2000,
    templates: [
    {
    id: 'best-category-for-tag',
    enabled: true,
    minItems: 3,
    maxItems: 24,
    title: 'Best {category} {nouns} for {tag} ({year})',
    description: 'The {count} best {category} {nouns} for {tag}, compared on pricing, platforms and features. Updated {year}.',
    h1: 'Best {category} {nouns} for {tag}',
    intro: 'Looking for {category} {nouns} built for {tag}? We compared {count} of them, starting with {topNames}.\n\nEvery {noun} here is listed in our {category} category and tagged {tag}.',
    faq: [{ q: 'How were these {category} {nouns} selected?', a: 'Every {noun} is submitted, reviewed by a human and tagged by hand.' }],
    },
    ],
    },

    pnpm check:config answers site.config.ts OK — <name> (<url>); …. An unknown placeholder or a repeated id fails here, naming the field.

  2. Build and read the generator’s line.

    Terminal
    pnpm build
    pseo: generated N (dropped by minItems M, truncated K)

    N is the page count. M is how many candidates had fewer listings than their minItems. K is non-zero only above pseo.maxPages. A slug that collides with an existing page is named on its own [zerodirs:pseo] skipped "<slug>" line. Near the end, seo-report: … pseo N pages (best-category-for-tag=A, name-alternatives=B) counts the same pages from the rendered output.

  3. Open one page: https://<your domain>/best-<category>-for-<tag>/ after a deploy, or dist/client/best-<category>-for-<tag>/index.html in the build.

    The demo’s /best-writing-for-long-form/ page: breadcrumb, h1, intro, the item list, the FAQ and the sibling links

  4. Check the sitemap chunk. https://<your domain>/sitemap-pseo-0.xml lists exactly the generated URLs; it is absent while there are none.

Where You should see
The count the build output pseo: generated N (dropped by minItems M, truncated K) with N above 0
A page https://<your domain>/best-<category>-for-<tag>/ your h1, the intro, one card per listing, the FAQ, “Related guides”
The other direction any listing page “Appears in”, linking the generated pages that list it
The sitemap https://<your domain>/sitemap-pseo-0.xml one <loc> per generated page
Coverage the build output seo-report: categories without pSEO coverage: none
Template id URL One page per minItems default Ships enabled
best-category-for-tag /best-{category}-for-{tag}/ category and tag pair: the listings in that category carrying that tag 3 yes
name-alternatives /{name}-alternatives/ listing: the same ranking as its related block, minus itself 2 yes
free-category /free-{category}-{nouns}/ category: its listings priced free 3 no

The generator runs at build time, template by template in config order. A candidate with fewer listings than the template’s minItems is dropped and counted. A slug that collides with a reserved segment, a route base, a category, a tag or a listing is skipped with a warning; two templates producing the same slug keep the first. Survivors are sorted by listing count, largest first, and cut to pseo.maxPages. A page shows at most maxItems listings — featured first, then by popularity; alternatives keep their ranking — and never more than 24, the ceiling the ItemList schema and the link budget assume; a larger value is clamped and reported.

Every page links sideways under “Related guides”. A category-and-tag page gets up to six pages of the same category with other tags and six of the same tag in other categories; an alternatives page gets the alternatives pages of its listed items and the best-of pages of its category, six each. The template holds a page to 30 internal links inside <main>: the breadcrumb, the cards and the “More” link come first, and the siblings take what is left.

Copy is interpolated from the SEO placeholders plus {pricing} and {topNames}, the first three names — see Registries. intro is plain text: a blank line starts a paragraph, and no Markdown is rendered. To rewrite one page by hand, add src/content/pseo/<slug>.md: its body replaces the intro, and a title or description in its frontmatter replaces the meta. The FAQ is visible copy only; no FAQPage schema is emitted.

Config state The visitor sees /admin/ reports Fix
features.pseo: false no generated pages and no “Appears in”; the report says pseo 0 pages (disabled) nothing set it to true
pseo.enabled: false the same nothing set it to true
one template enabled: false that template’s pages are gone, the others stay nothing set it to true, or delete the entry
minItems higher than the data supports few or no pages; dropped by minItems M is large and seo-report: categories without pSEO coverage: names categories nothing lower minItems, or tag more listings
the file budget above 80% of the plan limit the build warns; above 90% it fails with Cut files (seo.ogScope, fewer pSEO templates, fewer listings) nothing fewer templates, seo.ogScope.pseo: false; see Cost and limits
the free edition no generated pages nothing the paid starter

/admin/ reports nothing for any of these: generation has no runtime.

Field Default What it changes
features.pseo true the feature switch
pseo.enabled true the second switch; the template list can stay in the file while off
pseo.maxPages 2000 the cap, applied after sorting by size
pseo.templates[] []; two in the demo id, enabled, minItems, maxItems, title, description, h1, intro, faq[]
seo.templates.pseo '{title}' and '{title} — {count} {nouns} compared, updated for {year}.' the title and meta description wrapper
seo.ogScope.pseo false one generated OG image per page

See features, pseo and seo.

  • Directorysrc/
    • Directoryfeatures/
      • index.ts the facade: no pages when either switch is off
      • Directorypaid/pseo/
        • patterns.ts the patterns and their slug shapes
        • generate.ts minItems, collisions, ordering, maxPages, siblings
        • copy.ts the placeholders and the plain-text intro
    • templates/PseoPage.astro the page: breadcrumbs, list, FAQ, siblings, JSON-LD, the link budget
    • Directorycomponents/pseo/ the intro, list, FAQ and siblings blocks
    • Directorycontent/pseo/ one Markdown file per page you override
  • site.config.ts the demo’s templates