Spectacle

Spectacle

API documentation generator that turns OpenAPI specs into beautiful, fast, static HTML. Three-column layout, auto code samples, zero runtime.

TypeScriptPreactOpenAPIViteShiki

The existing API documentation tools are either ugly or slow. Usually both. Swagger UI looks like a government form. Redoc is better but ships an entire React app to render what should be static HTML. We wanted Stripe-quality docs that generate from a spec file and load instantly. So we rebuilt Spectacle from scratch.

Give it an OpenAPI 3.x or Swagger 2.0 spec and it generates clean, static HTML. Three-column layout with a dark code panel, auto-generated curl/JavaScript/Python examples, Shiki syntax highlighting. No runtime. No JavaScript framework shipping to the browser. The output is HTML and CSS that works with JavaScript disabled.

The pipeline

Most documentation tools parse your spec and render it in one messy pass. Spectacle treats spec processing as a pipeline with distinct stages, each handling one concern:

  YAML/JSON --> LoadedSpec --> ParsedSpec --> OpenAPI 3.x --> NormalizedSpec --> HTML
      |              |              |              |               |              |
    loader        parser        converter      normalizer       renderer      builder
   path/URL    dereference     2.0 -> 3.0      tags, ops       Preact SSG    css + js
               resolve $ref   (passthrough    schemas         renderToString  + assets
               from file dir   if 3.x)        security

The loader accepts file paths, URLs, JSON, or YAML. The parser dereferences all $ref pointers. The converter handles Swagger 2.0 by converting to OpenAPI 3.0. The normalizer transforms raw OpenAPI into a format-agnostic internal representation. The renderer turns that into HTML. Each stage is independently testable and replaceable.

Dereferencing happens before conversion - deliberately. $ref pointers need to resolve against the original file’s directory structure. A relative reference like ./schemas.yaml#/Pet only makes sense from the source file’s location. Convert first and you lose that context. There’s a second reason: swagger2openapi needs circular references broken before it receives input, so the flattened document goes through JSON.parse(JSON.stringify()) before conversion.

The normalizer

The normalizer is the single abstraction layer between OpenAPI’s sprawling format and what the renderer needs. Components never import OpenAPI types - they work with NormalizedSpec, so Swagger 2.0, OpenAPI 3.0, and OpenAPI 3.1 all render through the same code path.

  • Parameter deduplication by (in, name) tuple with operation-level overriding path-level
  • Implicit tag creation for operations referencing undeclared tags
  • Nullable unification - 3.0’s nullable: true, x-nullable, and 3.1’s type: ["string", "null"] collapse to a single boolean
  • Recursive schema building for nested properties, items, and allOf/oneOf/anyOf compositions

The normalizer extracts constraints, it doesn’t validate them. Adding support for a future OpenAPI version means changing the normalizer, not the renderer.

Code samples

Spectacle auto-generates curl, JavaScript, and Python examples at build time. The example generator recurses through schemas with cycle detection, following a priority order: example > default > first enum > const > type-specific fallback. String formats produce intelligent defaults (date-time gives an ISO timestamp, email gives user@example.com). For allOf, examples merge. For oneOf/anyOf, it picks the first option.

All examples are baked into the HTML. Zero client-side overhead.

Rendering

Preact runs renderToString() once at build time. No hydration, no client-side framework. The component tree splits into layout (Page, Sidebar, Head), OpenAPI-specific (Operation, Parameters, Responses, Security), recursive schema rendering (SchemaView, ExampleView with a depth guard at 10 levels), and reusable UI primitives (Badge, CodeBlock, SectionLabel).

The three-column layout is pure CSS flexbox. .doc-copy and .doc-examples split 50/50 at 64em+. The dark panel background is a positioned element - no DOM changes between desktop and mobile. Below the breakpoint, everything stacks.

All client-side interactivity is vanilla JavaScript - sidebar, scroll tracking via IntersectionObserver, globally-synced language tabs, clipboard copy, search, dark mode. Six small modules concatenated in dependency order. No bundler, no framework.

Dev server and theming

File watching with 200ms debounce triggers a full rebuild. Live reload uses Server-Sent Events - simpler than WebSocket, works through proxies. The reload script is injected by string replacement of </body>.

CSS custom properties control everything: colors, fonts, radii, sidebar width, method colors. Override any of them via spectacle.json and the build injects your values as a :root rule. No Sass, no theme API. The dark code panel uses separate variables (--dark-bg, --dark-text) to prevent light-theme bleed.

Usage

npm install -g spectacle-docs

spectacle build your_api.yaml -o docs/    # Build static docs
spectacle dev your_api.yaml               # Dev server with live reload
spectacle validate your_api.yaml          # Validate an OpenAPI spec