@blaaiz/docs-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +103 -0
  3. package/dist/chunk-3ZX4WIE3.js +2984 -0
  4. package/dist/chunk-3ZX4WIE3.js.map +1 -0
  5. package/dist/chunk-JCYR6RPE.js +31 -0
  6. package/dist/chunk-JCYR6RPE.js.map +1 -0
  7. package/dist/chunk-ZKOOKLZ3.js +124 -0
  8. package/dist/chunk-ZKOOKLZ3.js.map +1 -0
  9. package/dist/cli.js +534 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/generator.cjs +508 -0
  12. package/dist/generator.cjs.map +1 -0
  13. package/dist/generator.d.cts +122 -0
  14. package/dist/generator.d.ts +122 -0
  15. package/dist/generator.js +177 -0
  16. package/dist/generator.js.map +1 -0
  17. package/dist/index.cjs +3024 -0
  18. package/dist/index.cjs.map +1 -0
  19. package/dist/index.d.cts +1182 -0
  20. package/dist/index.d.ts +1182 -0
  21. package/dist/index.js +3 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/navigation-CGqFIPlP.d.cts +498 -0
  24. package/dist/navigation-CGqFIPlP.d.ts +498 -0
  25. package/dist/openapi-types-CJ6p5Cux.d.cts +78 -0
  26. package/dist/openapi-types-CJ6p5Cux.d.ts +78 -0
  27. package/dist/ui/api-try-it.cjs +654 -0
  28. package/dist/ui/api-try-it.cjs.map +1 -0
  29. package/dist/ui/api-try-it.d.cts +78 -0
  30. package/dist/ui/api-try-it.d.ts +78 -0
  31. package/dist/ui/api-try-it.js +509 -0
  32. package/dist/ui/api-try-it.js.map +1 -0
  33. package/dist/ui/ask-ai.cjs +810 -0
  34. package/dist/ui/ask-ai.cjs.map +1 -0
  35. package/dist/ui/ask-ai.d.cts +57 -0
  36. package/dist/ui/ask-ai.d.ts +57 -0
  37. package/dist/ui/ask-ai.js +808 -0
  38. package/dist/ui/ask-ai.js.map +1 -0
  39. package/dist/ui/copy-page.cjs +312 -0
  40. package/dist/ui/copy-page.cjs.map +1 -0
  41. package/dist/ui/copy-page.d.cts +33 -0
  42. package/dist/ui/copy-page.d.ts +33 -0
  43. package/dist/ui/copy-page.js +183 -0
  44. package/dist/ui/copy-page.js.map +1 -0
  45. package/dist/ui/mermaid.cjs +363 -0
  46. package/dist/ui/mermaid.cjs.map +1 -0
  47. package/dist/ui/mermaid.d.cts +13 -0
  48. package/dist/ui/mermaid.d.ts +13 -0
  49. package/dist/ui/mermaid.js +361 -0
  50. package/dist/ui/mermaid.js.map +1 -0
  51. package/dist/ui.cjs +661 -0
  52. package/dist/ui.cjs.map +1 -0
  53. package/dist/ui.d.cts +428 -0
  54. package/dist/ui.d.ts +428 -0
  55. package/dist/ui.js +537 -0
  56. package/dist/ui.js.map +1 -0
  57. package/package.json +145 -0
  58. package/patches/fumadocs-openapi.patch +173 -0
  59. package/skills/AGENTS-section.md +36 -0
  60. package/skills/SKILL.md +363 -0
  61. package/styles/api-reference.css +1417 -0
  62. package/styles/ask-ai.css +563 -0
  63. package/styles/auth.css +462 -0
  64. package/styles/docs.css +247 -0
  65. package/styles/home.css +376 -0
  66. package/templates/init/content/docs/index.mdx.tmpl +52 -0
  67. package/templates/init/content/docs/meta.json.tmpl +3 -0
  68. package/templates/init/content/docs.json.tmpl +12 -0
  69. package/templates/init/content/nav.json.tmpl +7 -0
  70. package/templates/init/docs.config.ts.tmpl +36 -0
  71. package/templates/init/env.example.tmpl +15 -0
@@ -0,0 +1,363 @@
1
+ ---
2
+ name: docs-core
3
+ description: Work on a documentation site built with @blaaiz/docs-core. Use whenever a task touches docs.config.ts, content/docs.json, content/docs/**, or an import from @blaaiz/docs-core — adding or reordering doc pages, tabs, and groups; publishing an OpenAPI endpoint with the try-it playground; changing theme colours, the logo, or the favicon; setting up public or workspace authentication; turning on Ask AI, copy-page, search, or SEO; or fixing an error the framework raises.
4
+ ---
5
+
6
+ # docs-core
7
+
8
+ `@blaaiz/docs-core` is a documentation framework that wraps Fumadocs and adds the
9
+ parts every docs site repeats: navigation, theming, access control, an OpenAPI
10
+ reference with a try-it playground, LLM-ready page export, search, SEO, and Ask
11
+ AI. It has zero runtime dependencies.
12
+
13
+ **The golden rule: the core owns the logic, the site owns config and content.**
14
+ If a thing is the same for every organization, it is inside the package. If it
15
+ changes per organization, it is in the site.
16
+
17
+ So: **never edit anything under `node_modules/@blaaiz/docs-core`.** Every change
18
+ you make lands in `docs.config.ts`, `content/docs.json`, a content file, or one
19
+ of the site's own wiring files. A change that seems to need a package edit is
20
+ either a config field you have not found yet, or a framework change that belongs
21
+ in the `docs-core` repository — say so instead of patching the install.
22
+
23
+ ## The files that matter
24
+
25
+ | File | Owned by | Edit it to |
26
+ | ----------------------------------- | ------------- | ------------------------------------------------------------------------------ |
27
+ | `docs.config.ts` | the site | Change the brand, the access mode, the proxy allow-list, features, SEO, Ask AI |
28
+ | `content/docs.json` | the site | Change what is published and in what order: tabs, groups, pages, API endpoints |
29
+ | `content/docs/**/*.mdx` | the site | Write or edit prose |
30
+ | `content/**/*-openapi.json` | the site | Add or change an API endpoint's spec |
31
+ | `content/docs/**/meta.json` | see below | Sidebar order |
32
+ | `content/nav.json` | see below | Top-navigation links, one per tab |
33
+ | `content/api-methods.json` | the generator | Sidebar method badges — never by hand |
34
+ | `content/docs/api/**` | the generator | Generated endpoint pages — never by hand |
35
+ | `app/**`, `lib/**`, `middleware.ts` | the site | Wiring: routes, layouts, the page tree |
36
+
37
+ `meta.json` and `nav.json` are hand-written on a prose-only site. On a site with
38
+ an API reference the generator writes them from `docs.json` on every run, so
39
+ hand edits are lost — change `docs.json` instead. A site runs the generator when
40
+ its `package.json` has a `predev` / `prebuild` script calling
41
+ `@blaaiz/docs-core/generator`; check that first.
42
+
43
+ ## docs.config.ts
44
+
45
+ One call to `defineDocsConfig`. It returns its argument unchanged; it exists for
46
+ type-checking and completion.
47
+
48
+ ```ts
49
+ import { defineDocsConfig } from '@blaaiz/docs-core';
50
+
51
+ export default defineDocsConfig({
52
+ theme: { name: 'Example Docs', colors: { primary: '#4c63f5' } },
53
+ auth: 'public',
54
+ proxy: { allowedOrigins: [] },
55
+ });
56
+ ```
57
+
58
+ `theme`, `auth`, and `proxy` are required. `features`, `seo`, and `ai` are
59
+ optional, and omitting one leaves everything it controls off.
60
+
61
+ ### theme
62
+
63
+ | Field | Type | Default | Notes |
64
+ | ------------ | ----------------- | ------------------ | --------------------------------------------------- |
65
+ | `name` | string | — | Required. Display name, and the base for SEO titles |
66
+ | `logo` | `{ light, dark }` | none | Image paths the site serves |
67
+ | `favicon` | string | none | Path the site serves |
68
+ | `colors` | palette | framework defaults | Light theme, and the base for dark |
69
+ | `darkColors` | palette | inherits `colors` | Dark-theme overrides only |
70
+
71
+ A palette accepts `primary`, `background`, `foreground`, `card`, `border`, and
72
+ `muted`. Every token is optional. `buildThemeCss(config.theme)` turns the palette
73
+ into CSS variables; the site's root layout injects the result.
74
+
75
+ ### auth
76
+
77
+ `'public'` or a workspace block. Public mode needs no middleware, no sign-in
78
+ page, and no auth routes.
79
+
80
+ | Field | Type | Default | Notes |
81
+ | ------------------- | ------------- | --------------------- | ---------------------------------- |
82
+ | `mode` | `'workspace'` | — | Required |
83
+ | `allowedDomains` | string[] | — | Email domains permitted to sign in |
84
+ | `providers` | object | — | Any combination of the three below |
85
+ | `signInPath` | string | `/signin` | Where the gate redirects |
86
+ | `sessionSecretEnv` | string | `DOCS_SESSION_SECRET` | Env var holding the signing key |
87
+ | `sessionTtlSeconds` | number | 8 hours | Session lifetime |
88
+ | `secureCookies` | boolean | `true` | Set `false` only for local http |
89
+
90
+ Providers:
91
+
92
+ | Provider | Shape | Env vars |
93
+ | -------- | --------------------------------------------------- | ------------------------------------------ |
94
+ | `email` | `true` | — |
95
+ | `secret` | `{ secretEnv? }` | `DOCS_ACCESS_SECRET` |
96
+ | `google` | `{ clientIdEnv?, clientSecretEnv?, redirectPath? }` | `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` |
97
+
98
+ `secret` supersedes `email` when both are enabled: offering email-only as well
99
+ would let a reader skip the secret. `google`'s `redirectPath` defaults to
100
+ `/api/auth/google/callback`.
101
+
102
+ ### proxy
103
+
104
+ | Field | Type | Notes |
105
+ | -------------------- | -------- | -------------------------------------------------------------------------------------------------------------------- |
106
+ | `allowedOrigins` | string[] | Security-critical. The exhaustive list of upstream origins the try-it playground may reach. Anything else is refused |
107
+ | `rateLimitPerMinute` | number | Forwards per minute per client IP. Default 60. Enforced by both proxy factories |
108
+
109
+ The proxy forwards the reader's own credentials and injects none of its own. It
110
+ also refuses loopback, private, and link-local hosts, so it cannot be used as an
111
+ SSRF relay. An empty `allowedOrigins` is correct for a site with no API
112
+ reference.
113
+
114
+ Pass the limit through when you mount the route, or the default of 60 applies:
115
+
116
+ ```ts
117
+ export const { GET, POST, PUT, PATCH, DELETE, OPTIONS } = createTryItProxyRoute({
118
+ allowedOrigins: config.proxy.allowedOrigins,
119
+ rateLimitPerMinute: config.proxy.rateLimitPerMinute,
120
+ });
121
+ ```
122
+
123
+ A caller over the limit gets `429`, a `Retry-After` header, and the body
124
+ `{ "error": "Too many requests. Try again in 60 seconds.", "retryAfterSeconds": 60 }`.
125
+ The key is the client IP: the first entry of `x-forwarded-for`, then
126
+ `x-real-ip`, then one shared bucket. The window is in memory per server process.
127
+
128
+ ### features
129
+
130
+ | Field | Default | Effect |
131
+ | ---------- | ------- | ------------------------------------------------------------------------------ |
132
+ | `copyPage` | `false` | Shows the "Copy page / View as Markdown" control and serves the Markdown route |
133
+
134
+ ### seo
135
+
136
+ | Field | Default | Notes |
137
+ | --------------- | --------------------- | ------------------------------------------------------------------------ |
138
+ | `siteUrl` | none | Canonical origin. Without it, canonical and Open Graph URLs are relative |
139
+ | `titleTemplate` | `'%s · <theme.name>'` | `%s` is the page title. The home title is `theme.name` verbatim |
140
+ | `description` | none | Used where a page declares none |
141
+ | `ogImage` | none | Default sharing image |
142
+ | `twitter` | none | `@handle` for card attribution |
143
+ | `keywords` | none | Applied site-wide |
144
+ | `noindex` | `false` | Set `true` on a private or pre-launch site |
145
+
146
+ ### ai
147
+
148
+ Naming a provider is a complete setup. Every other field has a default.
149
+
150
+ | Field | Default | Notes |
151
+ | -------------------- | -------------------------------------- | -------------------------------------------- |
152
+ | `provider` | — | Required: `'anthropic'` or `'openai'` |
153
+ | `model` | `claude-sonnet-5` / `gpt-5-mini` | Validated against the provider's allow-list |
154
+ | `apiKeyEnv` | `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` | The env var, never the key |
155
+ | `rateLimitPerMinute` | `10` | Per client IP, 1–600. Always enforced |
156
+ | `maxContextPages` | `6` | Pages retrieved per question, 1–20 |
157
+ | `systemPrompt` | none | Extra instructions, 4000 characters or fewer |
158
+
159
+ Allowed models:
160
+
161
+ - **anthropic** — `claude-sonnet-5`, `claude-haiku-4-5`, `claude-opus-5`,
162
+ `claude-opus-4-8`, `claude-sonnet-4-6`
163
+ - **openai** — `gpt-5-mini`, `gpt-5`, `gpt-5.2`, `gpt-5-nano`, `gpt-4.1`,
164
+ `gpt-4.1-mini`, `gpt-4o`, `gpt-4o-mini`
165
+
166
+ The whole `ai` block is validated when the route is built, so a typo fails the
167
+ build rather than a reader's question. An API key never belongs in this file.
168
+
169
+ ## content/docs.json
170
+
171
+ The navigation, and the only thing that decides what is published.
172
+
173
+ ```json
174
+ {
175
+ "name": "Example Docs",
176
+ "navigation": {
177
+ "tabs": [
178
+ { "tab": "Home", "icon": "house", "pages": ["index", "quickstart"] },
179
+ {
180
+ "tab": "Guides",
181
+ "icon": "book",
182
+ "groups": [
183
+ { "group": "Getting started", "pages": ["guides/install", "guides/config"] },
184
+ { "group": "Advanced", "pages": ["guides/webhooks"] }
185
+ ]
186
+ },
187
+ {
188
+ "tab": "API reference",
189
+ "icon": "code",
190
+ "groups": [
191
+ {
192
+ "group": "Authentication",
193
+ "pages": [
194
+ "api-reference/user/auth/login-openapi.json POST /api/user/login",
195
+ "api-reference/user/auth/register-openapi.json POST /api/user/register"
196
+ ]
197
+ }
198
+ ]
199
+ }
200
+ ]
201
+ }
202
+ }
203
+ ```
204
+
205
+ Rules the parser enforces:
206
+
207
+ - A tab needs a string `tab` title. It carries either `pages` or `groups`, never
208
+ both — `groups` wins if both appear.
209
+ - A group needs a string `group` title and a `pages` array. Groups nest: a
210
+ `pages` array may hold group objects.
211
+ - A page string is either a content path relative to `content/docs`, without the
212
+ `.mdx` extension, or an OpenAPI entry shaped `<file> <METHOD> <route>`.
213
+ - `icon` is optional on a tab, and the site resolves the name through `docsIcon`.
214
+ - Order in the file is order in the sidebar.
215
+
216
+ ## Task recipes
217
+
218
+ ### Add a doc page
219
+
220
+ 1. Create `content/docs/<path>.mdx` with a frontmatter `title` and `description`.
221
+ 2. Add `"<path>"` to the right `pages` array in `content/docs.json`, in the
222
+ position you want it.
223
+ 3. On a prose-only site, add the same entry to the folder's `meta.json` `pages`
224
+ array. On an API site, re-run the generator instead.
225
+ 4. Run the typecheck and the build.
226
+
227
+ ### Add a tab
228
+
229
+ 1. Add a tab object to `navigation.tabs` in `content/docs.json`, with `tab`,
230
+ `icon`, and either `pages` or `groups`.
231
+ 2. Create every page the tab lists.
232
+ 3. Add the tab's folder to the root `content/docs/meta.json` `pages` array, and
233
+ give the folder its own `meta.json` with `"root": true`, a `title`, and an
234
+ `icon`. On an API site, re-run the generator instead.
235
+ 4. Add the tab to `content/nav.json` — `{ text, url, icon }`, pointing at its
236
+ first page. The generator writes this file on an API site.
237
+
238
+ ### Add an API endpoint
239
+
240
+ 1. Create one OpenAPI file holding one operation. The name must end in
241
+ `-openapi.json`. Give the operation an `operationId`: it becomes the page
242
+ slug. Without one, the slug is derived from `METHOD path`.
243
+ 2. Add the entry to `content/docs.json` in the group where it belongs:
244
+ `"<file> <METHOD> <route>"`, with the file path relative to `content`.
245
+ 3. Re-run the generator: `pnpm generate:api`, or whatever script calls
246
+ `generateDocsTree`. It merges the specs, writes one page per declared
247
+ endpoint into its group folder, and rewrites `meta.json`, `nav.json`, and
248
+ `api-methods.json`.
249
+ 4. Check the page renders and the try-it panel targets an allow-listed origin.
250
+
251
+ An operation that exists in a spec but is not declared in `docs.json` gets no
252
+ page. The navigation decides what is published. The API folder is deleted and
253
+ rebuilt on every run, so a removed endpoint leaves no stale page.
254
+
255
+ ### Change theme colours
256
+
257
+ Edit `theme.colors` and `theme.darkColors` in `docs.config.ts`. Nothing else:
258
+ the site's layout already turns the palette into CSS variables. Set `primary` in
259
+ both palettes — a light-theme accent rarely has enough contrast on a dark
260
+ background.
261
+
262
+ ### Enable Ask AI
263
+
264
+ 1. Add one line to `docs.config.ts`: `ai: { provider: 'anthropic' }`.
265
+ 2. Set `ANTHROPIC_API_KEY` (or `OPENAI_API_KEY`) in the server environment. Never
266
+ in `docs.config.ts`, never in a `NEXT_PUBLIC_` variable.
267
+ 3. Mount the route. It needs the Node runtime, because the corpus is read from
268
+ the MDX files on disk:
269
+
270
+ ```ts
271
+ // app/api/ai/route.ts
272
+ import { createAskAiRoute } from '@blaaiz/docs-core';
273
+ import config from '@/docs.config';
274
+ import { askAiDocuments, loadAskAiPageMarkdown } from '@/lib/ask-ai';
275
+
276
+ export const runtime = 'nodejs';
277
+
278
+ export const { POST } = createAskAiRoute({
279
+ config: config.ai!,
280
+ siteName: config.theme.name,
281
+ documents: askAiDocuments,
282
+ loadPageMarkdown: loadAskAiPageMarkdown,
283
+ });
284
+ ```
285
+
286
+ 4. Mount the panel once in the docs layout, and import its stylesheet in
287
+ `app/globals.css`:
288
+
289
+ ```tsx
290
+ import { AskAi } from '@blaaiz/docs-core/ui/ask-ai';
291
+ // <AskAi endpoint="/api/ai" /> — endpoint defaults to /api/ai
292
+ ```
293
+
294
+ ```css
295
+ @import '@blaaiz/docs-core/styles/ask-ai.css';
296
+ ```
297
+
298
+ `AskAi` accepts `endpoint`, `suggestions`, `label`, and `placeholder`, and all
299
+ four have defaults.
300
+
301
+ ### Enable copy page
302
+
303
+ 1. Set `features: { copyPage: true }` in `docs.config.ts`.
304
+ 2. Render `CopyPageButton` from `@blaaiz/docs-core/ui/copy-page` in the page
305
+ component, guarded by `config.features?.copyPage`.
306
+ 3. Serve the Markdown: `createMarkdownRoute({ read })` in
307
+ `app/md/[[...slug]]/route.ts`, where `read` returns a page's raw MDX.
308
+
309
+ ### Set up workspace auth with the shared secret
310
+
311
+ 1. In `docs.config.ts`:
312
+
313
+ ```ts
314
+ auth: {
315
+ mode: 'workspace',
316
+ allowedDomains: ['example.com'],
317
+ providers: { secret: { secretEnv: 'DOCS_ACCESS_SECRET' } },
318
+ }
319
+ ```
320
+
321
+ 2. Set `DOCS_SESSION_SECRET` (a long random value — `openssl rand -hex 32`) and
322
+ `DOCS_ACCESS_SECRET` in the server environment.
323
+ 3. Mount `createSecretSignIn` on a route, and `createAuthGate` in
324
+ `middleware.ts`. Import `@blaaiz/docs-core/styles/auth.css` for `AuthScreen`.
325
+ 4. Check the middleware matcher covers every route that must be gated. A matcher
326
+ that misses a route leaves that route open.
327
+ 5. Consider `seo: { noindex: true }` on a gated site.
328
+
329
+ ## Verification habits
330
+
331
+ - Run the site's `typecheck` and `build` after every change. A config mistake
332
+ usually fails at build time by design.
333
+ - After an API change, re-run the generator and confirm the page count it
334
+ reports matches the endpoints you declared.
335
+ - Never hand-edit `content/docs/api/**`, `content/api-methods.json`, or — on a
336
+ site that runs the generator — `meta.json` and `nav.json`. They are outputs.
337
+ - Check a content change in the browser at `/docs`: sidebar order, the top nav,
338
+ and search.
339
+ - A palette change needs a look in both light and dark mode.
340
+
341
+ ## Troubleshooting
342
+
343
+ Each message below is raised verbatim by the framework.
344
+
345
+ | Message | Cause and fix |
346
+ | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
347
+ | `ai.provider must be one of 'anthropic', 'openai', got '…'.` | Typo in `docs.config.ts`. Use one of the two names |
348
+ | `ai.model must be one of '…' for provider '…', got '…'.` | The model is not on that provider's allow-list. Use a listed id, or leave `model` out for the default |
349
+ | `Ask AI is not configured: set the ANTHROPIC_API_KEY environment variable on the server.` | The route is mounted but the key is missing. Set the env var named by `ai.apiKeyEnv` where the server runs — not in `docs.config.ts` |
350
+ | `Target origin is not allow-listed: https://api.example.com` | The try-it request targets an origin missing from `proxy.allowedOrigins`. Add it, exactly, scheme included |
351
+ | `Blocked host: localhost` | The proxy refuses loopback, private, and link-local hosts on purpose. Point the playground at a reachable public origin |
352
+ | `docs.json navigation must have a "tabs" array.` | `content/docs.json` is missing `navigation.tabs`. The older Mintlify `navigation` array form is not supported |
353
+ | `Navigation entry at navigation.tabs[N].pages[M] must be a page string or a group object.` | That entry is neither a string nor an object with a `group` key. A common cause is `{ "page": "…" }` |
354
+ | `Group at <path> is missing a string "group" title.` | Add the `group` title, or move a bare string into `pages` |
355
+ | `Duplicate operation POST /api/x across inputs.` | Two OpenAPI files define the same method on the same route. The merge refuses to overwrite silently — delete one |
356
+ | `Each OpenAPI input must have a "paths" object.` | One spec file is not a real OpenAPI document. Even a one-operation file needs `paths` |
357
+ | `[docs-core] docs.json references POST /api/x, but <file> defines no such operation.` | The `docs.json` entry and the spec disagree on method or route. Fix whichever is wrong |
358
+
359
+ Error classes are exported, so a site can branch on them:
360
+ `InvalidNavigationError`, `OpenApiMergeError`, `ProxyTargetError`, and
361
+ `AiConfigError`.
362
+
363
+ Full documentation: <https://docs-core.blaaiz.dev>