This guide covers local development, docs authoring, search indexing, deployment, and the optional AI Assistant.
- Node.js 22.18 or later
- pnpm
pnpm installCopy the example environment file:
cp .env.example .envUpdate .env with the service keys you need for the feature you are working on. Most docs-only changes do not require every optional secret.
Start the development server on http://localhost:3000:
pnpm devpnpm buildPages live as Markdown files under content/. Frontmatter fields are validated by the schema in content.config.ts.
Framework guides live under content/frameworks/<framework>/. The numeric prefix on filenames (01., 02., ...) controls sidebar sort order only. It has no semantic meaning; renumber freely.
The section frontmatter field controls grouping on the /frameworks/<framework> hub page:
section: start-hereappears in the "Start Here" block at the top.section: guidesor unset appears in the "Guides" block below.
Minimal frontmatter for a new framework guide:
---
title: Fetch Data from Directus with Foo
description: Learn how to integrate Directus in your Foo app.
section: start-here
technologies:
- foo
navigation:
title: Data Fetching
---The repository includes scripts that keep docs routes stable when files move and that index the docs into Typesense.
pnpm stable-ids:ensure # Add missing stableId frontmatter
pnpm stable-ids:check # Validate stableId frontmatter
pnpm redirects:sync # Update redirects.json for moved pages
pnpm redirects:check # Check redirect coverage without writing files
pnpm index:docs # Build the search index in Typesense
pnpm typesense:cleanup-preview # Delete stale Typesense preview indexes
pnpm typecheck:scripts # Type check repository scriptsStable IDs give each public docs page a permanent identity. Nuxt Content derives its unique page IDs from file paths, so moving a page changes its built-in ID. Redirect sync compares the current branch to origin/main, so moved pages keep their old URLs working.
CI runs pnpm stable-ids:check and pnpm redirects:check for docs changes.
- New docs page: run
pnpm stable-ids:ensure, then commit the newstableId. - Moved docs page: keep the existing
stableId, runpnpm redirects:sync, then commitredirects.json. - Deleted, split, or merged docs page: run
pnpm redirects:sync, review.docs/redirect-decisions-needed.md, choose target redirects, then re-runpnpm redirects:check. - Before opening a PR: run
pnpm stable-ids:checkandpnpm redirects:check.
Redirect scripts compare against origin/main by default. To check a release branch or another target, fetch it first, then pass --base directly to the script:
git fetch origin release/v13
node scripts/redirects-sync.ts --base origin/release/v13 --no-write --fail-on-unresolved
node scripts/redirects-sync.ts --base origin/release/v13 --write-deterministic --fail-on-unresolvedThe in-site AI Assistant is optional. It is disabled unless OPENROUTER_API_KEY is present and ASSISTANT_ENABLED is not set to false.
OPENROUTER_API_KEY=sk-or-v1-...
ASSISTANT_FP_SECRET=long-random-stringASSISTANT_FP_SECRET salts daily fingerprint identifiers. Use a long random value and keep it secret.
Use one supported Redis env pair for cross-instance burst and daily limits:
UPSTASH_REDIS_REST_URL=https://your-db.upstash.io
UPSTASH_REDIS_REST_TOKEN=your_upstash_tokenor:
KV_REST_API_URL=https://your-db.upstash.io
KV_REST_API_TOKEN=your_upstash_tokenWithout Redis env vars, limits fall back to per-process memory. That is fine for local development, but not enough for production or preview deployments.
AI_MODEL=google/gemini-3.1-flash-lite
ASSISTANT_ENABLED=true
ASSISTANT_FEEDBACK_SURVEY_ID=019e081e-2c3b-0000-04c3-564ad5dff4ed
GITHUB_TOKEN=github_pat_...
POSTHOG_AI_HOST=https://us.i.posthog.comASSISTANT_ENABLED=falseis the kill switch. Set it before build/deploy to hide the assistant and skip the server route; the server also rejects requests when the flag is false.GITHUB_TOKENis required for the assistant's source-code search tool and raises GitHub raw-file rate limits.ASSISTANT_FEEDBACK_SURVEY_IDenables thumbs up/down feedback on assistant responses.POSTHOG_AI_HOSTonly needs to be set when AI telemetry should use a different PostHog host thanPOSTHOG_API_HOST.
Useful assistant tests:
pnpm exec vitest run modules/assistant/index.test.ts modules/assistant/runtime/composables/useAssistant.test.ts modules/assistant/runtime/server/utils/admit.test.ts modules/assistant/runtime/server/utils/abuse-gate.test.ts modules/assistant/runtime/server/utils/bind-tools.test.ts modules/assistant/runtime/server/utils/rate-limit.test.ts modules/assistant/runtime/server/utils/request-context.test.ts server/utils/rate-limit.test.ts server/utils/docs-api-limit.test.tsDev-only helpers:
ASSISTANT_RESET_TOKEN=change-me
RATE_LIMIT_WINDOW_MS=60000
POSTHOG_AI_DEBUG=true
POSTHOG_AI_SELF_TEST=trueThe reset/status endpoints are only registered in dev and only respond from a local development context.
Search is powered by Typesense. The browser palette (UCommandPalette-based) lives at app/components/DocsSearchPalette.vue and queries Typesense directly via app/services/typesenseService.ts. The official typesense npm client is used by the indexer only.
The indexer at scripts/index-docs.ts walks /content, chunks each Markdown page, attaches synonyms, and pushes everything to Typesense. OpenAPI indexing is deferred to a later branch. Run it locally with:
pnpm index:docsCI runs the same command on every push to main (production index) and on every PR commit (per-branch preview index). See .github/workflows/search-index.yml.
Indexes use a blue/green slot pattern with a stable alias:
main-> aliasdirectus-docs, slotsdirectus-docs-a/directus-docs-b- Branch
bry/foo-> aliasdirectus-docs-preview-bry-foo, slots...-a/...-b - Local branch runs use the same branch-derived alias as CI
Each indexer run writes to whichever slot the alias is not currently pointing at, swaps the alias, then deletes the previous slot.
For one-off writes, override the index target with TYPESENSE_INDEX_TARGET=....
The browser reads from TYPESENSE_COLLECTION when set. Otherwise it derives the same branch alias as the indexer. The app reads the alias, never the -a / -b slot name.
PR preview indexes are deleted when same-repo PRs close. The cleanup job deletes the branch alias and both fixed slots:
pnpm typesense:cleanup-preview --branch bry/fooFor one-time cleanup of accumulated preview indexes, run a dry run first:
pnpm typesense:cleanup-preview --stale --dry-run
pnpm typesense:cleanup-preview --staleStale cleanup keeps preview aliases for currently open PR branches and deletes the rest. It requires TYPESENSE_URL, TYPESENSE_PRIVATE_API_KEY, and authenticated gh.
Section boosts and personalization live in buildPersonalizedSortBy in app/composables/useDocsSearch.ts. The same sectionPriority array drives both the Typesense _eval boost order and the chip-bar render order in the palette.
Search synonyms live in server/data/synonyms.ts and are pushed to Typesense on every indexer run. Two formats: multiway (equivalent terms) and oneway (directional shorthand -> canonical, e.g. db -> database). Header comment in the file explains both.
Write H2s and first paragraphs so they work as standalone search results.
The documentation automatically deploys to Vercel when changes are merged into the main branch.
- Open a pull request.
- Review the deploy preview.
- Once the PR is approved and merged to
main, Vercel builds and deploys the updated documentation.