@solutiondesk/help 0.0.0 → 1.0.0-69d9f69

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wemogy GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,45 +1,250 @@
1
1
  # @solutiondesk/help
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ Typed client for the public [SolutionDesk](https://solutiondesk.io) help center API, plus a ready-made adapter for [Fumadocs](https://fumadocs.dev). Your team writes the documentation in SolutionDesk; your docs website reads it with this package.
4
4
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
5
+ - The client has no dependencies: ESM and CommonJS, full TypeScript types, Node.js 18+, Deno, Bun, edge runtimes and browsers.
6
+ - No key and no login: the API only ever returns pages that are published, public and placed in the navigation.
7
+ - `@solutiondesk/help/fumadocs` turns a help site into a Fumadocs source and renders its pages with Fumadocs UI.
6
8
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
9
+ ```bash
10
+ npm install @solutiondesk/help
11
+ ```
8
12
 
9
- ## Purpose
13
+ The guide [Help center on your website](https://help.solutiondesk.io/docs/team/help-center-website) explains the API behind this package and where to set it up in SolutionDesk.
10
14
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@solutiondesk/help`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
15
+ ## Quick start
15
16
 
16
- ## What is OIDC Trusted Publishing?
17
+ ```ts
18
+ import { createHelpClient } from "@solutiondesk/help";
17
19
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
20
+ const help = createHelpClient({ workspace: "acme", site: "docs", lang: "en" });
19
21
 
20
- ## Setup Instructions
22
+ const { site, tree } = await help.site(); // the navigation
23
+ const { page } = await help.page("guides/login"); // one page with its text
24
+ ```
21
25
 
22
- To properly configure OIDC trusted publishing for this package:
26
+ `workspace` is your workspace's slug and `site` the doc site's slug, the `acme` and `docs` in `https://api.solutiondesk.io/v1/help/acme/docs`.
23
27
 
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
28
+ ## What you can read
28
29
 
29
- ## DO NOT USE THIS PACKAGE
30
+ | Method | API | Returns |
31
+ | --- | --- | --- |
32
+ | `site({ lang })` | `GET …/site` | The site (`name`, `languages`, `websiteUrl`) and its navigation tree: pages, folders (with index page, `root` tab, `defaultOpen`, `collapsible`), separators and links. Empty folders are left out. |
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 `""`. |
35
+ | `pageMarkdown(path, { lang })` | `GET …/page.md?path=` | One page as plain Markdown. |
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
+ | `preview(token, { lang })` | `GET …/preview?token=` | A page in any state (drafts too) for your preview route. |
38
+ | `llmsTxt({ lang })` / `llmsFullTxt({ lang })` | `GET …/llms.txt`, `…/llms-full.txt` | The site for language models: the index, or every page as Markdown. |
30
39
 
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
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`.
36
41
 
37
- ## More Information
42
+ ### Moved pages
38
43
 
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
44
+ When the team renames or moves a page, its old path answers with a `301` to the new one.
42
45
 
43
- ---
46
+ - Default (`redirect: "follow"`): you get the page; `movedFrom` holds the path you asked for (else `null`). Redirect your visitor to `page.path`.
47
+ - `redirect: "manual"`: you get `{ redirectTo: "new/path" }` instead.
44
48
 
45
- **Maintained for OIDC setup purposes only**
49
+ ### Languages
50
+
51
+ Pass `lang` to the client (default for every call) or per call. A page that doesn't exist in that language comes in the site's main language. Every answer names the language it is really in (`locale`) and the site's languages (`languages`, main language first).
52
+
53
+ ### Errors, caching, options
54
+
55
+ Every failed request rejects with a `SolutionDeskError` (`status`, `message`, `body`, `url`, `isNotFound`, `isRateLimited`); `status` is `0` when no answer arrived. A missing page, a hidden page, a turned-off module and an unknown site all answer `404`, on purpose alike.
56
+
57
+ Answers carry `Cache-Control: public, max-age=60, must-revalidate` and an `ETag`. `etagCache: true` makes the client revalidate with `If-None-Match`. Per call, `fetchOptions` goes to `fetch` unchanged, e.g. Next.js `{ next: { tags: ["solutiondesk-help"], revalidate: 3600 } }` or `{ cache: "no-store" }` for previews.
58
+
59
+ ```ts
60
+ createHelpClient({
61
+ workspace: "acme", // required
62
+ site: "docs", // required
63
+ lang: "en", // default language
64
+ baseUrl: "https://api.solutiondesk.io", // the API origin
65
+ fetch: customFetch,
66
+ headers: { "x-request-id": "…" },
67
+ etagCache: true,
68
+ });
69
+ ```
70
+
71
+ ## Fumadocs
72
+
73
+ `@solutiondesk/help/fumadocs` needs `fumadocs-core`, `fumadocs-ui` (16.x) and `react` 19, which a Fumadocs site has anyway. It is ESM only and tested against Fumadocs 16.14.
74
+
75
+ ### The source
76
+
77
+ ```ts
78
+ // lib/help.ts
79
+ import { createHelpClient } from "@solutiondesk/help";
80
+ import { createHelpSource } from "@solutiondesk/help/fumadocs";
81
+ import { loader } from "fumadocs-core/source";
82
+ import { lucideIconsPlugin } from "fumadocs-core/source/plugins/lucide-icons";
83
+ import { cache } from "react";
84
+
85
+ export const HELP_TAG = "solutiondesk-help";
86
+ export const help = createHelpClient({ workspace: "acme", site: "docs" });
87
+
88
+ // Built per request from the cached navigation: after the webhook revalidates
89
+ // the tag, the next request sees the new tree.
90
+ export const getSource = cache(async () =>
91
+ loader({
92
+ baseUrl: "/docs",
93
+ source: await createHelpSource(help, {
94
+ requestOptions: { fetchOptions: { next: { tags: [HELP_TAG], revalidate: 3600 } } },
95
+ }),
96
+ plugins: [lucideIconsPlugin()], // the icons are Lucide names
97
+ }),
98
+ );
99
+ ```
100
+
101
+ The navigation becomes the files Fumadocs knows from a content folder:
102
+
103
+ | SolutionDesk | Fumadocs |
104
+ | --- | --- |
105
+ | page `guides/login` | `guides/login.mdx` (`slugs: ["guides", "login"]`) |
106
+ | home page | `index.mdx` (always at the top) |
107
+ | folder `guides` | `guides/meta.json` with `title`, `icon`, `root`, `defaultOpen`, `collapsible`, `pages` |
108
+ | folder's own page | `guides/index.mdx` (`pagesIndex: "index"`, so it also works for `root` tabs) |
109
+ | any other page with the slug `index` | `guides/index_.mdx` (`slugs: ["guides", "index"]`), so it never collides with the folder's own page |
110
+ | order of the top level | `meta.json` (`title` = site name) |
111
+ | separator | `"---[Icon]Title---"` (a separator without a title is `"---"`) |
112
+ | link | `"[Icon][Title](url)"`, `"external:[Icon][Title](url)"` |
113
+
114
+ 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
+
116
+ **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.
117
+
118
+ **Several languages.** Pass the languages of your Fumadocs i18n config. The source then holds one set of files per language with Fumadocs' default `dot` naming (`guides/login.en.mdx`, `guides/meta.en.json`):
119
+
120
+ ```ts
121
+ loader({
122
+ baseUrl: "/docs",
123
+ i18n: defineI18n({ languages: ["de", "en"], defaultLanguage: "de" }),
124
+ source: await createHelpSource(help, { languages: ["de", "en"] }),
125
+ });
126
+ ```
127
+
128
+ `helpDynamicSource(help, options)` is the same for `dynamicLoader()` from `fumadocs-core/source/dynamic`.
129
+
130
+ ### The page
131
+
132
+ ```tsx
133
+ // app/docs/[[...slug]]/page.tsx
134
+ import { getHelpToc, HelpBody } from "@solutiondesk/help/fumadocs";
135
+ import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/layouts/docs/page";
136
+ import { icons } from "lucide-react";
137
+ import { notFound } from "next/navigation";
138
+ import { getSource } from "@/lib/help";
139
+
140
+ const icon = (name: string) => {
141
+ const Icon = icons[name as keyof typeof icons];
142
+ return Icon ? <Icon /> : null;
143
+ };
144
+
145
+ export default async function Page({ params }: { params: Promise<{ slug?: string[] }> }) {
146
+ const entry = (await getSource()).getPage((await params).slug);
147
+ if (!entry) notFound();
148
+ const page = await entry.data.load();
149
+ if (!page) notFound(); // taken offline since the navigation was cached
150
+ return (
151
+ <DocsPage toc={getHelpToc(page.headings)}>
152
+ <DocsTitle>{page.title}</DocsTitle>
153
+ <DocsDescription>{page.description}</DocsDescription>
154
+ <DocsBody>
155
+ <HelpBody markdown={page.markdown} headings={page.headings} icon={icon} />
156
+ </DocsBody>
157
+ </DocsPage>
158
+ );
159
+ }
160
+
161
+ export async function generateStaticParams() {
162
+ return (await getSource()).generateParams();
163
+ }
164
+ ```
165
+
166
+ `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
+
168
+ ```tsx
169
+ import { DynamicCodeBlock } from "fumadocs-ui/components/dynamic-codeblock";
170
+
171
+ <HelpBody
172
+ markdown={page.markdown}
173
+ headings={page.headings}
174
+ components={{ Code: ({ code, lang, title }) => <DynamicCodeBlock lang={lang ?? "text"} code={code} codeblock={{ title }} /> }}
175
+ />;
176
+ ```
177
+
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.
179
+
180
+ ### Refresh when something changes
181
+
182
+ 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:
183
+
184
+ ```ts
185
+ // app/api/solutiondesk/help/route.ts
186
+ import { createHelpRevalidateHandler } from "@solutiondesk/help/fumadocs";
187
+ import { revalidateTag } from "next/cache";
188
+ import { HELP_TAG } from "@/lib/help";
189
+
190
+ export const POST = createHelpRevalidateHandler({
191
+ secret: process.env.SOLUTIONDESK_HELP_WEBHOOK_SECRET ?? "",
192
+ revalidate: () => revalidateTag(HELP_TAG, "max"),
193
+ });
194
+ ```
195
+
196
+ The handler works with any framework that has `Request` and `Response`. Events: `page.published`, `page.updated`, `page.unpublished`, `page.deleted`, `navigation.updated` and `ping` (the test call, which doesn't call `revalidate`). Without the handler, check calls yourself with `verifyHelpWebhook(secret, rawBody, header)`, which returns the body or `null`.
197
+
198
+ ### Preview with Draft Mode
199
+
200
+ Set the site's preview address to `https://help.acme.com/api/preview?token={token}&path={path}`:
201
+
202
+ ```ts
203
+ // app/api/preview/route.ts
204
+ import { cookies, draftMode } from "next/headers";
205
+ import { redirect } from "next/navigation";
206
+
207
+ export async function GET(request: Request) {
208
+ const url = new URL(request.url);
209
+ const token = url.searchParams.get("token");
210
+ if (!token) return new Response("Missing token", { status: 400 });
211
+ (await draftMode()).enable();
212
+ (await cookies()).set("sd-help-preview", token, { httpOnly: true, sameSite: "lax", maxAge: 3600 });
213
+ redirect(`/docs/${(url.searchParams.get("path") ?? "").replace(/[^a-z0-9/-]/g, "")}`);
214
+ }
215
+ ```
216
+
217
+ In the page, read the page from the preview while Draft Mode is on:
218
+
219
+ ```ts
220
+ if ((await draftMode()).isEnabled) {
221
+ const token = (await cookies()).get("sd-help-preview")?.value;
222
+ if (token) return (await help.preview(token, {}, { fetchOptions: { cache: "no-store" } })).page;
223
+ }
224
+ ```
225
+
226
+ A complete Next.js app with these files is in [`examples/fumadocs-next`](./examples/fumadocs-next).
227
+
228
+ ## Any other website
229
+
230
+ Without Fumadocs, use `page.html` and style its stable classes:
231
+
232
+ ```ts
233
+ const { page, movedFrom } = await help.page(path);
234
+ if (movedFrom) redirect(`/docs/${page.path}`);
235
+ document.querySelector("article")!.innerHTML = page.html;
236
+ ```
237
+
238
+ | Block | HTML |
239
+ | --- | --- |
240
+ | 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">` |
241
+ | steps | `<ol class="sd-steps">` of `<li class="sd-step">` with `<p class="sd-step-title">` and `<div class="sd-step-body">` |
242
+ | 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) |
243
+ | 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">` |
244
+ | titled code | `<figure class="sd-code"><figcaption>client.ts</figcaption><pre><code class="language-ts">` |
245
+
246
+ Build the table of contents from `page.headings` (`#id`), the breadcrumb from `page.breadcrumbs` and the footer links from `page.previous`/`page.next`.
247
+
248
+ ## License
249
+
250
+ MIT © wemogy GmbH