SEO for Solos

Programmatic pages

Programmatic pages covers 8 of the SEO for Soloschecklist's implementation checks. Generating hundreds or thousands of pages without producing doorway pages. The gate matters more than the template.

8
Checks in group
2
Free snippets
4
Average impact
of 5
5
Position
of 12 groups

0 of 8

Saved in this browser

8 checks shown.

Programmatic pages

Generating hundreds or thousands of pages without producing doorway pages. The gate matters more than the template.

  • Every programmatic route is gated against an authoritative entity list5/M

    Why it matters

    An ungated dynamic route generates a page for any slug anyone types, including slugs an attacker generates. That is how a site accidentally publishes ten thousand thin pages.

    How to verify

    curl -s -o /dev/null -w '%{http_code}\n' https://example.com/glossary/not-a-real-term

    Then confirm 404, not 200.

    Implementation

    kit/pseo/gating.ts
    /**
     * Code Kit: entity gating.
     * Implements checks: pseo-entity-gate, found-404-returns-404
     *
     * An ungated dynamic route renders a page for any slug it is given, including
     * slugs a crawler invented by mangling a URL. Each one is an indexable page
     * with no content. This module is the gate.
     */
    
    export type GatedEntity = { slug: string };
    
    export type EntityGate<T extends GatedEntity> = {
      /** Every valid slug, for static generation. */
      slugs: () => string[];
      /** Returns undefined for anything not on the list. Never a partial match. */
      resolve: (slug: string) => T | undefined;
      /** Canonical slug to redirect to, or undefined. Aliases never render. */
      resolveAlias: (slug: string) => string | undefined;
      all: () => readonly T[];
      size: number;
    };
    
    export function createEntityGate<T extends GatedEntity>(
      entities: readonly T[],
      aliases: Readonly<Record<string, string>> = {},
    ): EntityGate<T> {
      const bySlug = new Map(entities.map((entity) => [entity.slug, entity]));
    
      // A duplicate slug means two entities compete for one URL. Fail loudly at
      // module load rather than serving whichever one won the Map insertion.
      if (bySlug.size !== entities.length) {
        const seen = new Set<string>();
        const duplicates = entities
          .map((entity) => entity.slug)
          .filter((slug) => (seen.has(slug) ? true : (seen.add(slug), false)));
        throw new Error(`Duplicate entity slugs: ${[...new Set(duplicates)].join(", ")}`);
      }
    
      return {
        slugs: () => entities.map((entity) => entity.slug),
        resolve: (slug) => bySlug.get(slug),
        resolveAlias: (slug) => {
          const target = aliases[slug];
          return target && bySlug.has(target) ? target : undefined;
        },
        all: () => entities,
        size: entities.length,
      };
    }
    
    /**
     * Slug normalisation for the URLs you generate, not for the ones you receive.
     * Receiving a slug that needs normalising means the link that produced it is
     * wrong; fix the link rather than accepting both forms.
     */
    export function toSlug(value: string): string {
      return value
        .toLowerCase()
        .normalize("NFKD")
        .replace(/[̀-ͯ]/g, "")
        .replace(/[^a-z0-9]+/g, "-")
        .replace(/^-+|-+$/g, "");
    }
    pseo-entity-gateImpact 5/5MediumFreeRead the chapter
  • Apply the strip-the-entity-name test before publishing a tier5/S

    Why it matters

    Remove the entity name from the page. If nothing unique remains, the tier is doorway pages and shipping it risks the whole domain, not just the tier.

    How to verify

    Take three pages from the tier, delete every occurrence of the entity name, and diff them. If the diff is empty, do not publish.

    pseo-strip-the-name-testImpact 5/5SmallFreeRead the chapter
  • Each page carries numbers computed from its own entity5/L

    Why it matters

    Numbers are the cheapest form of genuine uniqueness and the hardest to fake at scale. A page with six entity-specific figures cannot be a template with the name swapped.

    How to verify

    Pick five random pages in the tier and confirm at least four numeric values differ across all five.

    Implementation snippet

    The tested implementation from kit/pseo/facts.ts, plus the test that fails if it regresses.

    Unlock with the Code Kit$14930 day refund, no questions.
    pseo-per-entity-numbersImpact 5/5LargeRead the chapter
  • FAQs are computed per entity, not templated with a name substitution4/L

    Why it matters

    A per-entity FAQ answer that contains a real figure is quotable by an answer engine. The same sentence with only the name changed is recognisably generated.

    How to verify

    Compare the FAQ answers on two pages in the tier. If they differ only in the entity name, they are not computed.

    Implementation snippet

    The tested implementation from kit/pseo/facts.ts, plus the test that fails if it regresses.

    Unlock with the Code Kit$14930 day refund, no questions.
    pseo-computed-faqsImpact 4/5LargeRead the chapter
  • Missing data renders an honest label, never a fabricated value3/M

    Why it matters

    A template that prints 0 or an em dash when data is missing is asserting something false. Labelling the gap is both accurate and, unexpectedly, a trust signal.

    How to verify

    Find an entity with incomplete source data and confirm the page says so in words rather than rendering a zero.

    Implementation snippet

    The tested implementation from kit/pseo/facts.ts, plus the test that fails if it regresses.

    Unlock with the Code Kit$14930 day refund, no questions.
    pseo-honest-fallbacksImpact 3/5MediumRead the chapter
  • Generated data is committed as typed source, with a stamped update time3/M

    Why it matters

    Querying a database at build time makes the build non-reproducible and couples deploys to database availability. Committed snapshots make a data change a reviewable diff.

    How to verify

    Confirm the generated module exports a `*_UPDATED_AT` constant and that the file is in version control.

    Implementation snippet

    The tested implementation from kit/pseo/generate.ts, plus the test that fails if it regresses.

    Unlock with the Code Kit$14930 day refund, no questions.
    pseo-snapshot-generationImpact 3/5MediumRead the chapter
  • Every programmatic URL is prerendered, not rendered on demand3/M

    Why it matters

    A tier rendered on request depends on the database being up for the sales funnel to work, and pays a cold-start cost on the first crawl of every page.

    How to verify

    Check the build output and confirm the tier routes are listed as static, then confirm a request to a tier page returns a cache hit.

    Implementation snippet

    The tested implementation from kit/pseo/template.tsx, plus the test that fails if it regresses.

    Unlock with the Code Kit$14930 day refund, no questions.
    pseo-static-generationImpact 3/5MediumRead the chapter