@solutiondesk/help 1.0.0-1edea54 → 1.0.0-2914fd8

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/README.md CHANGED
@@ -29,15 +29,15 @@ const { page } = await help.page("guides/login"); // one page with its text
29
29
 
30
30
  | Method | API | Returns |
31
31
  | --- | --- | --- |
32
- | `site({ lang })` | `GET …/site` | The site (`name`, `languages`, `websiteUrl`) and its navigation tree: pages, folders (with index page, `root` tab, `group`, `defaultOpen`, `collapsible`), separators and links. Empty folders are left out. |
32
+ | `site({ lang })` | `GET …/site` | The site (`name`, `languages`, `websiteUrl`, its own building `blocks`) and its navigation tree: pages, folders (with index page, `listIndex`, `root` tab, `description`, `group`, `defaultOpen`, `collapsible`), separators and links. Empty folders are left out. |
33
33
  | `pages({ lang })` | `GET …/pages` | Every visible page without its text, in reading order (for builds and sitemaps). |
34
- | `page(path, { lang, redirect })` | `GET …/page?path=` | One page: `markdown`, `html`, `headings`, `blocks`, `breadcrumbs`, `previous`/`next`. The home page has the path `""`. |
34
+ | `page(path, { lang, redirect })` | `GET …/page?path=` | One page: `markdown`, `html`, `headings`, `blocks`, `customBlocks`, `breadcrumbs`, `previous`/`next`. The home page has the path `""`. |
35
35
  | `pageMarkdown(path, { lang })` | `GET …/page.md?path=` | One page as plain Markdown. |
36
36
  | `search(query, { lang, limit })` | `GET …/search?q=` | Up to 20 hits: title hits before heading hits before text hits, with an `excerpt` and the best `heading`. |
37
37
  | `preview(token, { lang })` | `GET …/preview?token=` | A page in any state (drafts too) for your preview route. |
38
38
  | `llmsTxt({ lang })` / `llmsFullTxt({ lang })` | `GET …/llms.txt`, `…/llms-full.txt` | The site for language models: the index, or every page as Markdown. |
39
39
 
40
- `html` is safe to insert as it is: raw HTML in the Markdown is shown as text, links only go to web addresses, `mailto:`, paths and anchors, images only come from your help center. Headings carry the `id`s listed in `headings`.
40
+ `html` is safe to insert as it is: raw HTML in the Markdown is shown as text, links only go to web addresses, `mailto:`, paths and anchors, images only come from your help center. Headings carry the `id`s listed in `headings`, made the way GitHub and Fumadocs make them (umlauts kept, `-1` for a repeat), so a site moved from Fumadocs keeps its `#links`; a step's title is a heading too.
41
41
 
42
42
  ### Moved pages
43
43
 
@@ -104,7 +104,7 @@ The navigation becomes the files Fumadocs knows from a content folder:
104
104
  | --- | --- |
105
105
  | page `guides/login` | `guides/login.mdx` (`slugs: ["guides", "login"]`) |
106
106
  | home page | `index.mdx` at the top, or `(core)/index.mdx` when it is a top-level group's own page |
107
- | folder `guides` | `guides/meta.json` with `title`, `icon`, `root`, `defaultOpen`, `collapsible`, `pages` |
107
+ | folder `guides` | `guides/meta.json` with `title`, `description` (shown under a tab's name), `pagesIndex` — or, with `listIndex`, `"index"` first in `pages` (after a leading separator), so the page is an entry and the name only opens the folder —, `icon`, `root`, `defaultOpen`, `collapsible`, `pages` |
108
108
  | group folder `core` (adds no address part) | `(core)/` — a Fumadocs folder group, so `(core)/sign-up/index.mdx` is still `sign-up` |
109
109
  | folder's own page | `guides/index.mdx` (`pagesIndex: "index"`, so it also works for `root` tabs) |
110
110
  | any other page with the slug `index` | `guides/index_.mdx` (`slugs: ["guides", "index"]`), so it never collides with the folder's own page |
@@ -112,7 +112,7 @@ The navigation becomes the files Fumadocs knows from a content folder:
112
112
  | separator | `"---[Icon]Title---"` (a separator without a title is `"---"`) |
113
113
  | link | `"[Icon][Title](url)"`, `"external:[Icon][Title](url)"` |
114
114
 
115
- Every page's `data` holds `title`, `description`, `icon`, `path`, `lang`, `load()` (the page with its text, or `null` when the page was taken offline after the source was built — answer with a 404) and `structuredData()` (for Fumadocs search; empty for such a page, so it never breaks the search index).
115
+ Every page's `data` holds `title`, `description`, `icon`, `path`, `lang`, `blocks` (the site's own building blocks, for `HelpBody`), `load()` (the page with its text, or `null` when the page was taken offline after the source was built — answer with a 404) and `structuredData()` (for Fumadocs search; empty for such a page, so it never breaks the search index).
116
116
 
117
117
  **Lazy or eager.** The page list has no texts, so by default (`pages: "lazy"`) building the source costs one request per language, and `page.data.load()` fetches a page's text when it is rendered. Cache that with your framework (the tags above). With `pages: "eager"` every page is fetched while building (`concurrency`, default 6) and kept in `page.data.page`: slower to build, nothing fetched later, best for a fully static export.
118
118
 
@@ -153,7 +153,7 @@ export default async function Page({ params }: { params: Promise<{ slug?: string
153
153
  <DocsTitle>{page.title}</DocsTitle>
154
154
  <DocsDescription>{page.description}</DocsDescription>
155
155
  <DocsBody>
156
- <HelpBody markdown={page.markdown} headings={page.headings} icon={icon} />
156
+ <HelpBody page={page} blocks={entry.data.blocks} icon={icon} />
157
157
  </DocsBody>
158
158
  </DocsPage>
159
159
  );
@@ -164,20 +164,67 @@ export async function generateStaticParams() {
164
164
  }
165
165
  ```
166
166
 
167
- `HelpBody` renders the Markdown with Fumadocs UI: `:::callout` → `Callout`, `::::steps`/`:::step` → `Steps`/`Step`, `::::tabs`/`:::tab` → `Tabs`/`Tab`, `::::cards`/`:::card` → `Cards`/`Card`, a code fence with `title="…"` → `CodeBlock`. Pass `headings` so the anchors match `getHelpToc`. Replace any part with `components`, for example syntax highlighting:
167
+ `page` gives `HelpBody` the page's `markdown`, `headings` and `locale`; you can also pass `markdown`, `headings` and `lang` yourself. `HelpBody` renders the Markdown with Fumadocs UI: `:::callout` → `Callout`, `::::steps`/`:::step` → `Steps`/`Step`, `::::tabs`/`:::tab` → `Tabs`/`Tab`, `::::cards`/`:::card` → `Cards`/`Card`, a code fence with `title="…"` → `CodeBlock`. Pass `headings` (or `page`) so the anchors match `getHelpToc`. Replace any part with `components`, for example syntax highlighting:
168
168
 
169
169
  ```tsx
170
170
  import { DynamicCodeBlock } from "fumadocs-ui/components/dynamic-codeblock";
171
171
 
172
172
  <HelpBody
173
- markdown={page.markdown}
174
- headings={page.headings}
173
+ page={page}
175
174
  components={{ Code: ({ code, lang, title }) => <DynamicCodeBlock lang={lang ?? "text"} code={code} codeblock={{ title }} /> }}
176
175
  />;
177
176
  ```
178
177
 
179
178
  The same safety rules as the API's `html` apply: raw HTML is shown as text, links only go to `http(s)`, `mailto:`, `/paths` and `#anchors`, images only come from `http(s)` or `/paths` — never to another host through `//`, `/\`, `\\` or control characters hidden in the address. A directive SolutionDesk doesn't know stays the text it was written as.
180
179
 
180
+ ### Your own building blocks
181
+
182
+ In SolutionDesk, a doc site can define its own building blocks: what a block is called, whether it sits in a sentence, on its own line or around content, and which fields it has. How it looks is up to your website. Map each block's name to one of your components:
183
+
184
+ ```tsx
185
+ import { type HelpBlockProps, HelpBody } from "@solutiondesk/help/fumadocs";
186
+ import { icons } from "lucide-react";
187
+
188
+ // :app-button{icon="Palette" label="Your team"} — in a sentence
189
+ function AppButton({ icon, label }: HelpBlockProps<{ icon: string; label: string }>) {
190
+ const Icon = icon ? icons[icon as keyof typeof icons] : undefined;
191
+ return <kbd className="app-button">{Icon ? <Icon /> : null}{label}</kbd>;
192
+ }
193
+
194
+ // ::paid-addon{name="Events"} — on its own line
195
+ function PaidAddon({ name, lang }: HelpBlockProps<{ name: string }>) {
196
+ return <aside className="paid-addon">{lang === "de" ? "Kostenpflichtig" : "Paid add-on"}: {name}</aside>;
197
+ }
198
+
199
+ // :::download-buttons{title="UXP installer" versionsHref="#versions"} … ::: — around content
200
+ function DownloadButtons({ title, versionsHref, children }: HelpBlockProps<{ title: string; versionsHref: string }>) {
201
+ return (
202
+ <section className="downloads">
203
+ <h4>{title}</h4>
204
+ {children}
205
+ {versionsHref ? <a href={versionsHref}>Older versions</a> : null}
206
+ </section>
207
+ );
208
+ }
209
+
210
+ <HelpBody
211
+ page={page}
212
+ blocks={entry.data.blocks} // or `site.blocks` from `help.site()`
213
+ components={{ "app-button": AppButton, "paid-addon": PaidAddon, "download-buttons": DownloadButtons }}
214
+ />;
215
+ ```
216
+
217
+ A component gets:
218
+
219
+ - the block's **fields**, already checked the way SolutionDesk checks them: text fields as strings, on/off fields as `true`/`false`. A value that isn't valid (an unsafe address such as `javascript:`, a picture or video address that isn't allowed, an icon name that isn't one, an option that isn't offered) is left out, so every field may be missing — `HelpBlockProps<Fields>` types them as optional.
220
+ - `lang`: the page's language (`page.locale`, or the `lang` you pass) — no need to store it in the block.
221
+ - `block`: the block's name.
222
+ - `children`: for a block in a sentence or on its own line, its text in `[brackets]` (`:app-button[Share]{icon="Share2"}`); for a block around content, that content, rendered like the rest of the page (built-in and own blocks inside included).
223
+
224
+ `block`, `lang` and `children` always mean the above; SolutionDesk doesn't allow fields with these names. A block the site defines but you have no component for shows its **text version** (set in SolutionDesk, e.g. `Paid add-on: Events`) in a neutral `<span class="sd-block" data-block="…" data-props="…">` (in a sentence) or `<div class="sd-block">` (on its own line; around content with `<p class="sd-block-text">` and `<div class="sd-block-body">`) — the same as the API's `html`. A name the site doesn't define, or a block written in the wrong form, stays the text it was written as. Without `blocks`, every one of them stays text. Search data (`structuredData()`) and heading anchors use the text version as well, like the API's search.
225
+
226
+ `helpBlockValues(definition, attributes)` and `helpBlockText(definition, values, label?)` expose the same rules if you render blocks yourself.
227
+
181
228
  ### Refresh when something changes
182
229
 
183
230
  In SolutionDesk set the site's webhook to a route on your website and copy the signing secret. The handler checks the `X-SolutionDesk-Signature` header and answers `401` when it doesn't match (or is older than 5 minutes) and `400` for a signed body that isn't a help webhook:
@@ -239,10 +286,11 @@ document.querySelector("article")!.innerHTML = page.html;
239
286
  | Block | HTML |
240
287
  | --- | --- |
241
288
  | callout | `<div class="sd-callout" data-type="info\|warn\|error\|success\|idea" role="note">` with `<p class="sd-callout-title">` and `<div class="sd-callout-body">` |
242
- | steps | `<ol class="sd-steps">` of `<li class="sd-step">` with `<p class="sd-step-title">` and `<div class="sd-step-body">` |
289
+ | steps | `<ol class="sd-steps">` of `<li class="sd-step">` with `<h3 class="sd-step-title" id="…">` and `<div class="sd-step-body">` |
243
290
  | tabs | `<div class="sd-tabs">` of `<section class="sd-tab" data-title="…">` (build the tab buttons from `data-title`; `page.blocks` says when a page has tabs) |
244
291
  | cards | `<div class="sd-cards">` of `<a class="sd-card" href="…" data-icon="Rocket">` (or `<div class="sd-card">`) with `<p class="sd-card-title">` and `<div class="sd-card-body">` |
245
292
  | titled code | `<figure class="sd-code"><figcaption>client.ts</figcaption><pre><code class="language-ts">` |
293
+ | the site's own block | `<span class="sd-block" data-block="app-button" data-props="{…}">` in a sentence, `<div class="sd-block">` on its own line or around content (`<p class="sd-block-text">`, `<div class="sd-block-body">`); the text is the block's text version, `data-props` its checked values as JSON — hydrate your own component from it |
246
294
 
247
295
  Build the table of contents from `page.headings` (`#id`), the breadcrumb from `page.breadcrumbs` and the footer links from `page.previous`/`page.next`.
248
296
 
@@ -1,4 +1,4 @@
1
- import { ComponentType, ReactNode } from 'react';
1
+ import { JSXElementConstructor, ReactNode, ComponentType } from 'react';
2
2
  import { MetaData, PageData, StaticSource, VirtualFile, DynamicSource } from 'fumadocs-core/source';
3
3
  import { TOCItemType } from 'fumadocs-core/toc';
4
4
 
@@ -26,6 +26,40 @@ interface HelpPublicSite {
26
26
  languages: string[];
27
27
  /** The docs website's base address (`https://help.acme.com/docs`), if set. */
28
28
  websiteUrl: string | null;
29
+ /** The site's own building blocks pages may use (empty when it defines none). */
30
+ blocks: HelpPublicBlockDefinition[];
31
+ }
32
+ /** What a field of a site's own block holds (see {@link HelpPublicBlockField}). */
33
+ type HelpPublicBlockFieldType = "text" | "longText" | "icon" | "url" | "choice" | "image" | "video" | "boolean";
34
+ /**
35
+ * One field of a site's own block: an attribute of its directive
36
+ * (`key="value"`). `boolean` fields are on when written bare (`key`) or as
37
+ * `key="true"`; every other value is a string. `options` lists a `choice`
38
+ * field's values (null for other types).
39
+ */
40
+ interface HelpPublicBlockField {
41
+ key: string;
42
+ label: string;
43
+ type: HelpPublicBlockFieldType;
44
+ required: boolean;
45
+ options: string[] | null;
46
+ }
47
+ /**
48
+ * A building block the site defines itself — WHAT it is; how it looks is the
49
+ * website's own component. Written in Markdown as `:name[label]{…}` in running
50
+ * text (`inline`), `::name[label]{…}` alone on a line (`block`) or
51
+ * `:::name{…}` … `:::` around Markdown content (`container`). `text` is the
52
+ * template of its text version (`{key}` = a field's value, a `boolean` field's
53
+ * label when on; `{{`/`}}` = a brace) — what to show without a component.
54
+ */
55
+ interface HelpPublicBlockDefinition {
56
+ /** The directive name (`app-button`). */
57
+ name: string;
58
+ /** Display name. */
59
+ label: string;
60
+ kind: "inline" | "block" | "container";
61
+ fields: HelpPublicBlockField[];
62
+ text: string;
29
63
  }
30
64
  /** A page in the navigation. */
31
65
  interface HelpPublicTreePage {
@@ -47,6 +81,8 @@ interface HelpPublicTreeFolder {
47
81
  /** The folder's path (its index page, if any, lives here; a group's is its parent's). */
48
82
  path: string;
49
83
  title: string;
84
+ /** A short line under a tab's name in the tab menu (Fumadocs `description`); null without one. */
85
+ description: string | null;
50
86
  icon: string | null;
51
87
  /** A tab (top level only; Fumadocs `root`). */
52
88
  root: boolean;
@@ -59,6 +95,11 @@ interface HelpPublicTreeFolder {
59
95
  collapsible: boolean;
60
96
  /** The folder's own page, shown at the folder's path; null without one. */
61
97
  index: HelpPublicTreePage | null;
98
+ /**
99
+ * List `index` as the folder's first entry (after a section title it starts
100
+ * with); the folder's name then only opens it (Fumadocs: `index` in `pages`).
101
+ */
102
+ listIndex: boolean;
62
103
  children: HelpPublicTreeNode[];
63
104
  }
64
105
  /** A labelled divider between entries (Fumadocs `---Title---`). */
@@ -135,12 +176,17 @@ interface HelpPublicPageLink {
135
176
  }
136
177
  /** One page with its text. */
137
178
  interface HelpPublicPage extends HelpPublicPageSummary {
138
- /** The page's Markdown with directives (`:::callout`, `::::steps`, …). */
179
+ /**
180
+ * The page's Markdown with directives (`:::callout`, `::::steps`, …, and the
181
+ * site's own blocks as written — see `site.blocks`).
182
+ */
139
183
  markdown: string;
140
184
  /** Safe server-rendered HTML of `markdown` (stable `sd-*` classes for the directives). */
141
185
  html: string;
142
186
  headings: HelpPublicHeading[];
143
187
  blocks: HelpPublicBlock[];
188
+ /** The site's own building blocks the page uses, by name (in the site's order). */
189
+ customBlocks: string[];
144
190
  breadcrumbs: HelpPublicBreadcrumb[];
145
191
  previous: HelpPublicPageLink | null;
146
192
  next: HelpPublicPageLink | null;
@@ -215,8 +261,8 @@ interface HelpWebhookBody {
215
261
  * Parsing the help center's Markdown the way the API renders it
216
262
  * (`packages/db/src/help-markdown.ts`): GFM plus container directives in the
217
263
  * generic syntax of `remark-directive`. Only the directive names below mean
218
- * something; every other directive — and every leaf (`::x`) or text (`:x`)
219
- * directive — is printed back as the text it was written as.
264
+ * something, plus the site's own blocks (`blocks.ts`); every other directive
265
+ * is printed back as the text it was written as.
220
266
  */
221
267
  /** The directive names the help center knows. */
222
268
  declare const HELP_BLOCKS: readonly ["callout", "steps", "step", "tabs", "tab", "cards", "card"];
@@ -236,6 +282,38 @@ type HelpCalloutType = (typeof HELP_CALLOUT_TYPES)[number];
236
282
  */
237
283
  declare function safeHelpHref(raw: string | null | undefined): string | null;
238
284
 
285
+ /**
286
+ * A doc site's own building blocks (`site.blocks`) in the page Markdown — the
287
+ * same rules as the API (`packages/db/src/help-blocks.ts`, `help-markdown.ts`,
288
+ * contract in `docs/help/06-eigene-bausteine.md`), copied so the package has no
289
+ * dependency on the API's code; `test/blocks.test.tsx` holds both in step.
290
+ *
291
+ * The Markdown parser (micromark's directive extension) finds the directives;
292
+ * a use only counts when its name is defined, it is written in its kind's form
293
+ * (`:name` inline, `::name` alone on a line, `:::name` around content) and its
294
+ * source re-reads under the API's stricter syntax (label and attributes on one
295
+ * line, the basic character references) — otherwise it stays literal text.
296
+ */
297
+ /** A field's checked value: `boolean` fields are true/false, every other type a string. */
298
+ type HelpBlockValue = string | boolean;
299
+ /** A block's checked field values, in the definition's field order. */
300
+ type HelpBlockValues = Record<string, HelpBlockValue>;
301
+ /**
302
+ * A block's attributes as checked field values, by the API's rules: only
303
+ * defined fields; an absent `boolean` is `false`; an empty value counts as
304
+ * absent; invalid values (a `javascript:` link, a malformed icon, a choice
305
+ * not offered) and attributes without a field are left out.
306
+ */
307
+ declare function helpBlockValues(block: Pick<HelpPublicBlockDefinition, "fields">, attributes: Readonly<Record<string, string>>): HelpBlockValues;
308
+ /**
309
+ * A block's text version: its template with every `{key}` replaced by the
310
+ * field's value (a `boolean` by its label when on, else nothing; a missing
311
+ * value by nothing), `{{`/`}}` as braces, then `label` after a space;
312
+ * whitespace collapsed. What `HelpBody` shows without a component, and what
313
+ * search and the outline read.
314
+ */
315
+ declare function helpBlockText(block: Pick<HelpPublicBlockDefinition, "fields" | "text">, values: Readonly<HelpBlockValues>, label?: string): string;
316
+
239
317
  interface HelpCalloutProps {
240
318
  /** `info`, `warn`, `error`, `success` or `idea` (an unknown type arrives as `info`). */
241
319
  type: HelpCalloutType;
@@ -247,6 +325,8 @@ interface HelpStepsProps {
247
325
  }
248
326
  interface HelpStepProps {
249
327
  title?: string;
328
+ /** The title's anchor id (a step's title is a heading of the page, level 3). */
329
+ id?: string;
250
330
  children: ReactNode;
251
331
  }
252
332
  interface HelpTabsProps {
@@ -294,6 +374,40 @@ interface HelpImageProps {
294
374
  alt: string;
295
375
  title?: string;
296
376
  }
377
+ /** A field value of a site's own block, as its component receives it. */
378
+ type HelpBlockFieldValue = string | boolean | undefined;
379
+ /**
380
+ * The props of a component for one of the site's own blocks
381
+ * (`docs/help/06`). `Fields` names the block's fields: a `boolean` field is
382
+ * always there (`true`/`false`); any other field is a string, or absent when
383
+ * the page leaves it out or its value isn't valid (a `javascript:` link, an
384
+ * icon name that isn't one, a choice not offered) — even a required one, so
385
+ * every field is optional here.
386
+ *
387
+ * @example
388
+ * type AppButtonProps = HelpBlockProps<{ icon: string; label: string }>;
389
+ * function AppButton({ icon, label, children, lang }: AppButtonProps) { … }
390
+ */
391
+ type HelpBlockProps<Fields extends {
392
+ [K in keyof Fields]: HelpBlockFieldValue;
393
+ } = Record<never, never>> = {
394
+ [K in Exclude<keyof Fields, "block" | "lang" | "children">]?: Fields[K];
395
+ } & {
396
+ /** The block's name (`app-button`). */
397
+ block: string;
398
+ /** The page's language (`page.locale`, or `HelpBody`'s `lang`). */
399
+ lang: string;
400
+ /**
401
+ * `inline`/`block`: the `[label]` (rendered Markdown), if written;
402
+ * `container`: the rendered content — built-in and own blocks included.
403
+ */
404
+ children?: ReactNode;
405
+ };
406
+ /**
407
+ * A component for a site's own block. Typed loosely so any component fits;
408
+ * type yours with {@link HelpBlockProps}.
409
+ */
410
+ type HelpBlockComponent = JSXElementConstructor<never>;
297
411
  /** Everything `HelpBody` renders through a component — replace any of them. */
298
412
  interface HelpBodyComponents {
299
413
  Callout: ComponentType<HelpCalloutProps>;
@@ -311,17 +425,47 @@ interface HelpBodyComponents {
311
425
  }
312
426
  /** The Fumadocs UI defaults. */
313
427
  declare const helpBodyComponents: HelpBodyComponents;
428
+ /**
429
+ * `HelpBody`'s components: any of the built-in ones by their (capitalised)
430
+ * name, and a component per site block by the block's name (`app-button`).
431
+ */
432
+ interface HelpBodyComponentMap extends Partial<HelpBodyComponents> {
433
+ [block: string]: HelpBlockComponent | undefined;
434
+ }
314
435
  interface HelpBodyProps {
315
- /** The page's Markdown with directives (`page.markdown` from the API). */
316
- markdown: string;
436
+ /**
437
+ * The page from the API (`page.data.load()`, `help.page()`, `help.preview()`):
438
+ * its `markdown`, `headings` and `locale` (passed to block components as
439
+ * `lang`). Or pass `markdown` (and `headings`, `lang`) yourself.
440
+ */
441
+ page?: {
442
+ markdown: string;
443
+ headings?: readonly HelpPublicHeading[];
444
+ locale?: string;
445
+ };
446
+ /** The page's Markdown with directives (`page.markdown` from the API); wins over `page`. */
447
+ markdown?: string;
317
448
  /**
318
449
  * The page's `headings` from the API: their ids become the headings' anchors,
319
450
  * so they always match `getHelpToc(page.headings)`. Without them the ids are
320
451
  * computed the API's way.
321
452
  */
322
453
  headings?: readonly HelpPublicHeading[];
323
- /** Replace any of the default Fumadocs UI components. */
324
- components?: Partial<HelpBodyComponents>;
454
+ /** The page's language for block components (default `page.locale`, else `""`). */
455
+ lang?: string;
456
+ /**
457
+ * The site's own blocks (`page.data.blocks` of a help source, or
458
+ * `site.blocks` from `help.site()`). Without them every such directive stays
459
+ * the text it was written as.
460
+ */
461
+ blocks?: readonly HelpPublicBlockDefinition[];
462
+ /**
463
+ * Replace any of the default Fumadocs UI components (`Callout`, `Code`, …),
464
+ * and render the site's own blocks with your components, keyed by the
465
+ * block's name: `{ "app-button": AppButton }`. A defined block without a
466
+ * component shows its text version.
467
+ */
468
+ components?: HelpBodyComponentMap;
325
469
  /**
326
470
  * Turn a card's Lucide icon name (`Rocket`) into an element, e.g. with
327
471
  * `lucide-react`: `(name) => { const Icon = icons[name as keyof typeof icons]; return Icon ? <Icon /> : null; }`.
@@ -332,15 +476,20 @@ interface HelpBodyProps {
332
476
  /**
333
477
  * A help page's Markdown with its blocks (callout, steps, tabs, cards, titled
334
478
  * code fences) as Fumadocs UI components — a React Server Component, also fine
335
- * on the client. The API's safety rules apply: raw HTML is shown as text,
336
- * links only to http(s), mailto, `/paths` and `#anchors`, images only from
337
- * http(s) or `/paths`; unknown directives stay the text they were written as.
479
+ * on the client. The site's own blocks (`blocks`) render with your
480
+ * `components`, or as their text version. The API's safety rules apply: raw
481
+ * HTML is shown as text, links only to http(s), mailto, `/paths` and
482
+ * `#anchors`, images only from http(s) or `/paths`; unknown directives stay
483
+ * the text they were written as.
338
484
  *
339
485
  * @example
340
- * const page = await source.getPage(slugs)?.data.load();
341
- * <DocsBody><HelpBody markdown={page.markdown} headings={page.headings} /></DocsBody>
486
+ * const entry = source.getPage(slugs);
487
+ * const page = await entry.data.load();
488
+ * <DocsBody>
489
+ * <HelpBody page={page} blocks={entry.data.blocks} components={{ "app-button": AppButton }} />
490
+ * </DocsBody>
342
491
  */
343
- declare function HelpBody({ markdown, headings, components, icon }: HelpBodyProps): ReactNode;
492
+ declare function HelpBody(props: HelpBodyProps): ReactNode;
344
493
 
345
494
  /** Per-call options. */
346
495
  interface RequestOptions {
@@ -483,9 +632,11 @@ interface HelpStructuredData {
483
632
  /**
484
633
  * A page's search data for Fumadocs (`createFromSource`): every heading with
485
634
  * its anchor, and every paragraph, quote and table cell under the heading it
486
- * belongs to — code is left out, as in Fumadocs' own `remark-structure`.
635
+ * belongs to — code is left out, as in Fumadocs' own `remark-structure`. The
636
+ * site's own `blocks` count as their text version (like the API's search): a
637
+ * container's text version is an entry of its own, followed by its content.
487
638
  */
488
- declare function helpStructuredData(markdown: string, headings?: readonly HelpPublicHeading[]): HelpStructuredData;
639
+ declare function helpStructuredData(markdown: string, headings?: readonly HelpPublicHeading[], blocks?: readonly HelpPublicBlockDefinition[]): HelpStructuredData;
489
640
 
490
641
  /** What every page of a help source carries in `page.data`. */
491
642
  interface HelpPageData extends PageData {
@@ -497,6 +648,11 @@ interface HelpPageData extends PageData {
497
648
  path: string;
498
649
  /** The language this file was built for (the `lang` asked for; the API falls back per page). */
499
650
  lang: string;
651
+ /**
652
+ * The site's own building blocks (`site.blocks`) — pass them to `HelpBody`
653
+ * (`blocks={page.data.blocks}`) so pages render them with your components.
654
+ */
655
+ blocks: HelpPublicBlockDefinition[];
500
656
  /**
501
657
  * The page with its text (`markdown`, `html`, `headings`, `breadcrumbs`,
502
658
  * `previous`/`next`). Fetched when called — or right away with
@@ -517,6 +673,8 @@ interface HelpPageData extends PageData {
517
673
  /** What every folder's `meta.json` of a help source carries. */
518
674
  interface HelpMetaData extends MetaData {
519
675
  title?: string;
676
+ /** Shown under a tab's name in Fumadocs' tab menu. */
677
+ description?: string;
520
678
  icon?: string;
521
679
  root?: boolean;
522
680
  defaultOpen?: boolean;
@@ -619,4 +777,4 @@ declare function getHelpToc(headings: readonly HelpPublicHeading[] | {
619
777
  headings: readonly HelpPublicHeading[];
620
778
  }, options?: HelpTocOptions): TOCItemType[];
621
779
 
622
- export { HELP_BLOCKS, HELP_CALLOUT_TYPES, type HelpBlockName, HelpBody, type HelpBodyComponents, type HelpBodyProps, type HelpCalloutProps, type HelpCalloutType, type HelpCardProps, type HelpCardsProps, type HelpCodeProps, type HelpFilesOptions, type HelpHeadingProps, type HelpImageProps, type HelpLinkProps, type HelpMetaData, type HelpPageData, type HelpRevalidateOptions, type HelpSource, type HelpSourceConfig, type HelpSourceOptions, type HelpStepProps, type HelpStepsProps, type HelpStructuredData, type HelpTabProps, type HelpTabsProps, type HelpTocOptions, type HelpVirtualFile, type HelpWebhookRequest, createHelpRevalidateHandler, createHelpSource, getHelpToc, helpBodyComponents, helpDynamicSource, helpMetaEntry, helpStructuredData, helpTreePaths, helpTreeToFiles, safeHelpHref };
780
+ export { HELP_BLOCKS, HELP_CALLOUT_TYPES, type HelpBlockComponent, type HelpBlockFieldValue, type HelpBlockName, type HelpBlockProps, type HelpBlockValue, type HelpBlockValues, HelpBody, type HelpBodyComponentMap, type HelpBodyComponents, type HelpBodyProps, type HelpCalloutProps, type HelpCalloutType, type HelpCardProps, type HelpCardsProps, type HelpCodeProps, type HelpFilesOptions, type HelpHeadingProps, type HelpImageProps, type HelpLinkProps, type HelpMetaData, type HelpPageData, type HelpRevalidateOptions, type HelpSource, type HelpSourceConfig, type HelpSourceOptions, type HelpStepProps, type HelpStepsProps, type HelpStructuredData, type HelpTabProps, type HelpTabsProps, type HelpTocOptions, type HelpVirtualFile, type HelpWebhookRequest, createHelpRevalidateHandler, createHelpSource, getHelpToc, helpBlockText, helpBlockValues, helpBodyComponents, helpDynamicSource, helpMetaEntry, helpStructuredData, helpTreePaths, helpTreeToFiles, safeHelpHref };
package/dist/fumadocs.js CHANGED
@@ -1,26 +1,57 @@
1
+ import { directiveFromMarkdown } from 'mdast-util-directive';
2
+ import { fromMarkdown } from 'mdast-util-from-markdown';
3
+ import { gfmFromMarkdown } from 'mdast-util-gfm';
4
+ import { directive } from 'micromark-extension-directive';
5
+ import { gfm } from 'micromark-extension-gfm';
1
6
  import { Callout } from 'fumadocs-ui/components/callout';
2
7
  import { Card, Cards } from 'fumadocs-ui/components/card';
3
8
  import { CodeBlock, Pre } from 'fumadocs-ui/components/codeblock';
4
9
  import { Heading } from 'fumadocs-ui/components/heading';
5
10
  import { Step, Steps } from 'fumadocs-ui/components/steps';
6
11
  import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
7
- import { Fragment as Fragment$1 } from 'react';
8
- import { directiveFromMarkdown } from 'mdast-util-directive';
9
- import { fromMarkdown } from 'mdast-util-from-markdown';
10
- import { gfmFromMarkdown } from 'mdast-util-gfm';
11
- import { directive } from 'micromark-extension-directive';
12
- import { gfm } from 'micromark-extension-gfm';
12
+ import { Fragment as Fragment$1, createElement } from 'react';
13
13
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
14
14
 
15
- // src/fumadocs/body.tsx
15
+ // src/fumadocs/markdown.ts
16
16
  var HELP_BLOCKS = ["callout", "steps", "step", "tabs", "tab", "cards", "card"];
17
17
  var HELP_CALLOUT_TYPES = ["info", "warn", "error", "success", "idea"];
18
18
  var isHelpBlock = (name) => HELP_BLOCKS.includes(name);
19
+ var CODE_FENCE = /^[ \t]*(`{3,}|~{3,})(.*)$/;
20
+ var CLOSING_FENCE = /^([ \t]*):(:{2,}[ \t]*)$/;
21
+ var MASK = "\uE000";
22
+ function maskCodeFences(markdown) {
23
+ if (markdown.includes(MASK)) return markdown;
24
+ let open = null;
25
+ return markdown.split("\n").map((line) => {
26
+ const fence = CODE_FENCE.exec(line);
27
+ if (open) {
28
+ if (fence?.[1]?.[0] === open.char && fence[1].length >= open.length && !fence[2]?.trim()) {
29
+ open = null;
30
+ return line;
31
+ }
32
+ return line.replace(CLOSING_FENCE, `$1${MASK}$2`);
33
+ }
34
+ if (fence?.[1] && !(fence[1][0] === "`" && fence[2]?.includes("`"))) {
35
+ open = { char: fence[1][0] ?? "`", length: fence[1].length };
36
+ }
37
+ return line;
38
+ }).join("\n");
39
+ }
40
+ function unmask(node) {
41
+ if ("value" in node && typeof node.value === "string" && node.value.includes(MASK)) {
42
+ node.value = node.value.replaceAll(MASK, ":");
43
+ }
44
+ if ("children" in node) for (const child of node.children) unmask(child);
45
+ }
19
46
  function parseHelpMarkdown(markdown) {
20
- return fromMarkdown(markdown ?? "", {
47
+ const source = markdown ?? "";
48
+ const masked = maskCodeFences(source);
49
+ const root = fromMarkdown(masked, {
21
50
  extensions: [gfm(), directive()],
22
51
  mdastExtensions: [gfmFromMarkdown(), directiveFromMarkdown()]
23
52
  });
53
+ if (masked !== source) unmask(root);
54
+ return root;
24
55
  }
25
56
  var CONTROL = /[\u0000-\u001f\u007f]/;
26
57
  var PROTOCOL_RELATIVE = /^[\\/]{2}/;
@@ -40,10 +71,14 @@ function safeHelpImageSrc(raw) {
40
71
  if (/^\/[^\s]*$/.test(src)) return src;
41
72
  return null;
42
73
  }
43
- function plainText(node) {
74
+ function plainText(node, override) {
75
+ const own = override?.(node);
76
+ if (own !== void 0) return own;
44
77
  if ("value" in node && typeof node.value === "string") return node.value;
45
78
  if (node.type === "image") return node.alt ?? "";
46
- if ("children" in node) return node.children.map(plainText).join("");
79
+ if ("children" in node) {
80
+ return node.children.map((child) => plainText(child, override)).join("");
81
+ }
47
82
  return "";
48
83
  }
49
84
  function sourceOf(node, markdown) {
@@ -77,34 +112,31 @@ function codeInfo(lang, meta) {
77
112
  const title = (match ? match[1] ?? match[2] ?? match[3] : void 0)?.trim() || void 0;
78
113
  return { language, title };
79
114
  }
80
- var TRANSLITERATIONS = {
81
- \u00E4: "ae",
82
- \u00F6: "oe",
83
- \u00FC: "ue",
84
- \u00DF: "ss",
85
- \u00E6: "ae",
86
- \u0153: "oe",
87
- \u00F8: "o",
88
- \u00E5: "a",
89
- \u0111: "d",
90
- \u00F0: "d",
91
- \u00FE: "th",
92
- \u0142: "l",
93
- \u0131: "i"
94
- };
95
- function headingSlug(text) {
96
- return text.normalize("NFC").toLowerCase().replace(/[äöüßæœøåđðþłı]/g, (ch) => TRANSLITERATIONS[ch] ?? ch).normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 96).replace(/-+$/, "");
115
+ var GITHUB_SLUG_DROP = /[^\p{L}\p{M}\p{N}\p{Pc} -]/gu;
116
+ function headingId(text, used) {
117
+ const base = text.toLowerCase().replace(GITHUB_SLUG_DROP, "").replace(/ /g, "-") || "section";
118
+ let id = base;
119
+ while (used.has(id)) {
120
+ const count = (used.get(base) ?? 0) + 1;
121
+ used.set(base, count);
122
+ id = `${base}-${count}`;
123
+ }
124
+ used.set(id, 0);
125
+ return id;
126
+ }
127
+ function isTitledStep(node) {
128
+ return node.type === "containerDirective" && node.name === "step" && !!directiveTitle(node);
97
129
  }
98
130
  function collectHeadings(root) {
99
131
  const out = [];
100
132
  const walk = (node) => {
101
- if (node.type === "heading") out.push(node);
133
+ if (node.type === "heading" || isTitledStep(node)) out.push(node);
102
134
  if ("children" in node) for (const child of node.children) walk(child);
103
135
  };
104
136
  walk(root);
105
137
  return out;
106
138
  }
107
- function headingIds(root, known) {
139
+ function headingIds(root, known, override) {
108
140
  const headings = collectHeadings(root);
109
141
  const ids = /* @__PURE__ */ new Map();
110
142
  if (known && known.length === headings.length) {
@@ -114,15 +146,231 @@ function headingIds(root, known) {
114
146
  return ids;
115
147
  }
116
148
  const used = /* @__PURE__ */ new Map();
149
+ const withoutBlocks = override ? (node) => {
150
+ if (override(node) === void 0) return void 0;
151
+ if (node.type !== "textDirective") return "";
152
+ return node.children.map((child) => plainText(child)).join("").trim();
153
+ } : void 0;
117
154
  for (const heading of headings) {
118
- const text = plainText(heading).replace(/\s+/g, " ").trim();
119
- const base = headingSlug(text) || "section";
120
- const seen = used.get(base) ?? 0;
121
- used.set(base, seen + 1);
122
- ids.set(heading, seen === 0 ? base : `${base}-${seen + 1}`);
155
+ const text = heading.type === "heading" ? plainText(heading, withoutBlocks) : (directiveTitle(heading) ?? "").replace(/\s+/g, " ").trim();
156
+ ids.set(heading, headingId(text, used));
123
157
  }
124
158
  return ids;
125
159
  }
160
+
161
+ // src/fumadocs/blocks.ts
162
+ var TEXT_VALUE_MAX = 300;
163
+ var LONG_VALUE_MAX = 2e3;
164
+ var ICON = /^[A-Za-z][A-Za-z0-9]{0,47}$/;
165
+ var NAMED_ENTITIES = {
166
+ amp: "&",
167
+ lt: "<",
168
+ gt: ">",
169
+ quot: '"',
170
+ apos: "'",
171
+ nbsp: "\xA0"
172
+ };
173
+ function decodeEntities(value) {
174
+ return value.replace(/&(#x[0-9a-f]{1,6}|#[0-9]{1,7}|[a-z]+);/gi, (match, ref) => {
175
+ if (ref[0] === "#") {
176
+ const code = ref[1] === "x" || ref[1] === "X" ? parseInt(ref.slice(2), 16) : Number(ref.slice(1));
177
+ return Number.isFinite(code) && code > 0 && code <= 1114111 ? String.fromCodePoint(code) : match;
178
+ }
179
+ return NAMED_ENTITIES[ref.toLowerCase()] ?? match;
180
+ });
181
+ }
182
+ var ATTRIBUTE = /^(?:#([^\s"'<>=`}#.]+)|\.([^\s"'<>=`}#.]+)|([A-Za-z_:][A-Za-z0-9_.:-]*)(?:[ \t]*=[ \t]*(?:"([^"\n]*)"|'([^'\n]*)'|([^\s"'<>=`}]+)))?)/;
183
+ function parseAttributes(src) {
184
+ const attributes = {};
185
+ let pos = 1;
186
+ for (; ; ) {
187
+ while (src[pos] === " " || src[pos] === " ") pos++;
188
+ if (src[pos] === "}") return { attributes, length: pos + 1 };
189
+ const match = ATTRIBUTE.exec(src.slice(pos));
190
+ if (!match) return null;
191
+ const [whole, id, cls, key, dq, sq, bare] = match;
192
+ if (id !== void 0) attributes.id = decodeEntities(id);
193
+ else if (cls !== void 0)
194
+ attributes.class = attributes.class ? `${attributes.class} ${cls}` : cls;
195
+ else if (key !== void 0) attributes[key] = decodeEntities(dq ?? sq ?? bare ?? "");
196
+ pos += whole.length;
197
+ if (src[pos] !== " " && src[pos] !== " " && src[pos] !== "}") return null;
198
+ }
199
+ }
200
+ function parseLabel(src) {
201
+ let depth = 0;
202
+ for (let pos = 0; pos < src.length; pos++) {
203
+ const char = src[pos];
204
+ if (char === "\n") return null;
205
+ if (char === "\\") {
206
+ pos++;
207
+ continue;
208
+ }
209
+ if (char === "[") depth++;
210
+ else if (char === "]") {
211
+ depth--;
212
+ if (depth === 0) return { label: src.slice(1, pos), length: pos + 1 };
213
+ }
214
+ }
215
+ return null;
216
+ }
217
+ function parseDirectiveTail(src) {
218
+ let rest = src;
219
+ let label;
220
+ let attributes = {};
221
+ if (rest.startsWith("[")) {
222
+ const parsed = parseLabel(rest);
223
+ if (!parsed) return null;
224
+ label = parsed.label;
225
+ rest = rest.slice(parsed.length);
226
+ }
227
+ if (rest.startsWith("{")) {
228
+ const parsed = parseAttributes(rest);
229
+ if (!parsed) return null;
230
+ attributes = parsed.attributes;
231
+ rest = rest.slice(parsed.length);
232
+ }
233
+ return { ...label !== void 0 ? { label } : {}, attributes, rest };
234
+ }
235
+ function plainLabel(label) {
236
+ if (label === void 0) return void 0;
237
+ const text = label.replace(/\\(.)/g, "$1").replace(/[*_`~]/g, "").trim();
238
+ return text || void 0;
239
+ }
240
+ var isVideoAddress = (value) => safeHelpHref(value) === value && (/^https:\/\//i.test(value) || value.startsWith("/"));
241
+ function fieldValue(field, raw) {
242
+ if (field.type === "boolean") {
243
+ const flag = raw.trim().toLowerCase();
244
+ if (flag === "" || flag === "true") return true;
245
+ if (flag === "false") return false;
246
+ return void 0;
247
+ }
248
+ const value = raw.trim();
249
+ switch (field.type) {
250
+ case "text":
251
+ return !/[\r\n]/.test(value) && value.length <= TEXT_VALUE_MAX ? value : void 0;
252
+ case "longText":
253
+ return value.length <= LONG_VALUE_MAX ? value : void 0;
254
+ case "icon":
255
+ return ICON.test(value) ? value : void 0;
256
+ case "url":
257
+ return value.length <= LONG_VALUE_MAX && safeHelpHref(value) === value ? value : void 0;
258
+ case "choice":
259
+ return field.options?.includes(value) ? value : void 0;
260
+ case "image":
261
+ return value.length <= LONG_VALUE_MAX && safeHelpImageSrc(value) === value ? value : void 0;
262
+ case "video":
263
+ return value.length <= LONG_VALUE_MAX && isVideoAddress(value) ? value : void 0;
264
+ default:
265
+ return void 0;
266
+ }
267
+ }
268
+ function helpBlockValues(block, attributes) {
269
+ const values = {};
270
+ for (const field of block.fields) {
271
+ const raw = Object.hasOwn(attributes, field.key) ? attributes[field.key] : void 0;
272
+ if (field.type === "boolean") {
273
+ if (raw === void 0) {
274
+ values[field.key] = false;
275
+ continue;
276
+ }
277
+ } else if (raw === void 0 || raw.trim() === "") {
278
+ continue;
279
+ }
280
+ const value = fieldValue(field, raw);
281
+ if (value !== void 0) values[field.key] = value;
282
+ }
283
+ return values;
284
+ }
285
+ function isTemplate(template) {
286
+ for (let i = 0; i < template.length; i++) {
287
+ const char = template[i];
288
+ if ((char === "{" || char === "}") && template[i + 1] === char) {
289
+ i++;
290
+ continue;
291
+ }
292
+ if (char === "}") return false;
293
+ if (char !== "{") continue;
294
+ const end = template.indexOf("}", i + 1);
295
+ if (end === -1) return false;
296
+ const key = template.slice(i + 1, end).trim();
297
+ if (!key || key.includes("{")) return false;
298
+ i = end;
299
+ }
300
+ return true;
301
+ }
302
+ function helpBlockText(block, values, label) {
303
+ const template = block.text ?? "";
304
+ const fields = new Map(block.fields.map((f) => [f.key, f]));
305
+ let text = "";
306
+ if (isTemplate(template)) {
307
+ for (let i = 0; i < template.length; i++) {
308
+ const char = template[i] ?? "";
309
+ if ((char === "{" || char === "}") && template[i + 1] === char) {
310
+ text += char;
311
+ i++;
312
+ continue;
313
+ }
314
+ if (char !== "{") {
315
+ text += char;
316
+ continue;
317
+ }
318
+ const end = template.indexOf("}", i + 1);
319
+ const key = template.slice(i + 1, end).trim();
320
+ const value = values[key];
321
+ if (value === true) text += fields.get(key)?.label ?? "";
322
+ else if (typeof value === "string") text += value;
323
+ i = end;
324
+ }
325
+ }
326
+ return `${text} ${label ?? ""}`.replace(/\s+/g, " ").trim();
327
+ }
328
+ var FORMS = {
329
+ textDirective: "inline",
330
+ leafDirective: "block",
331
+ containerDirective: "container"
332
+ };
333
+ function helpBlockMatcher(blocks, markdown) {
334
+ const defined = new Map((blocks ?? []).map((block) => [block.name, block]));
335
+ const cache = /* @__PURE__ */ new Map();
336
+ const find = (node) => {
337
+ if (defined.size === 0) return null;
338
+ if (node.type !== "textDirective" && node.type !== "leafDirective" && node.type !== "containerDirective") {
339
+ return null;
340
+ }
341
+ const block = defined.get(node.name);
342
+ if (!block || block.kind !== FORMS[node.type]) return null;
343
+ const start = node.position?.start.offset;
344
+ const end = node.position?.end.offset;
345
+ if (start === void 0 || end === void 0) return null;
346
+ const lineEnd = markdown.indexOf("\n", start);
347
+ const line = markdown.slice(start, lineEnd === -1 ? void 0 : lineEnd);
348
+ const head = /^(:+)([A-Za-z][A-Za-z0-9_-]*)/.exec(line);
349
+ const colons = head?.[1]?.length ?? 0;
350
+ const form = colons === 1 ? "textDirective" : colons === 2 ? "leafDirective" : "containerDirective";
351
+ if (!head || form !== node.type || head[2] !== node.name) return null;
352
+ const tail = parseDirectiveTail(line.slice(head[0].length));
353
+ if (!tail) return null;
354
+ if (node.type === "textDirective") {
355
+ if (line.length - tail.rest.length !== end - start) return null;
356
+ } else if (tail.rest.trim() !== "") return null;
357
+ const values = helpBlockValues(block, tail.attributes);
358
+ const label = node.type === "containerDirective" ? void 0 : plainLabel(tail.label);
359
+ return { block, node, values, text: helpBlockText(block, values, label) };
360
+ };
361
+ const use = (node) => {
362
+ if (!cache.has(node)) cache.set(node, find(node));
363
+ return cache.get(node) ?? null;
364
+ };
365
+ const text = (node) => {
366
+ const found = use(node);
367
+ if (!found) return void 0;
368
+ if (found.node.type !== "containerDirective") return found.text;
369
+ const inner = contentOf(found.node).map((child) => plainText(child, text)).join(" ");
370
+ return `${found.text} ${inner}`.trim();
371
+ };
372
+ return { use, text };
373
+ }
126
374
  var PLAIN_CODE_COLORS = {
127
375
  "--shiki-light": "var(--color-fd-foreground)",
128
376
  "--shiki-dark": "var(--color-fd-foreground)"
@@ -130,8 +378,8 @@ var PLAIN_CODE_COLORS = {
130
378
  var helpBodyComponents = {
131
379
  Callout: ({ type, title, children }) => /* @__PURE__ */ jsx(Callout, { type, title, children }),
132
380
  Steps: ({ children }) => /* @__PURE__ */ jsx(Steps, { children }),
133
- Step: ({ title, children }) => /* @__PURE__ */ jsxs(Step, { children: [
134
- title ? /* @__PURE__ */ jsx("h3", { children: title }) : null,
381
+ Step: ({ title, id, children }) => /* @__PURE__ */ jsxs(Step, { children: [
382
+ title ? /* @__PURE__ */ jsx(Heading, { as: "h3", id, children: title }) : null,
135
383
  children
136
384
  ] }),
137
385
  Tabs: ({ items, children }) => /* @__PURE__ */ jsx(Tabs, { items, children }),
@@ -152,6 +400,15 @@ var helpBodyComponents = {
152
400
  Link: ({ href, title, children }) => /* @__PURE__ */ jsx("a", { href, title, children }),
153
401
  Image: ({ src, alt, title }) => /* @__PURE__ */ jsx("img", { src, alt, title, loading: "lazy" })
154
402
  };
403
+ var BUILT_IN_KEYS = Object.keys(helpBodyComponents);
404
+ function pickBuiltIns(components) {
405
+ const out = {};
406
+ for (const key of BUILT_IN_KEYS) {
407
+ const component = components[key];
408
+ if (component) out[key] = component;
409
+ }
410
+ return out;
411
+ }
155
412
  var normalizeId = (id) => id.trim().replace(/\s+/g, " ").toLowerCase();
156
413
  function collectDefinitions(root) {
157
414
  const out = /* @__PURE__ */ new Map();
@@ -254,8 +511,30 @@ function renderTabs(children, ctx) {
254
511
  );
255
512
  }) });
256
513
  }
514
+ function renderCustomBlock(use, ctx) {
515
+ const { block, node, values, text } = use;
516
+ const isContainer = node.type === "containerDirective";
517
+ const children = node.type === "containerDirective" ? renderAll(contentOf(node), { ...ctx, tight: false }) : node.children.length ? renderAll(node.children, { ...ctx, inline: true }) : void 0;
518
+ const Component = Object.hasOwn(ctx.custom, block.name) ? ctx.custom[block.name] : void 0;
519
+ if (Component) {
520
+ return createElement(Component, { ...values, block: block.name, lang: ctx.lang }, children);
521
+ }
522
+ const attributes = {
523
+ className: "sd-block",
524
+ "data-block": block.name,
525
+ "data-props": JSON.stringify(values)
526
+ };
527
+ if (block.kind === "inline") return /* @__PURE__ */ jsx("span", { ...attributes, children: text });
528
+ if (!isContainer) return /* @__PURE__ */ jsx("div", { ...attributes, children: text });
529
+ return /* @__PURE__ */ jsxs("div", { ...attributes, children: [
530
+ text ? /* @__PURE__ */ jsx("p", { className: "sd-block-text", children: text }) : null,
531
+ /* @__PURE__ */ jsx("div", { className: "sd-block-body", children })
532
+ ] });
533
+ }
257
534
  function renderDirective(node, ctx) {
258
535
  const { c } = ctx;
536
+ const custom = ctx.blocks.use(node);
537
+ if (custom) return renderCustomBlock(custom, ctx);
259
538
  if (!isHelpBlock(node.name)) {
260
539
  const source = sourceOf(node, ctx.markdown);
261
540
  const lines = source.split("\n");
@@ -275,7 +554,7 @@ function renderDirective(node, ctx) {
275
554
  return /* @__PURE__ */ jsx(c.Steps, { children: content.map(
276
555
  (child, i) => isDirective(child, "step") ? (
277
556
  // biome-ignore lint/suspicious/noArrayIndexKey: static content, never reordered.
278
- /* @__PURE__ */ jsx(c.Step, { title: directiveTitle(child), children: renderAll(contentOf(child), ctx) }, i)
557
+ /* @__PURE__ */ jsx(c.Step, { title: directiveTitle(child), id: ctx.ids.get(child), children: renderAll(contentOf(child), ctx) }, i)
279
558
  ) : (
280
559
  // A loose block between steps becomes a step of its own.
281
560
  // biome-ignore lint/suspicious/noArrayIndexKey: static content, never reordered.
@@ -283,7 +562,7 @@ function renderDirective(node, ctx) {
283
562
  )
284
563
  ) });
285
564
  case "step":
286
- return /* @__PURE__ */ jsx(c.Steps, { children: /* @__PURE__ */ jsx(c.Step, { title, children: renderAll(content, ctx) }) });
565
+ return /* @__PURE__ */ jsx(c.Steps, { children: /* @__PURE__ */ jsx(c.Step, { title, id: ctx.ids.get(node), children: renderAll(content, ctx) }) });
287
566
  case "tabs":
288
567
  return renderTabs(content, ctx);
289
568
  case "tab":
@@ -358,24 +637,34 @@ function renderNode(node, ctx) {
358
637
  return null;
359
638
  case "containerDirective":
360
639
  return renderDirective(node, { ...ctx, tight: false });
361
- // Not ours: printed back as written.
362
640
  case "leafDirective":
641
+ case "textDirective": {
642
+ const custom = ctx.blocks.use(node);
643
+ if (custom) return renderCustomBlock(custom, ctx);
644
+ return node.type === "leafDirective" ? literalBlock(sourceOf(node, ctx.markdown)) : sourceOf(node, ctx.markdown);
645
+ }
646
+ // Not ours: printed back as written.
363
647
  case "footnoteDefinition":
364
648
  return literalBlock(sourceOf(node, ctx.markdown));
365
- case "textDirective":
366
649
  case "footnoteReference":
367
650
  return sourceOf(node, ctx.markdown);
368
651
  default:
369
652
  return "children" in node ? renderAll(node.children, ctx) : "value" in node ? String(node.value) : plainText(node);
370
653
  }
371
654
  }
372
- function HelpBody({ markdown, headings, components, icon }) {
373
- const source = markdown ?? "";
655
+ function HelpBody(props) {
656
+ const { page, components = {}, icon } = props;
657
+ const source = props.markdown ?? page?.markdown ?? "";
658
+ const headings = props.headings ?? page?.headings;
374
659
  const root = parseHelpMarkdown(source);
660
+ const blocks = helpBlockMatcher(props.blocks, source);
375
661
  const ctx = {
376
662
  markdown: source,
377
- ids: headingIds(root, headings),
378
- c: { ...helpBodyComponents, ...components },
663
+ blocks,
664
+ custom: components,
665
+ lang: props.lang ?? page?.locale ?? "",
666
+ ids: headingIds(root, headings, blocks.text),
667
+ c: { ...helpBodyComponents, ...pickBuiltIns(components) },
379
668
  icon,
380
669
  definitions: collectDefinitions(root),
381
670
  linkless: false,
@@ -484,12 +773,17 @@ function parseBody(raw) {
484
773
  }
485
774
 
486
775
  // src/fumadocs/structured.ts
487
- function helpStructuredData(markdown, headings) {
488
- const root = parseHelpMarkdown(markdown);
489
- const ids = headingIds(root, headings);
776
+ function helpStructuredData(markdown, headings, blocks) {
777
+ const source = markdown ?? "";
778
+ const root = parseHelpMarkdown(source);
779
+ const matcher = helpBlockMatcher(blocks, source);
780
+ const ids = headingIds(root, headings, matcher.text);
490
781
  const out = { headings: [], contents: [] };
491
782
  let current;
492
- const text = (node) => plainText(node).replace(/\s+/g, " ").trim();
783
+ const text = (node) => plainText(node, matcher.text).replace(/\s+/g, " ").trim();
784
+ const add = (content) => {
785
+ if (content) out.contents.push({ heading: current, content });
786
+ };
493
787
  const walk = (node) => {
494
788
  if (node.type === "heading") {
495
789
  current = ids.get(node);
@@ -497,9 +791,22 @@ function helpStructuredData(markdown, headings) {
497
791
  if (current && content) out.headings.push({ id: current, content });
498
792
  return;
499
793
  }
794
+ if (isTitledStep(node)) {
795
+ current = ids.get(node);
796
+ const content = directiveTitle(node) ?? "";
797
+ if (current) out.headings.push({ id: current, content });
798
+ for (const child of contentOf(node)) walk(child);
799
+ return;
800
+ }
801
+ const use = matcher.use(node);
802
+ if (use) {
803
+ add(use.text);
804
+ if (use.node.type === "containerDirective")
805
+ for (const child of contentOf(use.node)) walk(child);
806
+ return;
807
+ }
500
808
  if (node.type === "paragraph" || node.type === "tableCell") {
501
- const content = text(node);
502
- if (content) out.contents.push({ heading: current, content });
809
+ add(text(node));
503
810
  return;
504
811
  }
505
812
  if (node.type === "code" || node.type === "html" || node.type === "definition") return;
@@ -540,6 +847,7 @@ function helpTreeToFiles(payload, options) {
540
847
  const ext = (name, extension) => `${name}${options.suffix ? `.${options.suffix}` : ""}.${extension}`;
541
848
  const join = (dir, name) => dir ? `${dir}/${name}` : name;
542
849
  const seen = /* @__PURE__ */ new Set();
850
+ const blocks = payload.site.blocks ?? [];
543
851
  const addPage = (page, file) => {
544
852
  if (seen.has(file)) return;
545
853
  seen.add(file);
@@ -552,25 +860,33 @@ function helpTreeToFiles(payload, options) {
552
860
  ...page.icon ? { icon: page.icon } : {},
553
861
  path: page.path,
554
862
  lang: options.lang,
863
+ blocks,
555
864
  load,
556
865
  structuredData: async () => {
557
866
  const full = await load();
558
- return full ? helpStructuredData(full.markdown, full.headings) : { headings: [], contents: [] };
867
+ return full ? helpStructuredData(full.markdown, full.headings, blocks) : { headings: [], contents: [] };
559
868
  },
560
869
  ...prefetched ? { page: prefetched } : {}
561
870
  };
562
871
  files.push({ type: "page", path: file, slugs: slugsOf(page.path), data });
563
872
  };
873
+ const folderPages = (folder) => {
874
+ const pages = folder.children.map(helpMetaEntry);
875
+ if (!folder.index || !folder.listIndex) return pages;
876
+ const at = folder.children[0]?.type === "separator" ? 1 : 0;
877
+ return [...pages.slice(0, at), "index", ...pages.slice(at)];
878
+ };
564
879
  const addFolder = (folder, dir) => {
565
880
  if (folder.index) addPage(folder.index, join(dir, ext("index", "mdx")));
566
881
  const data = {
567
882
  title: folder.title,
883
+ ...folder.description ? { description: folder.description } : {},
568
884
  ...folder.icon ? { icon: folder.icon } : {},
569
885
  ...folder.root ? { root: true } : {},
570
886
  defaultOpen: folder.defaultOpen,
571
887
  collapsible: folder.collapsible,
572
- ...folder.index ? { pagesIndex: "index" } : {},
573
- pages: folder.children.map(helpMetaEntry)
888
+ ...folder.index && !folder.listIndex ? { pagesIndex: "index" } : {},
889
+ pages: folderPages(folder)
574
890
  };
575
891
  files.push({ type: "meta", path: join(dir, ext("meta", "json")), data });
576
892
  addChildren(folder.children, dir);
@@ -662,4 +978,4 @@ function getHelpToc(headings, options = {}) {
662
978
  return list.filter((heading) => heading.level >= min && heading.level <= max && heading.id).map((heading) => ({ title: heading.text, url: `#${heading.id}`, depth: heading.level }));
663
979
  }
664
980
 
665
- export { HELP_BLOCKS, HELP_CALLOUT_TYPES, HelpBody, createHelpRevalidateHandler, createHelpSource, getHelpToc, helpBodyComponents, helpDynamicSource, helpMetaEntry, helpStructuredData, helpTreePaths, helpTreeToFiles, safeHelpHref };
981
+ export { HELP_BLOCKS, HELP_CALLOUT_TYPES, HelpBody, createHelpRevalidateHandler, createHelpSource, getHelpToc, helpBlockText, helpBlockValues, helpBodyComponents, helpDynamicSource, helpMetaEntry, helpStructuredData, helpTreePaths, helpTreeToFiles, safeHelpHref };
package/dist/index.d.cts CHANGED
@@ -153,6 +153,40 @@ interface HelpPublicSite {
153
153
  languages: string[];
154
154
  /** The docs website's base address (`https://help.acme.com/docs`), if set. */
155
155
  websiteUrl: string | null;
156
+ /** The site's own building blocks pages may use (empty when it defines none). */
157
+ blocks: HelpPublicBlockDefinition[];
158
+ }
159
+ /** What a field of a site's own block holds (see {@link HelpPublicBlockField}). */
160
+ type HelpPublicBlockFieldType = "text" | "longText" | "icon" | "url" | "choice" | "image" | "video" | "boolean";
161
+ /**
162
+ * One field of a site's own block: an attribute of its directive
163
+ * (`key="value"`). `boolean` fields are on when written bare (`key`) or as
164
+ * `key="true"`; every other value is a string. `options` lists a `choice`
165
+ * field's values (null for other types).
166
+ */
167
+ interface HelpPublicBlockField {
168
+ key: string;
169
+ label: string;
170
+ type: HelpPublicBlockFieldType;
171
+ required: boolean;
172
+ options: string[] | null;
173
+ }
174
+ /**
175
+ * A building block the site defines itself — WHAT it is; how it looks is the
176
+ * website's own component. Written in Markdown as `:name[label]{…}` in running
177
+ * text (`inline`), `::name[label]{…}` alone on a line (`block`) or
178
+ * `:::name{…}` … `:::` around Markdown content (`container`). `text` is the
179
+ * template of its text version (`{key}` = a field's value, a `boolean` field's
180
+ * label when on; `{{`/`}}` = a brace) — what to show without a component.
181
+ */
182
+ interface HelpPublicBlockDefinition {
183
+ /** The directive name (`app-button`). */
184
+ name: string;
185
+ /** Display name. */
186
+ label: string;
187
+ kind: "inline" | "block" | "container";
188
+ fields: HelpPublicBlockField[];
189
+ text: string;
156
190
  }
157
191
  /** A page in the navigation. */
158
192
  interface HelpPublicTreePage {
@@ -174,6 +208,8 @@ interface HelpPublicTreeFolder {
174
208
  /** The folder's path (its index page, if any, lives here; a group's is its parent's). */
175
209
  path: string;
176
210
  title: string;
211
+ /** A short line under a tab's name in the tab menu (Fumadocs `description`); null without one. */
212
+ description: string | null;
177
213
  icon: string | null;
178
214
  /** A tab (top level only; Fumadocs `root`). */
179
215
  root: boolean;
@@ -186,6 +222,11 @@ interface HelpPublicTreeFolder {
186
222
  collapsible: boolean;
187
223
  /** The folder's own page, shown at the folder's path; null without one. */
188
224
  index: HelpPublicTreePage | null;
225
+ /**
226
+ * List `index` as the folder's first entry (after a section title it starts
227
+ * with); the folder's name then only opens it (Fumadocs: `index` in `pages`).
228
+ */
229
+ listIndex: boolean;
189
230
  children: HelpPublicTreeNode[];
190
231
  }
191
232
  /** A labelled divider between entries (Fumadocs `---Title---`). */
@@ -262,12 +303,17 @@ interface HelpPublicPageLink {
262
303
  }
263
304
  /** One page with its text. */
264
305
  interface HelpPublicPage extends HelpPublicPageSummary {
265
- /** The page's Markdown with directives (`:::callout`, `::::steps`, …). */
306
+ /**
307
+ * The page's Markdown with directives (`:::callout`, `::::steps`, …, and the
308
+ * site's own blocks as written — see `site.blocks`).
309
+ */
266
310
  markdown: string;
267
311
  /** Safe server-rendered HTML of `markdown` (stable `sd-*` classes for the directives). */
268
312
  html: string;
269
313
  headings: HelpPublicHeading[];
270
314
  blocks: HelpPublicBlock[];
315
+ /** The site's own building blocks the page uses, by name (in the site's order). */
316
+ customBlocks: string[];
271
317
  breadcrumbs: HelpPublicBreadcrumb[];
272
318
  previous: HelpPublicPageLink | null;
273
319
  next: HelpPublicPageLink | null;
@@ -430,4 +476,4 @@ declare function createHelpClient(options: HelpClientOptions): HelpClient;
430
476
  */
431
477
  declare function verifyHelpWebhook(secret: string, rawBody: WebhookBody, signatureHeader: string | null | undefined, options?: VerifyWebhookOptions): Promise<HelpWebhookBody | null>;
432
478
 
433
- export { type BaseClientOptions, type CachedResponse, DEFAULT_BASE_URL, EVENT_HEADER, type EtagCache, type FetchInitLike, type FetchLike, type FetchResponseLike, type GetPageParams, type HelpClient, type HelpClientOptions, type HelpPageResult, type HelpPublicBlock, type HelpPublicBreadcrumb, type HelpPublicHeading, type HelpPublicPage, type HelpPublicPageLink, type HelpPublicPagePayload, type HelpPublicPageSummary, type HelpPublicPagesPayload, type HelpPublicPreviewPage, type HelpPublicPreviewPayload, type HelpPublicRedirect, type HelpPublicSearchHit, type HelpPublicSearchPayload, type HelpPublicSite, type HelpPublicSitePayload, type HelpPublicTreeFolder, type HelpPublicTreeLink, type HelpPublicTreeNode, type HelpPublicTreePage, type HelpPublicTreeSeparator, type HelpWebhookBody, type HelpWebhookEvent, type LangParams, type RequestOptions, SIGNATURE_HEADER, type SearchParams, SolutionDeskError, type VerifyWebhookOptions, type WebhookBody, createHelpClient, createMemoryEtagCache, normalizeHelpPath, verifyHelpWebhook, verifyWebhookSignature };
479
+ export { type BaseClientOptions, type CachedResponse, DEFAULT_BASE_URL, EVENT_HEADER, type EtagCache, type FetchInitLike, type FetchLike, type FetchResponseLike, type GetPageParams, type HelpClient, type HelpClientOptions, type HelpPageResult, type HelpPublicBlock, type HelpPublicBlockDefinition, type HelpPublicBlockField, type HelpPublicBlockFieldType, type HelpPublicBreadcrumb, type HelpPublicHeading, type HelpPublicPage, type HelpPublicPageLink, type HelpPublicPagePayload, type HelpPublicPageSummary, type HelpPublicPagesPayload, type HelpPublicPreviewPage, type HelpPublicPreviewPayload, type HelpPublicRedirect, type HelpPublicSearchHit, type HelpPublicSearchPayload, type HelpPublicSite, type HelpPublicSitePayload, type HelpPublicTreeFolder, type HelpPublicTreeLink, type HelpPublicTreeNode, type HelpPublicTreePage, type HelpPublicTreeSeparator, type HelpWebhookBody, type HelpWebhookEvent, type LangParams, type RequestOptions, SIGNATURE_HEADER, type SearchParams, SolutionDeskError, type VerifyWebhookOptions, type WebhookBody, createHelpClient, createMemoryEtagCache, normalizeHelpPath, verifyHelpWebhook, verifyWebhookSignature };
package/dist/index.d.ts CHANGED
@@ -153,6 +153,40 @@ interface HelpPublicSite {
153
153
  languages: string[];
154
154
  /** The docs website's base address (`https://help.acme.com/docs`), if set. */
155
155
  websiteUrl: string | null;
156
+ /** The site's own building blocks pages may use (empty when it defines none). */
157
+ blocks: HelpPublicBlockDefinition[];
158
+ }
159
+ /** What a field of a site's own block holds (see {@link HelpPublicBlockField}). */
160
+ type HelpPublicBlockFieldType = "text" | "longText" | "icon" | "url" | "choice" | "image" | "video" | "boolean";
161
+ /**
162
+ * One field of a site's own block: an attribute of its directive
163
+ * (`key="value"`). `boolean` fields are on when written bare (`key`) or as
164
+ * `key="true"`; every other value is a string. `options` lists a `choice`
165
+ * field's values (null for other types).
166
+ */
167
+ interface HelpPublicBlockField {
168
+ key: string;
169
+ label: string;
170
+ type: HelpPublicBlockFieldType;
171
+ required: boolean;
172
+ options: string[] | null;
173
+ }
174
+ /**
175
+ * A building block the site defines itself — WHAT it is; how it looks is the
176
+ * website's own component. Written in Markdown as `:name[label]{…}` in running
177
+ * text (`inline`), `::name[label]{…}` alone on a line (`block`) or
178
+ * `:::name{…}` … `:::` around Markdown content (`container`). `text` is the
179
+ * template of its text version (`{key}` = a field's value, a `boolean` field's
180
+ * label when on; `{{`/`}}` = a brace) — what to show without a component.
181
+ */
182
+ interface HelpPublicBlockDefinition {
183
+ /** The directive name (`app-button`). */
184
+ name: string;
185
+ /** Display name. */
186
+ label: string;
187
+ kind: "inline" | "block" | "container";
188
+ fields: HelpPublicBlockField[];
189
+ text: string;
156
190
  }
157
191
  /** A page in the navigation. */
158
192
  interface HelpPublicTreePage {
@@ -174,6 +208,8 @@ interface HelpPublicTreeFolder {
174
208
  /** The folder's path (its index page, if any, lives here; a group's is its parent's). */
175
209
  path: string;
176
210
  title: string;
211
+ /** A short line under a tab's name in the tab menu (Fumadocs `description`); null without one. */
212
+ description: string | null;
177
213
  icon: string | null;
178
214
  /** A tab (top level only; Fumadocs `root`). */
179
215
  root: boolean;
@@ -186,6 +222,11 @@ interface HelpPublicTreeFolder {
186
222
  collapsible: boolean;
187
223
  /** The folder's own page, shown at the folder's path; null without one. */
188
224
  index: HelpPublicTreePage | null;
225
+ /**
226
+ * List `index` as the folder's first entry (after a section title it starts
227
+ * with); the folder's name then only opens it (Fumadocs: `index` in `pages`).
228
+ */
229
+ listIndex: boolean;
189
230
  children: HelpPublicTreeNode[];
190
231
  }
191
232
  /** A labelled divider between entries (Fumadocs `---Title---`). */
@@ -262,12 +303,17 @@ interface HelpPublicPageLink {
262
303
  }
263
304
  /** One page with its text. */
264
305
  interface HelpPublicPage extends HelpPublicPageSummary {
265
- /** The page's Markdown with directives (`:::callout`, `::::steps`, …). */
306
+ /**
307
+ * The page's Markdown with directives (`:::callout`, `::::steps`, …, and the
308
+ * site's own blocks as written — see `site.blocks`).
309
+ */
266
310
  markdown: string;
267
311
  /** Safe server-rendered HTML of `markdown` (stable `sd-*` classes for the directives). */
268
312
  html: string;
269
313
  headings: HelpPublicHeading[];
270
314
  blocks: HelpPublicBlock[];
315
+ /** The site's own building blocks the page uses, by name (in the site's order). */
316
+ customBlocks: string[];
271
317
  breadcrumbs: HelpPublicBreadcrumb[];
272
318
  previous: HelpPublicPageLink | null;
273
319
  next: HelpPublicPageLink | null;
@@ -430,4 +476,4 @@ declare function createHelpClient(options: HelpClientOptions): HelpClient;
430
476
  */
431
477
  declare function verifyHelpWebhook(secret: string, rawBody: WebhookBody, signatureHeader: string | null | undefined, options?: VerifyWebhookOptions): Promise<HelpWebhookBody | null>;
432
478
 
433
- export { type BaseClientOptions, type CachedResponse, DEFAULT_BASE_URL, EVENT_HEADER, type EtagCache, type FetchInitLike, type FetchLike, type FetchResponseLike, type GetPageParams, type HelpClient, type HelpClientOptions, type HelpPageResult, type HelpPublicBlock, type HelpPublicBreadcrumb, type HelpPublicHeading, type HelpPublicPage, type HelpPublicPageLink, type HelpPublicPagePayload, type HelpPublicPageSummary, type HelpPublicPagesPayload, type HelpPublicPreviewPage, type HelpPublicPreviewPayload, type HelpPublicRedirect, type HelpPublicSearchHit, type HelpPublicSearchPayload, type HelpPublicSite, type HelpPublicSitePayload, type HelpPublicTreeFolder, type HelpPublicTreeLink, type HelpPublicTreeNode, type HelpPublicTreePage, type HelpPublicTreeSeparator, type HelpWebhookBody, type HelpWebhookEvent, type LangParams, type RequestOptions, SIGNATURE_HEADER, type SearchParams, SolutionDeskError, type VerifyWebhookOptions, type WebhookBody, createHelpClient, createMemoryEtagCache, normalizeHelpPath, verifyHelpWebhook, verifyWebhookSignature };
479
+ export { type BaseClientOptions, type CachedResponse, DEFAULT_BASE_URL, EVENT_HEADER, type EtagCache, type FetchInitLike, type FetchLike, type FetchResponseLike, type GetPageParams, type HelpClient, type HelpClientOptions, type HelpPageResult, type HelpPublicBlock, type HelpPublicBlockDefinition, type HelpPublicBlockField, type HelpPublicBlockFieldType, type HelpPublicBreadcrumb, type HelpPublicHeading, type HelpPublicPage, type HelpPublicPageLink, type HelpPublicPagePayload, type HelpPublicPageSummary, type HelpPublicPagesPayload, type HelpPublicPreviewPage, type HelpPublicPreviewPayload, type HelpPublicRedirect, type HelpPublicSearchHit, type HelpPublicSearchPayload, type HelpPublicSite, type HelpPublicSitePayload, type HelpPublicTreeFolder, type HelpPublicTreeLink, type HelpPublicTreeNode, type HelpPublicTreePage, type HelpPublicTreeSeparator, type HelpWebhookBody, type HelpWebhookEvent, type LangParams, type RequestOptions, SIGNATURE_HEADER, type SearchParams, SolutionDeskError, type VerifyWebhookOptions, type WebhookBody, createHelpClient, createMemoryEtagCache, normalizeHelpPath, verifyHelpWebhook, verifyWebhookSignature };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solutiondesk/help",
3
- "version": "1.0.0-1edea54",
3
+ "version": "1.0.0-2914fd8",
4
4
  "description": "Typed, zero-dependency client for the SolutionDesk public help center API, plus a Fumadocs adapter: navigation, pages, search, previews, llms.txt and webhook verification.",
5
5
  "keywords": [
6
6
  "solutiondesk",