unerr-docs
Documentation site for unerr, served at docs.unerr.dev. It is a Next.js 16 App Router app that renders MDX docs with Fumadocs. Pages live in content/docs/**; adding a page means dropping one .mdx file.
Stack
| Part | Version / choice |
|---|---|
| Framework | Next.js 16.2.9 (App Router, Turbopack, output: "standalone") |
| UI | React 19.2.7, Tailwind v4 |
| Docs engine | fumadocs-ui 16.10.2, fumadocs-core 16.10.2, fumadocs-mdx 14.2.11 |
| Language | TypeScript 5.9 |
| Package manager | pnpm@10.26.2 (Corepack), Node >=20.9.0 |
Quick start
corepack enable
pnpm install
pnpm dev
Open http://localhost:3000. predev runs fumadocs-mdx to generate .source/ before the dev server starts.
Other commands:
| Command | Does |
|---|---|
pnpm build |
Production build |
pnpm start |
Run the built app |
pnpm lint |
ESLint |
pnpm typecheck |
fumadocs-mdx && next typegen && tsc --noEmit |
pnpm format / pnpm format:check |
Prettier write / check |
Project layout
| Path | What |
|---|---|
content/docs/** |
MDX doc pages; folder order via meta.json |
app/ |
App Router: (home), docs/[[...slug]], api/search, api/chat, api/health, llms.txt/llms-full.txt/llms.mdx, og/ |
lib/source.ts |
Fumadocs source loader |
lib/site-url.ts |
Runtime base URL (from SITE_URL) |
lib/layout.shared.tsx |
Nav and branding |
components/ |
Shared components |
proxy.ts |
Markdown content negotiation |
source.config.ts |
Fumadocs MDX config |
.source/ |
Generated by fumadocs-mdx (git-ignored, regenerated on install and build) |
Adding a doc page
See CONTRIBUTING.md. Short version: add an .mdx file under content/docs/, set its order in the section's meta.json, run pnpm dev to preview.
Search and Ask-AI
- Search: built-in Orama at
/api/search. No external service. - Ask-AI:
/api/chatstreams from ChatGPT via the Vercel AI SDK +@ai-sdk/openai, usingOPENAI_API_KEY. Optional — when the key is unset, the Ask-AI trigger is hidden and keyword search still works. - Ask-AI abuse/cost guards (
lib/ai/guards.ts): same-origin check, per-IP rate limit (12 req/60s, in-memory), input caps (≤24 messages, ≤12k chars), and an 800-token reply cap. These are the first layer only — they are per-task, so durable cluster-wide limits belong at the ALB / AWS WAF (aws-infra). - Agent-readable docs:
/llms.txt,/llms-full.txt, and per-page.md.
Environment variables
No NEXT_PUBLIC_* vars. They are inlined at build time, so one Docker image could not serve multiple environments. All per-environment config is read at runtime on the server.
| Variable | When | Purpose |
|---|---|---|
SITE_URL |
Runtime | Base URL (read via lib/site-url.ts) |
OPENAI_API_KEY |
Runtime | Ask-AI key; optional. Unset = Ask-AI disabled |
OPENAI_MODEL |
Runtime | ChatGPT model for Ask-AI; injected by ECS |
Deployment
Docker image (multi-stage Alpine, standalone, non-root, tini) → AWS ECR → ECS/Fargate behind an ALB, at docs.unerr.dev. Health check at /api/health.
CI/CD: .github/workflows/ci-cd.yml — test → verify-image on PR → build-push on main → manual deploy-qa / deploy-prod. AWS infra (ECR/ECS/ALB/DNS) lives in the separate aws-infra Terraform repo.
See .internal/IMPLEMENTATION_PLAN.md for the full setup plan and status.