@blaaiz/docs-core 0.1.1 → 0.3.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.
package/skills/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
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.
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
4
  ---
5
5
 
6
6
  # docs-core
@@ -18,7 +18,7 @@ So: **never edit anything under `node_modules/@blaaiz/docs-core`.** Every change
18
18
  you make lands in `docs.config.ts`, `content/docs.json`, a content file, or one
19
19
  of the site's own wiring files. A change that seems to need a package edit is
20
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.
21
+ in the `docs-core` repository. Say so instead of patching the install.
22
22
 
23
23
  ## The files that matter
24
24
 
@@ -30,20 +30,21 @@ in the `docs-core` repository — say so instead of patching the install.
30
30
  | `content/**/*-openapi.json` | the site | Add or change an API endpoint's spec |
31
31
  | `content/docs/**/meta.json` | see below | Sidebar order |
32
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 |
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
35
  | `app/**`, `lib/**`, `middleware.ts` | the site | Wiring: routes, layouts, the page tree |
36
36
 
37
37
  `meta.json` and `nav.json` are hand-written on a prose-only site. On a site with
38
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
39
+ hand edits are lost; change `docs.json` instead. A site runs the generator when
40
40
  its `package.json` has a `predev` / `prebuild` script calling
41
41
  `@blaaiz/docs-core/generator`; check that first.
42
42
 
43
43
  ## docs.config.ts
44
44
 
45
- One call to `defineDocsConfig`. It returns its argument unchanged; it exists for
46
- type-checking and completion.
45
+ One call to `defineDocsConfig`. It validates the whole config and throws
46
+ `DocsConfigError` naming the exact field on a bad value; on success it returns
47
+ the config unchanged.
47
48
 
48
49
  ```ts
49
50
  import { defineDocsConfig } from '@blaaiz/docs-core';
@@ -158,9 +159,9 @@ Naming a provider is a complete setup. Every other field has a default.
158
159
 
159
160
  Allowed models:
160
161
 
161
- - **anthropic** — `claude-sonnet-5`, `claude-haiku-4-5`, `claude-opus-5`,
162
+ - **anthropic**: `claude-sonnet-5`, `claude-haiku-4-5`, `claude-opus-5`,
162
163
  `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
+ - **openai**: `gpt-5-mini`, `gpt-5`, `gpt-5.2`, `gpt-5-nano`, `gpt-4.1`,
164
165
  `gpt-4.1-mini`, `gpt-4o`, `gpt-4o-mini`
165
166
 
166
167
  The whole `ai` block is validated when the route is built, so a typo fails the
@@ -205,7 +206,7 @@ The navigation, and the only thing that decides what is published.
205
206
  Rules the parser enforces:
206
207
 
207
208
  - A tab needs a string `tab` title. It carries either `pages` or `groups`, never
208
- both — `groups` wins if both appear.
209
+ both; `groups` wins if both appear.
209
210
  - A group needs a string `group` title and a `pages` array. Groups nest: a
210
211
  `pages` array may hold group objects.
211
212
  - A page string is either a content path relative to `content/docs`, without the
@@ -232,7 +233,7 @@ Rules the parser enforces:
232
233
  3. Add the tab's folder to the root `content/docs/meta.json` `pages` array, and
233
234
  give the folder its own `meta.json` with `"root": true`, a `title`, and an
234
235
  `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
+ 4. Add the tab to `content/nav.json`: `{ text, url, icon }`, pointing at its
236
237
  first page. The generator writes this file on an API site.
237
238
 
238
239
  ### Add an API endpoint
@@ -252,11 +253,57 @@ An operation that exists in a spec but is not declared in `docs.json` gets no
252
253
  page. The navigation decides what is published. The API folder is deleted and
253
254
  rebuilt on every run, so a removed endpoint leaves no stale page.
254
255
 
256
+ ### Add, change, or remove the landing page
257
+
258
+ The site root (`app/page.tsx`) has two valid shapes, and switching between
259
+ them is a one-file change:
260
+
261
+ - **A landing page**: render the hero inside `HomeLayout` from
262
+ `fumadocs-ui/layouts/home`: the same top bar as the docs (logo, links,
263
+ search, theme toggle) with no sidebar. Hero content is plain elements
264
+ carrying the `home-*` classes from `@blaaiz/docs-core/styles/home.css`.
265
+ Clicking a link enters the sidebar shell under `/docs`. Sites scaffolded by
266
+ `create-docs-core` ship this shape.
267
+ - **No landing**: the file is a three-line `redirect('/docs')` and readers
268
+ land directly in the docs.
269
+
270
+ Do not put the landing inside `content/docs/`: a page there always renders
271
+ in the sidebar shell. For a wide page _inside_ the docs (sidebar kept, no
272
+ title row, no table of contents), set `full: true` in that page's
273
+ frontmatter instead. Mintlify's `mode: custom` frontmatter is not read by
274
+ this stack; `HomeLayout` at the root is its equivalent.
275
+
276
+ ### The top-navigation links
277
+
278
+ Both layouts build their links the same way. Never map `nav.json` by hand
279
+ in a layout:
280
+
281
+ ```tsx
282
+ links={docsNavLinks(navLinks).map((link) => ({
283
+ type: 'custom' as const,
284
+ children: <TopNavLink key={link.url} link={link} />,
285
+ }))}
286
+ ```
287
+
288
+ `docsNavLinks` (from `@blaaiz/docs-core/ui`) owns the policy: it prepends
289
+ Home pointing at the landing (`home: false` on a site without one, or a
290
+ custom entry to change its text or drop its icon), resolves icons (an entry
291
+ without `icon` renders text-only), and computes each link's `match` path:
292
+ exact for Home and the docs root, the section prefix for section links, so a
293
+ section stays lit on every page of its section. `TopNavLink` is a small
294
+ client component the site owns (like `components/logo.tsx`): it calls
295
+ `isDocsNavLinkActive(link, usePathname())` and renders an anchor with the
296
+ `topnav-link` class from `styles/docs.css`.
297
+
298
+ One name means one page: Home is always the landing. Give the docs root its
299
+ own tab in `docs.json` under a different name (Overview, Get started) rather
300
+ than a second Home, so no page is reachable only by typing its URL.
301
+
255
302
  ### Change theme colours
256
303
 
257
304
  Edit `theme.colors` and `theme.darkColors` in `docs.config.ts`. Nothing else:
258
305
  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
306
+ both palettes; a light-theme accent rarely has enough contrast on a dark
260
307
  background.
261
308
 
262
309
  ### Enable Ask AI
@@ -296,7 +343,15 @@ background.
296
343
  ```
297
344
 
298
345
  `AskAi` accepts `endpoint`, `suggestions`, `label`, and `placeholder`, and all
299
- four have defaults.
346
+ four have defaults. Write suggestions the corpus can actually answer. They
347
+ are the first thing a reader clicks.
348
+
349
+ On a site with an API reference, build the corpus with the merged OpenAPI
350
+ document so endpoints carry their fields: in `lib/ask-ai.ts`, pass
351
+ `openApiDocuments: { blaaiz: await getMergedSpec() }` to `mdxToMarkdown` (the
352
+ key must match the `document="…"` name in the generated pages). Retrieval
353
+ reads the last three user turns, so follow-ups keep their topic; answers
354
+ render Markdown tables.
300
355
 
301
356
  ### Enable copy page
302
357
 
@@ -305,8 +360,42 @@ four have defaults.
305
360
  component, guarded by `config.features?.copyPage`.
306
361
  3. Serve the Markdown: `createMarkdownRoute({ read })` in
307
362
  `app/md/[[...slug]]/route.ts`, where `read` returns a page's raw MDX.
363
+ 4. On a site with an API reference, also pass `loadOpenApiDocuments`: an async
364
+ loader returning the merged OpenAPI documents by name (usually
365
+ `{ blaaiz: await getMergedSpec() }`, matching the `document="…"` the
366
+ generated pages reference). Without it, an API page's Markdown is only its
367
+ title and `METHOD /path`; with it, parameters, body fields, enums, and
368
+ responses are rendered in. The same `openApiDocuments` option exists on
369
+ `mdxToMarkdown` and belongs in the Ask AI corpus builder for the same
370
+ reason (see the Enable Ask AI recipe).
371
+
372
+ ### Wire search
373
+
374
+ Fumadocs search is one route file, `app/api/search/route.ts`, re-exporting
375
+ `createFromSource(source)` from `fumadocs-core/search/server`; the dialog comes
376
+ from `RootProvider` and needs nothing else. For a custom surface (a command
377
+ palette, a static index), the framework exports `buildSearchIndex` and
378
+ `searchIndex` from `@blaaiz/docs-core`: pure functions, BM25 scoring with a
379
+ light stemmer, safe to ship to the browser as JSON. Do not reimplement scoring.
380
+
381
+ ### Wire SEO
382
+
383
+ Three framework builders read `docs.config.ts`; each mounts as one thin file:
384
+
385
+ 1. `buildMetadata(config)` exported as `metadata` from `app/layout.tsx`, and
386
+ `buildMetadata(config, { title, description, path })` from the page's
387
+ `generateMetadata`. Without the per-page call, every page shares one title.
388
+ 2. `buildRobots(config)` in `app/robots.ts`: `seo.noindex: true` disallows
389
+ crawling entirely; otherwise `/signin` and `/api/` are excluded.
390
+ 3. `buildSitemap(entries, config)` in `app/sitemap.ts`, with entries from
391
+ `source.getPages()`.
392
+
393
+ `theme.favicon` flows through `buildMetadata`. Never add a
394
+ `<link rel="icon">` by hand.
395
+
396
+ ### Set up workspace auth
308
397
 
309
- ### Set up workspace auth with the shared secret
398
+ Auth is config-only. The site's auth files ship installed and never change:
310
399
 
311
400
  1. In `docs.config.ts`:
312
401
 
@@ -314,17 +403,29 @@ four have defaults.
314
403
  auth: {
315
404
  mode: 'workspace',
316
405
  allowedDomains: ['example.com'],
317
- providers: { secret: { secretEnv: 'DOCS_ACCESS_SECRET' } },
406
+ providers: { secret: {} },
318
407
  }
319
408
  ```
320
409
 
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.
410
+ 2. Set `DOCS_SESSION_SECRET` (a long random value, for example
411
+ `openssl rand -hex 32`) and
412
+ `DOCS_ACCESS_SECRET` in the server environment. Restart. Never write new
413
+ auth files.
414
+
415
+ Providers combine freely: `email: true` (dev only), `secret`, and `google`
416
+ (needs `GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET`). A declared provider whose
417
+ env var is missing is offered as disabled, not broken; a missing session
418
+ secret fails at startup naming the variable.
419
+
420
+ On a site that adds docs-core to an existing Next.js app, mount
421
+ `createSiteAuth(config.auth)` once in `lib/auth.ts`
422
+ and re-export its handlers: `auth.gate` in `middleware.ts` (matcher must exempt
423
+ static images or the sign-in page's own logo request gets redirected),
424
+ `auth.secretSignIn` / `auth.emailSignIn` / `auth.google.start` /
425
+ `auth.google.callback` / `auth.signOut` as routes under `app/api/auth/`, and
426
+ `AuthScreen` (with `auth.secretEnabled` / `auth.googleEnabled`) on the sign-in
427
+ page. Import `@blaaiz/docs-core/styles/auth.css`. Consider
428
+ `seo: { noindex: true }` on a gated site.
328
429
 
329
430
  ## Verification habits
330
431
 
@@ -332,8 +433,8 @@ four have defaults.
332
433
  usually fails at build time by design.
333
434
  - After an API change, re-run the generator and confirm the page count it
334
435
  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.
436
+ - Never hand-edit `content/docs/api/**`, `content/api-methods.json`, or, on a
437
+ site that runs the generator, `meta.json` and `nav.json`. They are outputs.
337
438
  - Check a content change in the browser at `/docs`: sidebar order, the top nav,
338
439
  and search.
339
440
  - A palette change needs a look in both light and dark mode.
@@ -342,19 +443,19 @@ four have defaults.
342
443
 
343
444
  Each message below is raised verbatim by the framework.
344
445
 
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 |
446
+ | Message | Cause and fix |
447
+ | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
448
+ | `ai.provider must be one of 'anthropic', 'openai', got '…'.` | Typo in `docs.config.ts`. Use one of the two names |
449
+ | `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 |
450
+ | `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` |
451
+ | `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 |
452
+ | `Blocked host: localhost` | The proxy refuses loopback, private, and link-local hosts on purpose. Point the playground at a reachable public origin |
453
+ | `docs.json navigation must have a "tabs" array.` | `content/docs.json` is missing `navigation.tabs`. The older Mintlify `navigation` array form is not supported |
454
+ | `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": "…" }` |
455
+ | `Group at <path> is missing a string "group" title.` | Add the `group` title, or move a bare string into `pages` |
456
+ | `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 |
457
+ | `Each OpenAPI input must have a "paths" object.` | One spec file is not a real OpenAPI document. Even a one-operation file needs `paths` |
458
+ | `[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
459
 
359
460
  Error classes are exported, so a site can branch on them:
360
461
  `InvalidNavigationError`, `OpenApiMergeError`, `ProxyTargetError`, and
package/styles/ask-ai.css CHANGED
@@ -28,6 +28,10 @@
28
28
  bottom: 1.5rem;
29
29
  z-index: 9990;
30
30
  display: inline-flex;
31
+ /* A floating pill is always content-sized. Without this, some environments
32
+ stretch a fixed-position button past its content and the shortcut chip is
33
+ left floating mid-pill. */
34
+ width: max-content;
31
35
  align-items: center;
32
36
  gap: 9px;
33
37
  height: 42px;
package/styles/docs.css CHANGED
@@ -245,3 +245,31 @@ body {
245
245
  .page-head > h1 {
246
246
  margin: 0;
247
247
  }
248
+
249
+ /* Top-navigation links rendered by a site's TopNavLink component from
250
+ docsNavLinks data. Matches the fumadocs navbar look on theme tokens. */
251
+ .topnav-link {
252
+ display: inline-flex;
253
+ align-items: center;
254
+ gap: 6px;
255
+ padding: 6px 10px;
256
+ border-radius: 8px;
257
+ color: var(--color-fd-muted-foreground);
258
+ font-size: 14px;
259
+ font-weight: 500;
260
+ text-decoration: none;
261
+ transition: color 0.15s ease;
262
+ }
263
+
264
+ .topnav-link:hover {
265
+ color: var(--color-fd-accent-foreground, var(--color-fd-foreground));
266
+ }
267
+
268
+ .topnav-link[data-active='true'] {
269
+ color: var(--color-fd-primary);
270
+ }
271
+
272
+ .topnav-link svg {
273
+ width: 15px;
274
+ height: 15px;
275
+ }