# Welcome
## Why GraphStaff
GraphStaff turns a folder of MDX and one `docs.json` into a fast, searchable, AI-answerable documentation site.
Static first
The whole site — pages, navigation, and keyword search — works with no server and no model key. Drop `dist/` on any static host.
One model seam
Model choice lives in one `models.toml` file. Swap the model by editing one line; any OpenAI-compatible endpoint works.
Agentic retrieval
Ask AI searches, greps, and reads the actual pages before answering — grounded, cited answers with no vector index to build or maintain.
## Get started
Follow the [quickstart](/quickstart), explore the [built-in components](/writing/components), or open [Ask AI](/ask) and describe what you want to build.
# Quickstart
## Create a site
Scaffold a docs directory
Create a folder with a `docs.json` and an MDX page:
```sh
mkdir my-docs && cd my-docs
```
Describe your site in docs.json
```json
{
"name": "My Docs",
"colors": { "primary": "#4f46e5" },
"navigation": {
"groups": [{ "group": "Start", "pages": ["index"] }]
}
}
```
Run the dev server
```sh
graphstaff dev
```
`graphstaff dev` validates `docs.json` on every change and reports schema errors with the exact path of the offending key.
## What you get
| Capability | Needs a server? |
| --- | --- |
| Pages, navigation, theming | No |
| Command-K keyword search (Pagefind) | No |
| llms.txt and per-page Markdown twins | No |
| Ask AI (Command-I) | Yes — a server route plus a model API key |
# Installation
## Requirements
- Node.js 22 or newer, with pnpm
- A model API key (OpenRouter by default) — only if you enable the Ask AI assistant
## Install
```sh
pnpm install
pnpm build
```
```sh
npm install
npm run build
```
## Enabling Ask AI
The assistant runs inside the docs server itself: an agentic loop that searches, greps, and reads your pages before answering. Enabling it takes one config file and one environment variable.
`models.toml` sits next to `docs.json` and names the model; the key comes from the environment:
```sh
export OPENROUTER_API_KEY=sk-or-...
graphstaff dev
```
The model API key lives only in the server's environment. It never appears in `docs.json`, the built site, or the browser.
Deploying without Ask AI
Skip the key entirely. The site builds to a static `dist/` folder that works on any static host; the Ask AI modal shows a plain "not configured" notice and keyword search covers lookup.
# Public status
Public status
All public systems are operational.
# Writing pages
Every source page is an `.md` or `.mdx` file under the `pages/` directory with a small frontmatter block:
```mdx
---
title: Quickstart
description: From zero to a running site in five minutes.
---
Your content starts here.
```
## Create the page file
Create the file itself under `pages/` before adding it to navigation. For example, create `pages/guides/setup.mdx` for a guide whose navigation path will be `guides/setup`.
```mdx
---
title: Setup
description: Configure the project for local development.
---
Follow these steps to configure the project.
```
## Navigation
After creating the page file, add its path to `docs.json` so it appears in the sidebar:
```json
{
"navigation": {
"groups": [
{ "group": "Getting started", "pages": ["index", "quickstart"] }
]
}
}
```
The `pages` entries are paths relative to the `pages/` directory, without the `.md` or `.mdx` extension. For example, `pages/guides/setup.mdx` becomes `guides/setup`. Order in the array is order in the sidebar.
### Header tabs
Larger sites can split the navigation into top-level spaces — the header tabs you see on this site. Replace `groups` with `tabs`, where each tab holds its own groups and gets its own sidebar:
```json
{
"navigation": {
"tabs": [
{
"tab": "Guides",
"groups": [
{ "group": "Getting started", "pages": ["welcome", "quickstart"] }
]
},
{
"tab": "API Reference",
"groups": [
{
"group": "API reference",
"pages": ["api-reference/overview"],
"openapi": "api-reference/openapi.yaml"
}
]
}
]
}
}
```
A tab links to its first page. The sidebar, search filtering, and previous/next links all scope to the active tab; the AI assistant still answers from the whole site.
## Frontmatter fields
The page title, shown in the sidebar, the browser tab, and search results.
A one-sentence summary used for search-engine metadata and the AI assistant's context.
# Components
GraphStaff auto-imports its component library into every MDX page. This page demonstrates each one.
## Callouts
A note for neutral, supplementary information.
An info callout for background context.
A tip for best practices and shortcuts.
A warning for things that can go wrong.
A check for success states and confirmations.
A danger callout for destructive or irreversible actions.
```mdx
Use notes for neutral, supplementary information.
Use info callouts for background context.
Use tips for best practices and shortcuts.
Back up your database before running this command.
The deployment completed successfully.
This action permanently deletes the project.
```
## Cards and columns
[Card with icon](/quickstart)
Cards link to pages and take an optional icon by name.
Plain card
Cards also work without an icon or link.
```mdx
Build your first documentation site.
Browse the built-in component library.
```
## Tabs
macOS
Install with Homebrew: `brew install graphstaff`
Linux
Install with the shell script: `curl -fsSL https://example.com/install.sh | sh`
Windows
Install with winget: `winget install graphstaff`
```mdx
Run `brew install graphstaff`.
Run the shell installer.
```
## Code groups
```sh
pnpm add @graphstaff/ai-client
```
```sh
npm install @graphstaff/ai-client
```
````mdx
```sh title="pnpm"
pnpm add @graphstaff/ai-client
```
```sh title="npm"
npm install @graphstaff/ai-client
```
````
## Steps
First
Steps render an ordered, numbered sequence.
Second
Each step takes a title and arbitrary content.
```mdx
Add an MDX file under `pages/`.
Put its path in `docs.json`.
```
## Accordions
What is an accordion?
A collapsible section for content most readers can skip.
Can they group?
Yes — an AccordionGroup renders a connected stack.
```mdx
A collapsible section for optional details.
Yes. Wrap them in `AccordionGroup`.
```
## Expandable and Frame
Show the details
Expandable is the inline single-item variant of an accordion.
Frames wrap images and give them a caption.
Image: [Placeholder banner](https://placehold.co/1200x400/4f46e5/white?text=GraphStaff)
```mdx
Put optional details here.
```
## Video
Video: [Flower blooming](https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4)
A short CC0 video used to demonstrate the responsive player.
Add a written transcript or detailed summary here so Ask AI can answer
questions about information that appears only in the video.
```mdx
```
## API documentation fields
The search query. `ParamField` documents request parameters.
The synthesized answer. `ResponseField` documents response fields.
```mdx
The search query.
```
```mdx
The synthesized answer.
```
# Theming
Everything in the GraphStaff theme lives in `docs.json`. Set the brand color, preset, appearance, fonts, styling, and logo variants there; no custom CSS is required.
## Colors
`colors.primary` drives the derived OKLCH color ramps used across the site. Use `colors.light` and `colors.dark` when you want to override the accent color in one mode without changing the full ramp.
```json
{
"colors": {
"primary": "#4f46e5",
"light": "#0f766e",
"dark": "#5eead4"
}
}
```
## Presets
| Preset | Character |
| --- | --- |
| `default` | Balanced defaults for most documentation sites. |
| `minimal` | Flat and editorial, with restrained chrome. |
| `soft` | Rounded and friendly, with gentler surfaces. |
| `bold` | High-contrast and brand-forward. |
## Live preset preview
Default
Minimal
Soft
Bold
Preset sample
GraphStaff docs
Surfaces, navigation states, shadows, radius, and chrome respond to the selected preset.
Surface
Chrome
Accent
This preview applies to this visit only. Set it permanently with "theme" in docs.json. The bold preview is approximate because the higher-chroma primary ramp is generated only when bold is configured in docs.json.
.gs-theme-preview {
display: grid;
gap: 1rem;
margin-block: 1rem 1.5rem;
padding: 1rem;
border: 1px solid var(--gs-border);
border-radius: var(--gs-radius-lg);
background: var(--gs-surface-raised);
box-shadow: var(--gs-shadow-card);
}
.gs-theme-preview__buttons {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.gs-theme-preview button {
min-height: 2.25rem;
padding: 0.45rem 0.75rem;
border: 1px solid var(--gs-border);
border-radius: var(--gs-radius);
background: var(--gs-button-secondary-bg);
color: var(--gs-button-secondary-text);
font: inherit;
line-height: 1;
cursor: pointer;
}
.gs-theme-preview button:hover {
border-color: var(--gs-color-primary);
color: var(--gs-color-primary);
}
.gs-theme-preview button[aria-pressed="true"] {
border-color: var(--gs-button-primary-bg);
background: var(--gs-button-primary-bg);
color: var(--gs-button-primary-text);
box-shadow: 0 0 0 3px color-mix(in oklab, var(--gs-focus-ring) 18%, transparent);
}
.gs-theme-preview__sample {
display: grid;
gap: 0.55rem;
padding: 1rem;
border: 1px solid var(--gs-border);
border-radius: var(--gs-radius);
background: var(--gs-surface);
}
.gs-theme-preview__sample h3,
.gs-theme-preview__sample p,
.gs-theme-preview__note {
margin: 0;
}
.gs-theme-preview__sample h3 {
color: var(--gs-text);
font-size: 1.15rem;
}
.gs-theme-preview__sample p,
.gs-theme-preview__note {
color: var(--gs-text-muted);
}
.gs-theme-preview__eyebrow {
color: var(--gs-color-primary);
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
}
.gs-theme-preview__chips {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
}
.gs-theme-preview__chips span {
padding: 0.3rem 0.5rem;
border: 1px solid var(--gs-nav-active-border);
border-radius: var(--gs-radius-sm);
background: var(--gs-nav-active-bg);
color: var(--gs-nav-active-text);
font-size: 0.82rem;
}
(function () {
var presets = ["default", "minimal", "soft", "bold"];
function updatePreview(preview) {
var current = document.documentElement.dataset.gsPreset || "default";
preview.querySelectorAll("[data-gs-theme-preset]").forEach(function (button) {
var active = button.getAttribute("data-gs-theme-preset") === current;
button.setAttribute("aria-pressed", active ? "true" : "false");
});
}
function bindPreview(preview) {
if (preview.dataset.gsThemePreviewBound !== "true") {
preview.dataset.gsThemePreviewBound = "true";
preview.addEventListener("click", function (event) {
var target = event.target;
if (!(target instanceof Element)) return;
var button = target.closest("[data-gs-theme-preset]");
if (!button || !preview.contains(button)) return;
var preset = button.getAttribute("data-gs-theme-preset");
if (!preset || presets.indexOf(preset) === -1) return;
document.documentElement.dataset.gsPreset = preset;
updatePreview(preview);
});
}
updatePreview(preview);
}
function initThemePreviews() {
document.querySelectorAll("[data-gs-theme-preview]").forEach(function (preview) {
if (preview instanceof HTMLElement) bindPreview(preview);
});
}
window.__graphstaffThemePreviewInit = initThemePreviews;
if (window.__graphstaffThemePreviewBound !== true) {
window.__graphstaffThemePreviewBound = true;
document.addEventListener("astro:page-load", function () {
if (typeof window.__graphstaffThemePreviewInit === "function") {
window.__graphstaffThemePreviewInit();
}
});
}
initThemePreviews();
})();
Set a preset permanently with the top-level `theme` field:
```json
{
"theme": "soft"
}
```
## Appearance
`appearance.default` chooses the first mode: `"light"`, `"dark"`, or `"system"`. Set `appearance.strict` to `true` to pin one mode and hide the theme toggle.
```json
{
"appearance": {
"default": "dark",
"strict": true
}
}
```
## Fonts
Use a string for a Google Fonts family. Use the object form when you need `source: "system"` or explicit `weights`. Family names are normalized and validated before the site builds.
```json
{
"fonts": {
"body": "Inter",
"heading": {
"family": "Inter",
"source": "google",
"weights": [500, 600, 700]
},
"mono": {
"family": "JetBrains Mono",
"source": "system",
"weights": [400, 500]
}
}
}
```
## Styling overrides
`styling.radius` overrides the preset radius scale. `styling.accentStrength` controls how strongly accent color appears in navigation and chrome.
```json
{
"styling": {
"radius": "medium",
"accentStrength": "medium"
}
}
```
## Logo and dark mode
Logos load as `
` elements, so SVGs that rely on `currentColor` render black in dark mode. Ship light and dark logo variants instead.
```json
{
"logo": {
"light": "/src/assets/logo.svg",
"dark": "/src/assets/logo-dark.svg"
}
}
```
# Code blocks
## Basic blocks with titles
```ts
export function greet(name: string): string {
return `Hello, ${name}`;
}
```
## Line highlighting
```ts
const ramp = deriveTokens("#4f46e5");
const css = tokensToCss(ramp); // this line is highlighted
console.log(css);
```
## Grouped samples
Code groups keep one snippet per language or package manager, with synchronized tab selection across the page. Wrap consecutive fenced code blocks in a `CodeGroup` component and give each block a `title`:
```mdx
...one fenced code block per tab, each with a title="..." label...
```
Here is a rendered code group:
```sh
pnpm add @graphstaff/ai-client
```
```sh
npm install @graphstaff/ai-client
```
```sh
yarn add @graphstaff/ai-client
```
# Ask AI
Press Command+I (or Ctrl+I) anywhere in the docs to ask a question.
## The grounding contract
The assistant follows a strict user-experience contract:
1. The answer renders inline and streams token by token.
2. It is grounded strictly in what the assistant found in these pages.
3. It cites real page sections you can click.
4. When nothing relevant is found, it says so instead of guessing.
Retrieval is agentic: the model works with tools — keyword search, grep for exact strings, and full-page reads — in a short capped loop before answering. There are no embeddings and no vector index; the corpus is the documentation itself, so answers are never stale relative to the deployed pages.
## Degrading gracefully
If no model key is configured, the Ask AI modal shows a plain "not configured" notice. Keyword search (Command+K) always works — it runs entirely in the browser.
# Swapping models
Model selection is data, not code. The server reads `models.toml` once at startup; nothing else in the system names a model.
## Swap the answer model
Edit one line
```toml
[models]
synthesis = "anthropic/claude-sonnet-4.5"
```
The value is an OpenRouter model slug by default, so hundreds of models are one edit away.
Set the key
```sh
export OPENROUTER_API_KEY=sk-or-...
```
A different variable name can be configured with `endpoint.api_key_env`.
Restart the server
No feature code changes anywhere.
## Use any OpenAI-compatible endpoint
Point the endpoint at any server that speaks the OpenAI chat-completions API:
```toml
[models]
synthesis = "llama3.1"
[endpoint]
api_base = "http://localhost:11434/v1"
api_key_env = "OLLAMA_API_KEY"
```
The assistant needs a model that handles tool calling well — it drives search, grep, and page-read tools in a loop. Frontier and mid-tier hosted models are reliable; very small local models may struggle.
# API reference
This section is generated from `api-reference/openapi.yaml`. Each operation gets its own page with parameters, request and response schemas, and code samples.
Books API
A small example API — list books, fetch one book, create a book — used to demonstrate the generated reference.
# Changelog
All notable changes to this example project, newest first.
## 2026-07-11
This release refreshed the whole reading experience.
- **Tabbed navigation.** The docs are now organized into Guides, API Reference, and Changelog spaces with their own sidebars — set `navigation.tabs` in `docs.json` to use it on your site.
- **New default look.** Indigo replaces green as the default brand color, the canvas gained a subtle textured gradient, and dark mode is lighter and warmer.
- **API reference upgrades.** Sidebar entries show HTTP method chips, endpoint pages lead with the full request URL, and request samples carry language icons.
- **Ask AI everywhere.** An Ask AI button lives in the header, and the assistant greets you with a centered composer until the conversation starts.
## 2026-07-10
- **Ask AI became an agent.** The assistant holds multi-turn conversations, knows the page you are reading, and can navigate your browser to the page it cites.
- **Theming system.** Presets (`default`, `minimal`, `soft`, `bold`), appearance control, Google or system fonts, and per-mode accent colors — all from `docs.json`.
- **Brand onboarding.** `graphstaff init --from-url` scaffolds a docs project from your existing website's name, color, and logo.
## 2026-07-09
- **Reader authentication.** Password, JWT, and OIDC modes with group-scoped pages — the assistant provably cannot quote gated content to readers without access.
- **Hosted on Cloudflare Workers.** Push-to-deploy publishing with per-pull-request preview URLs.
- **Docs versioning, analytics, and structured logs** with per-answer token costs.