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
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.
| Generator | 5,000 generated pages | Rebuild after one spec change | Practical ceiling |
|---|---|---|---|
| Hugo | 9 s | 9 s (full) | Very high |
| Eleventy | 74 s | 3 s (incremental) | High |
| Astro | 260 s | 6 s (dev server) | Medium |
| Next.js export | 340 s | 8 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.
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.
Putting It Together
| Criterion | Hugo | Eleventy | Astro | Next export |
|---|---|---|---|---|
| Build at 5,000 pages | Excellent | Good | Fair | Fair |
| Schema-driven pages | Data files | Pagination | getStaticPaths + types | generateStaticParams |
| Type-checked spec changes | No | No | Yes | Yes |
| Per-version isolation | Easy | Easy | Easy | Easy |
| Interactive try-it components | Limited | Limited | Native islands | Native |
| Shared design system with the app | Hard | Hard | Yes | Yes |
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.
Related
- Parent: SSG Framework Selection Matrix — the general comparison this specialises.
- SSG Selection Checklist for Engineering Teams — the process around the decision.
- Hugo Build Times for Large Repositories — why Hugo dominates at this scale.
- Handling Dynamic Routes in Next.js Static Export — the export target's constraints.
- Content Collections vs the Eleventy Data Cascade — type checking for schema-driven content.