periwinkle
v0.4.0
Published
Static API documentation generator for OpenAPI 3.x — themable, self-contained, no runtime framework
Maintainers
Readme

periwinkle
Static API documentation generator for OpenAPI 3.x — turn a spec plus a small config into a polished, themable, self-contained docs site. Named after the violet-blooming periwinkle flower (Vinca).
Live demo: phranck.github.io/periwinkle — built from a fictional bookstore contract on every push.
- Static output:
index.html, one stylesheet, one small vanilla-JS bundle, and a copy of the spec asopenapi.json. No runtime framework, deployable to any host. - Sticky top navigation with a frosted-glass backdrop: optional brand logo, home link, search, GitHub link, and theme toggle — every affordance toggleable via config.
- Sidebar navigation with endpoint groups, integration guide, endpoint blocks with generated curl examples, schema cards with field tables and raw JSON view.
- Light/dark theming via CSS custom properties, fully configurable (colors, fonts, logo, radius). Pick which palette first-time visitors see, or follow their operating system.
- Document search dialog (
⌘K) and persisted collapsible sections — all progressive enhancement over working plain HTML. - Embeddable React components for host apps (e.g. Astro via
@astrojs/react).
Contents
- Quickstart
- Configuration
- Sidebar icons
- Deploying
- Embedding in an existing app
- CLI
- Projects using periwinkle
- License
Quickstart
npm install --save-dev periwinkle
npx periwinkle build --spec openapi.json --out dist
npx periwinkle preview --dir distThe spec may be JSON or YAML. Broken specs fail the build loudly — periwinkle never produces a silently wrong site.
Configuration
Create a periwinkle.config.ts (or .mts/.js/.mjs) next to your project; it is discovered automatically, or passed explicitly with --config. Every field is optional — an empty config produces a fully working site.
import { defineConfig } from "periwinkle";
export default defineConfig({
spec: "openapi.json",
site: { basePath: "/docs", title: "Example API" },
theme: {
colors: {
light: { accent: "#6667ab" },
dark: { accent: "#9a9bd4" },
},
},
navigation: { logo: "assets/logo.svg", github: { url: "https://github.com/acme/api" } },
});The config covers site identity, the full color palette per mode, fonts, corner radius, the top navigation bar, sidebar affordances, feature switches, typography/layout sizing, animation timing, the integration guide content, custom Markdown chapters, and the footer.
Full reference: every option, its type, default, and where it appears on the page is documented in CONFIGURATION.md.
Interactive builder: click your config together at phranck.github.io/periwinkle/config-builder/ — live preview, import an existing config, copy to clipboard, save file.
The builder is a second page periwinkle can generate, and it is off by default: a published API reference should not ship the tool that authors its own config. Turn it on where it belongs, as the demo does:
features: { configBuilder: true },
Sidebar icons
Endpoint groups take their sidebar icon from the tag name, so a generated reference does not repeat one mark down the whole rail. Shops gets a storefront, Search a magnifier, Users two avatars. Titles that are not covered keep a neutral default, and singular and plural resolve to the same entry unless the icon set offers a real plural counterpart.
The mapping lives in src/render/section-icons.json and covers common API vocabulary out of the box.
To adapt it, use the icon picker: browse or search all 993 Iconsax icons in both the Bulk and TwoTone style, add your own titles, then download the JSON and save it over the mapping file. The page keeps everything in the browser and uploads nothing.
It also ships in this repository as tools/icon-picker.html and runs offline, with no server and no network:
open tools/icon-picker.html # or double-click itAfter upgrading iconsax-react, refresh the tool's icon data:
node tools/extract-icons.mjs
node tools/build-icon-picker.mjsDeploying
The output directory is plain static files. Recipes:
Any static host (nginx, GitHub Pages, …) — upload dist/. With a sub-path (e.g. Pages project sites), set site.basePath accordingly.
GitHub Actions → Pages — see .github/workflows/pages.yml in this repo; it builds the live demo.
Hono / Node backend under /docs:
import { serveStatic } from "@hono/node-server/serve-static";
app.use("/docs/*", serveStatic({ root: "./docs-dist", rewriteRequestPath: (p) => p.replace(/^\/docs/, "") }));Build with site.basePath: "/docs" and serve the directory — no server-side rendering involved.
Embedding in an existing app
The same components that power the CLI are exported for host apps:
import { ApiDocs, prepareDocsData, resolveConfig } from "periwinkle";
import "periwinkle/styles.css";
const data = await prepareDocsData(openApiDocument, resolveConfig({ site: { basePath: "/docs" } }));
// e.g. in Astro with @astrojs/react:
<ApiDocs data={data} />Add periwinkle/client.js as a deferred script for search, collapsing, and the theme toggle, and emit compileThemeCss(config) (it takes the full resolved config, since sizing and motion tokens compile alongside the palette) into a <style> tag placed after the stylesheet link. All interactivity binds via data-pw-* attributes; the markup works without JavaScript.
CLI
periwinkle build [--spec <file>] [--config <file>] [--out <dir>]
periwinkle preview [--dir <dir>] [--port <number>]
periwinkle --version | --helpProjects using periwinkle
| Project | API reference | | --- | --- | | lmaa.space, a curated directory of independent online shops in Europe | api.lmaa.space/docs |
Built your docs with periwinkle? Add yourself to this list. Open a pull request with one more row, or open an issue with your link and it gets added for you. Seeing where periwinkle actually ends up is the most useful feedback this project can get, and it helps the next person decide whether it fits their API.
License
This repository has been published under the MIT license.
