Open-source, self-hostable documentation framework with first-class OpenAPI, full-text search, and an interactive API playground. A fully open alternative to Scalar and Mintlify.
markline.dev — built with Markline.
npm create @markline@latest my-docs # scaffold a new project
cd my-docs
npm install
npm run dev # preview at http://localhost:3000Or add Markline to an existing project:
npm install @markline/markline
npx markline init # scaffold markline.json + sample content in the current dir
npx markline devYour project holds only content + config:
my-docs/
├── markline.json # navigation, theme, branding, API settings
├── docs/ # your pages (*.mdx)
│ ├── index.mdx
│ └── quickstart.mdx
└── api/
└── openapi.json # rendered as an interactive API reference
| Command | Description |
|---|---|
markline init [dir] |
Scaffold a new docs project. |
markline dev |
Start the dev server against ./markline.json. |
markline build |
Build a production Node server bundle (Docker / Vercel). |
markline start |
Serve a production build. |
markline export |
Build a static HTML site into ./out (any CDN / GitHub Pages / S3). |
markline init scaffolds ready-to-use deploy configs (Dockerfile, netlify.toml,
.github/workflows/deploy.yml).
markline export # → ./outUpload out/ to any static host. The API playground runs in direct-fetch mode (set
api.playground.proxy to "never" if your API lacks CORS).
- GitHub Pages — the scaffolded workflow exports and deploys on push to
main. Project sites are served under/<repo>, so it builds withMARKLINE_BASE_PATH=/<repo>. For a user/org root site or custom domain, remove that env. - Netlify —
netlify.tomlsetscommand = "npm install && npx markline export"andpublish = "out". - Sub-path hosting — set
MARKLINE_BASE_PATH=/prefixso assets and the search bundle resolve correctly.
markline build && markline start # serves on :3000Enables the playground's server-side proxy for APIs without CORS. The scaffolded
Dockerfile builds and serves the site:
docker build -t my-docs . && docker run -p 3000:3000 my-docsConfig is read from markline.json (a docs.json is also accepted, for
Mintlify-style projects). theme.appearance sets the default color scheme —
"dark", "light", or "system" (follows the OS; the default).
topbar.width/topbar.homeWidth— topbar layout:"full"spans edge-to-edge;"contained"centers it to the content max-width.widthgoverns docs + the API reference (default"full");homeWidthgoverns the homepage independently (default"contained", so the marketing nav aligns with the hero).api.playground.mode— the "Try it" experience:"full"(inline param inputs + live console + API Explorer modal),"inline"(Mintlify-style, no modal),"explorer"(read-only docs + console + Explorer modal, Stripe-style), or"off"(static cURL panel).
Opt-in AI assistant for the API reference — a docked chat panel with "Ask about
this section" and source citations. Bring your own key; Markline never ships
one. Omit the ai block (or set enabled: false) and there is no AI route, no
client weight, and no UI — the framework is unchanged.
"ai": {
"enabled": true,
"provider": "openrouter", // openai · openrouter · together · groq · fireworks · local · openai-compatible
"baseUrl": null, // required for "openai-compatible"
"model": "anthropic/claude-3.5-sonnet",
"mode": "proxy", // "proxy" (operator key, server) | "byok" (reader key, browser)
"label": "Ask AI",
"maxTokens": 1024,
"rateLimit": { "perMinute": 10 }
}mode: "proxy"(Node/Docker/Vercel) — the key lives only on the server inMARKLINE_AI_KEY(nevermarkline.json, neverNEXT_PUBLIC_*). Requests go through the built-in/api/airoute, which is dropped in static export.⚠️ A public site with a proxy is an open relay to your paid LLM — the route ships with origin checks and a per-IP rate limit, but enabling proxy mode on a public site spends your budget; tunerateLimitandmaxTokens.mode: "byok"— the reader pastes their own key (stored in their browser) and calls the provider directly. Works on pure-static hosting; the operator spends nothing.- One transport speaks the OpenAI
/chat/completionsshape, so any OpenAI-compatible vendor works via theproviderpreset or abaseUrl.
The reference is generated from api/openapi.json, but you can layer authored
MDX on top — full components (callouts, tables, tabs) included:
api/
├── openapi.json
├── introduction.mdx # replaces the auto API landing page
├── sections/<tag>.mdx # rich summary for an OpenAPI tag/resource
└── operations/<operationId>.mdx # extra prose on a single operation page
sections/<tag>.mdxrenders as the resource intro on the reference landing (Stripe-style), overriding the OpenAPI tagdescription. The filename is the slugified tag (e.g.Payment Methods→payment-methods.mdx).operations/<operationId>.mdxrenders between the endpoint path and the auto-generated parameters on that operation's page.editUrl— adds an "Edit this page" link; the page's content-relative path is appended (e.g.docs/quickstart.mdx).analytics— opt-inplausible,googleAnalytics(measurementId), orposthog(apiKey,apiHost).feedback.endpoint— the "Was this page helpful?" widget POSTs{ answer, reason, comment, path }here (logs to console when unset).
Declare multiple versions to get a version switcher in the topbar. The first
version is the default and is served unprefixed; others are served under their
id and read content from <id>/docs/.
"versions": [
{ "id": "v2", "label": "v2 (latest)" },
{
"id": "v1",
"label": "v1",
"navigation": { "tabs": [ /* hrefs prefixed with /v1 */ ] }
}
]my-docs/
├── docs/ # v2 (default) -> /quickstart
└── v1/
└── docs/ # v1 -> /v1/quickstart
Identical mechanism to versioning, via i18n.locales. The first locale is the
default (unprefixed); others are served under their id with content in
<id>/docs/ and their own (translated) navigation. A topbar language switcher
appears automatically. A project uses versions or locales (not both).
"i18n": {
"locales": [
{ "id": "en", "label": "English" },
{ "id": "es", "label": "Español", "navigation": { "tabs": [ /* /es hrefs */ ] } }
]
}my-docs/
├── docs/ # en (default) -> /quickstart
└── es/
└── docs/ # es -> /es/quickstart
- Config-driven navigation, theming, and branding — no code to fork.
- MDX content with server-side syntax highlighting (Shiki).
- First-class OpenAPI: operation pages, schema tables, sample requests/responses.
- Interactive API playground ("Try it") on every endpoint.
- Static full-text search (Pagefind) — zero search infrastructure.
- Dark mode, table of contents, responsive layout.
Available in any MDX page without an import:
- Callouts —
<Note>,<Info>,<Tip>,<Check>,<Warning>,<Danger> - Layout —
<Card>,<CardGroup cols={2}>,<Steps>/<Step title> - Interactive —
<Tabs>/<Tab title>,<AccordionGroup>/<Accordion title> - API —
<ParamField path|query|header|body type required>,<ResponseField>
<Note>Heads up — this is worth knowing.</Note>
<Steps>
<Step title="Install">Run `npm install @markline/markline`.</Step>
<Step title="Develop">Run `markline dev`.</Step>
</Steps>MIT
{ "name": "My API", "theme": { "logo": { "light": "/logo-light.svg", "dark": "/logo-dark.svg", "text": "My API" }, "colors": { "primary": "#4f46e5", "primaryDark": "#818cf8" }, "appearance": "system" }, "topbar": { "links": [{ "label": "GitHub ↗", "href": "https://github.com/me/repo" }], "cta": { "label": "Get started", "href": "/quickstart" }, "width": "full", // docs + API reference topbar: "full" (edge-to-edge) | "contained" (centered max-width) "homeWidth": "contained" // homepage topbar — independent (defaults to "contained") }, "navigation": { "tabs": [ { "id": "docs", "label": "Documentation", "href": "/", "match": ["__default__"], "groups": [{ "group": "Get started", "pages": [{ "href": "/", "label": "Intro" }] }] }, { "id": "api", "label": "API reference", "href": "/api-reference", "match": ["/api-reference"], "openapi": true } ] }, "api": { "baseUrl": "https://api.example.com", "playground": { "mode": "full", "proxy": "auto" } }, "editUrl": "https://github.com/me/repo/edit/main", "analytics": { "plausible": { "domain": "docs.example.com" } }, "feedback": { "endpoint": "https://example.com/api/docs-feedback" }, "seo": { "title": "My API", "description": "..." } }