Choosing an SSG for API Reference Documentation

API reference is documentation with unusual constraints. Most of its pages are generated from an OpenAPI or GraphQL schema rather than written by a person. Several versions are published at once. And the page count is large enough that build time becomes a scheduling problem rather than an annoyance — a 200-endpoint API across four supported versions is easily five thousand pages.

Those constraints change which generator is right, and the answer is often different from the one that suits the hand-written guides on the same site. This guide works through the criteria and the trade-offs. It sits under the SSG Framework Selection Matrix.

Prerequisites

  • A machine-readable schema: OpenAPI, GraphQL SDL, Protobuf, or a typed source you can introspect.
  • A rough page count: endpoints plus schema objects, multiplied by supported versions.
  • A decision about whether reference and guides share one site or two.

The Four Constraints That Decide It

What makes API reference different Four constraints. Scale: thousands of pages per version. Generation: most pages come from a schema, not an author. Versioning: several versions published simultaneously. Search: the index outgrows what a client can download. Each constraint maps to a generator capability. Four constraints, four capabilities to compare Scale 1,200 pages per version × 4 versions → raw build throughput matters most Generation pages come from a schema → programmatic routing, typed data Versioning v1 … v4 live at once → per-version builds, stable URLs Search index over 5,000 pages → split, trim, or host it
None of these constraints applies to a fifty-page guide site, which is why the generator that suits your handbook may be the wrong one for your reference.

Scale: Build Throughput Is the First Filter

At five thousand pages, the difference between generators stops being aesthetic. A build that takes four seconds can run on every commit; one that takes eleven minutes cannot, and the workflow around it changes accordingly — fewer deploys, batched changes, and a slower feedback loop for the people writing the surrounding prose.

Generator5,000 generated pagesRebuild after one spec changePractical ceiling
Hugo9 s9 s (full)Very high
Eleventy74 s3 s (incremental)High
Astro260 s6 s (dev server)Medium
Next.js export340 s8 s (dev server)Medium

Two readings of that table matter. Hugo's number is the reason it dominates large reference sites: it is fast enough that no caching or incrementality is needed at all. And Eleventy's second column is the reason it competes: the full build is slower, but --incremental makes the edit loop comparable, as covered in Enabling Incremental Builds in Eleventy.

Generation: How Pages Come From the Schema

Every generator can produce pages from data; they differ in how much ceremony it takes and how much the build helps you when the schema changes shape.

// Eleventy — pagination over an OpenAPI spec produces one page per operation
module.exports = () => {
  const spec = require('./api/openapi.v4.json');
  const ops = [];
  for (const [path, methods] of Object.entries(spec.paths)) {
    for (const [method, op] of Object.entries(methods)) {
      ops.push({ path, method, id: op.operationId, summary: op.summary, op });
    }
  }
  return ops;
};
---
// Astro — getStaticPaths with typed spec data
import spec from '../../api/openapi.v4.json';
export function getStaticPaths() {
  return Object.entries(spec.paths).flatMap(([path, methods]) =>
    Object.entries(methods).map(([method, op]) => ({
      params: { slug: op.operationId },
      props: { path, method, op },
    })));
}
const { path, method, op } = Astro.props;
---
<h1>{method.toUpperCase()} {path}</h1>
<p>{op.summary}</p>
{{/* Hugo — one page per operation from a data file, via a headless section */}}
{{ range $path, $methods := .Site.Data.openapi_v4.paths }}
  {{ range $method, $op := $methods }}
    {{ $.Scratch.Add "ops" (dict "path" $path "method" $method "op" $op) }}
  {{ end }}
{{ end }}

The important difference is not syntax. Astro's typed props mean a spec that changes shape produces a build error at the component boundary; Hugo and Eleventy produce empty output for the missing field and carry on. On a reference site where the spec is generated by another team, that difference decides whether a breaking change is caught at build time or reported by a reader.

Versioning: The Constraint People Underestimate

Publishing several versions at once is where reference sites accumulate accidental complexity. The clean model is: version is a path segment, each version is generated from its own frozen spec, and old versions are rebuilt only when their spec changes.

/api/v4/operations/create-invoice/     ← current, regenerated on every spec change
/api/v3/operations/create-invoice/     ← frozen, rebuilt only if v3 spec changes
/api/v2/…                              ← frozen

Generate each version independently and deploy the union. That way a v4 spec change cannot break v3 pages, and a v2 deprecation is a matter of deleting one directory and adding redirects — the mechanics in Keeping Redirects Working After an SSG Migration apply exactly to version sunsets.

Per-version generation and deployment Four spec files, one per API version, each generating its own set of pages. Only the current version regenerates on a spec change; older versions are cached artifacts. All four sets deploy together as one site, with a version switcher linking equivalent pages. Independent builds, one deployment openapi.v4.json openapi.v3.json openapi.v2.json openapi.v1.json build v4 · 1,240 pages cached artifact cached artifact cached artifact one deployment /api/v1 … /api/v4 version switcher links equivalents A v4 spec change rebuilds 1,240 pages, not 5,000 — and cannot affect a frozen version at all
Caching the frozen versions is what keeps the build proportional to what changed. It also means an old version's pages are byte-identical to what was reviewed when that version shipped.

Search: The Constraint That Arrives Late

Client-side search is delightful at 200 pages and impossible at 5,000. A prebuilt index over a full reference is several megabytes, which nobody should download to search for one endpoint.

Three workable answers, in increasing order of effort: index only titles, operation IDs and one-line summaries, which keeps a 5,000-page index around 400 KB; split the index per version and load only the active one; or move search to a hosted service and keep the client payload at zero. Most reference sites end up at the second option and then the third.

Search index size as a reference site grows Index size against page count for three strategies. A full-text index reaches 5.8 megabytes at 5,000 pages. A titles-and-summaries index stays around 400 kilobytes. A per-version split index stays around 120 kilobytes for the active version. A dashed line marks a 500 kilobyte practical download budget. The index outgrows the client before the site feels large 500 KB budget full text · 5.8 MB titles + summaries · 400 KB 500 1,500 3,000 5,000 pages Green dashed: per-version split index, ~120 KB for the active version only Measured on generated OpenAPI reference output with a standard client-side index
Full-text indexing crosses a reasonable download budget somewhere around 1,500 pages, which on a reference site is one version of a medium API.

Putting It Together

CriterionHugoEleventyAstroNext export
Build at 5,000 pagesExcellentGoodFairFair
Schema-driven pagesData filesPaginationgetStaticPaths + typesgenerateStaticParams
Type-checked spec changesNoNoYesYes
Per-version isolationEasyEasyEasyEasy
Interactive try-it componentsLimitedLimitedNative islandsNative
Shared design system with the appHardHardYesYes

The practical split: Hugo when the reference is large and mostly static, Astro when it is smaller and needs interactive request builders sharing components with the product, and Eleventy when you want a middle ground with an excellent edit loop. Next.js export makes sense mainly when the documentation is part of a Next application already — the constraints of that target are in Handling Dynamic Routes in Next.js Static Export.

One more consideration decides more of these choices than the table suggests: who owns the reference. When the schema and the generator both belong to the API team, a fast build with plain templates is usually right, because the people maintaining it are not front-end specialists and will not enjoy a component framework. When the reference is owned by the same team that builds the product's interface, sharing components is worth real build time — a try-it panel that behaves exactly like the product's console is a genuinely better document than a code sample.

A perfectly reasonable answer is two generators: Hugo for the reference, something component-friendly for the guides, joined at the edge by path. It costs a routing rule and removes the compromise entirely.

Pitfalls & Rollback

  • Choosing on the guides and inheriting it for the reference. The constraints are different by an order of magnitude.
  • Committing generated pages. Diffs become unreviewable and hand edits silently diverge from the spec.
  • Rebuilding every version on every change. Freeze old versions as artifacts; only the current one needs regenerating.
  • Deferring the search decision. The index outgrows the client somewhere around a thousand pages, usually without warning.
  • Ignoring spec-shape changes. Without type checking, a renamed field produces empty sections rather than an error.
  • Rollback: because reference output is generated, switching generators is a matter of rewriting templates rather than migrating content. That is the one genuine advantage of a corpus nobody hand-edits.

Conclusion

Pick for the reference's constraints, not the guides': raw build throughput first, then how much the build helps when the schema changes, then per-version isolation, then search. Hugo wins on scale, Astro wins on interactivity and type safety, Eleventy sits comfortably between them, and running two generators joined at the edge is a legitimate answer rather than an admission of defeat. The general comparison is in the SSG Framework Selection Matrix.

FAQ

What makes API reference different from other documentation?

Three things: most pages are generated from a schema rather than written, several versions are published simultaneously, and the page count is an order of magnitude larger than the hand-written guides around it. Every selection criterion follows from those.

How many pages does a typical API reference produce?

A medium REST API with 200 endpoints and 400 schema objects produces roughly 600 to 1,200 pages per version once you emit a page per operation and per model. Multiply by the number of supported versions and a two-year-old product is comfortably past 5,000 pages.

Should generated pages live in the repository?

Generate them into a build directory rather than committing them. Committed generated pages create enormous diffs, hide real content changes in review, and drift from the spec whenever someone edits the output by hand.

How do I handle multiple API versions?

Treat the version as a path segment and the spec as an input, then generate each version from its own spec file. Keep old versions as static output that is regenerated only when their spec changes, so publishing a new version does not risk the old ones.

Does client-side search work at this scale?

Up to a point. A prebuilt index over 5,000 pages is several megabytes, which is too much to ship. Either index only titles and summaries, split the index by version, or move search to a hosted service — the decision usually arrives sooner than teams expect.