@solutiondesk/blog 0.0.0-stage → 1.0.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/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,3 +1,195 @@
1
- # Temporary Holding Version
1
+ # @solutiondesk/blog
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Typed client for the public [SolutionDesk](https://solutiondesk.io) blog API. Your team writes the articles in SolutionDesk; your website reads them with this package.
4
+
5
+ - No dependencies, ESM and CommonJS, full TypeScript types.
6
+ - Runs wherever `fetch` does: Node.js 18+, Deno, Bun, edge runtimes and browsers.
7
+ - No key and no login: the API only ever returns published articles.
8
+
9
+ ```bash
10
+ npm install @solutiondesk/blog
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```ts
16
+ import { createBlogClient } from "@solutiondesk/blog";
17
+
18
+ const blog = createBlogClient({ workspace: "acme", lang: "en" });
19
+
20
+ const { posts, nextCursor } = await blog.listPosts({ limit: 10 });
21
+ const { post } = await blog.getPost("metadata-that-pays-off");
22
+ ```
23
+
24
+ `workspace` is your workspace's slug, the `acme` in the blog address shown under **Blog → Website** (`https://api.solutiondesk.io/v1/blog/acme`).
25
+
26
+ ## What you can read
27
+
28
+ | Method | API | Returns |
29
+ | --- | --- | --- |
30
+ | `listPosts({ lang, tag, author, limit, cursor })` | `GET /v1/blog/{workspace}/posts` | One page, newest first, without the text; `nextCursor` for the next page (`limit` 1–50, default 20). |
31
+ | `allPosts({ lang, tag, author })` | the same, page by page | An async iterator over every published post, e.g. for a sitemap. |
32
+ | `getPost(slug, { lang, redirect })` | `GET /v1/blog/{workspace}/posts/{slug}` | One post with `markdown`, `html`, `headings` and `readingMinutes`. |
33
+ | `listTags({ lang })` | `GET /v1/blog/{workspace}/tags` | Every tag with its label and how many published posts carry it. |
34
+ | `listAuthors({ lang })` | `GET /v1/blog/{workspace}/authors` | Every author with their role. |
35
+ | `getPreview(token, { lang })` | `GET /v1/blog/{workspace}/preview?token=` | The current state of a post, drafts too, for your preview route. |
36
+ | `imageUrl(url, width)` | | The image at the next offered width up (320, 640, 960, 1280, 1920). |
37
+
38
+ `html` is safe to insert as it is: no scripts, links only to web addresses and paths on your site, images only from your blog. Headings carry `id`s for a table of contents. A tag or author key that doesn't exist matches nothing, never everything.
39
+
40
+ ## Example: Next.js App Router
41
+
42
+ ```tsx
43
+ // app/blog/page.tsx
44
+ import { createBlogClient } from "@solutiondesk/blog";
45
+
46
+ const blog = createBlogClient({ workspace: "acme", lang: "en" });
47
+
48
+ export default async function BlogPage() {
49
+ const { posts } = await blog.listPosts({ limit: 20 }, { fetchOptions: { next: { tags: ["blog"] } } });
50
+ return (
51
+ <ul>
52
+ {posts.map((post) => (
53
+ <li key={post.slug} lang={post.locale}>
54
+ <a href={`/blog/${post.slug}`}>{post.title}</a> · {post.readingMinutes} min
55
+ </li>
56
+ ))}
57
+ </ul>
58
+ );
59
+ }
60
+ ```
61
+
62
+ ```tsx
63
+ // app/blog/[slug]/page.tsx
64
+ import { createBlogClient, SolutionDeskError } from "@solutiondesk/blog";
65
+ import { notFound, permanentRedirect } from "next/navigation";
66
+
67
+ const blog = createBlogClient({ workspace: "acme", lang: "en" });
68
+
69
+ export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
70
+ const { slug } = await params;
71
+ try {
72
+ const { post, movedFrom } = await blog.getPost(slug, {}, { fetchOptions: { next: { tags: ["blog"] } } });
73
+ if (movedFrom) permanentRedirect(`/blog/${post.slug}`);
74
+ return <article lang={post.locale} dangerouslySetInnerHTML={{ __html: post.html }} />;
75
+ } catch (error) {
76
+ if (error instanceof SolutionDeskError && error.isNotFound) notFound();
77
+ throw error;
78
+ }
79
+ }
80
+ ```
81
+
82
+ Anything that runs on a server works the same way: Astro, Remix, SvelteKit, Nuxt or a plain Node script that renders your site at build time.
83
+
84
+ ## Moved articles
85
+
86
+ When the team changes an article's address, the old slug answers with a `301` to the new one.
87
+
88
+ - Default (`redirect: "follow"`): the client follows it and returns the post. `movedFrom` holds the slug you asked for (else `null`) — redirect your page to `post.slug`, as in the example above.
89
+ - `redirect: "manual"`: you get `{ redirectTo: "new-slug" }` instead of the post.
90
+
91
+ ```ts
92
+ const result = await blog.getPost("old-slug", { redirect: "manual" });
93
+ if ("redirectTo" in result) return Response.redirect(`https://www.acme.com/blog/${result.redirectTo}`, 301);
94
+ ```
95
+
96
+ ## Languages
97
+
98
+ Pass `lang` to the client (default for every call) or per call. When an article doesn't exist in that language you get its main language. Every post says which language its text is really in (`locale`) and every language it exists in (`languages`, main language first) — use `locale` for `lang`, `hreflang` and structured data.
99
+
100
+ ```ts
101
+ const { post } = await blog.getPost("hello", { lang: "de" });
102
+ post.locale; // "de", or the main language when there's no German version
103
+ post.languages; // ["en", "de"]
104
+ ```
105
+
106
+ ## Images
107
+
108
+ Image addresses are public and cached for a year. Add a width with `imageUrl(url, width)` to get a WebP at most that wide; the cover has a ready `srcset` and its `width`/`height`. Images inside `html` already come with a `srcset`.
109
+
110
+ ```ts
111
+ // image-loader.ts — Next.js: images.loader "custom", images.loaderFile "./image-loader.ts"
112
+ import { imageUrl } from "@solutiondesk/blog";
113
+
114
+ export default function solutionDeskLoader({ src, width }: { src: string; width: number }) {
115
+ return imageUrl(src, width);
116
+ }
117
+ ```
118
+
119
+ ## Caching and ETags
120
+
121
+ Answers are sent with `Cache-Control: public, max-age=60, stale-while-revalidate=600` and an `ETag`. With `etagCache: true` the client remembers each answer's ETag and sends `If-None-Match`; an unchanged answer is a body-less `304` and you get the remembered data back.
122
+
123
+ ```ts
124
+ const blog = createBlogClient({ workspace: "acme", etagCache: true }); // or etagCache: new Map()
125
+ ```
126
+
127
+ Per call, `fetchOptions` is handed to `fetch` unchanged — use it for your framework's caching (Next.js `{ next: { revalidate: 300, tags: ["blog"] } }`) or `{ cache: "no-store" }`. Previews are never cached by the API; in Next.js pass `fetchOptions: { cache: "no-store" }` to `getPreview`.
128
+
129
+ ## Webhooks: refresh when something changes
130
+
131
+ Under **Blog → Website** set **Tell your website about changes** to a URL on your site and copy the signing secret (shown once). SolutionDesk then `POST`s whenever readers would see something different. Check the `X-SolutionDesk-Signature` header before acting on it:
132
+
133
+ ```ts
134
+ // app/api/solutiondesk/blog/route.ts (Next.js)
135
+ import { parseBlogWebhook } from "@solutiondesk/blog";
136
+ import { revalidatePath, revalidateTag } from "next/cache";
137
+
138
+ export async function POST(request: Request) {
139
+ const body = await parseBlogWebhook(
140
+ process.env.SOLUTIONDESK_BLOG_WEBHOOK_SECRET!,
141
+ await request.text(), // the raw body, exactly as received
142
+ request.headers.get("x-solutiondesk-signature"),
143
+ );
144
+ if (!body) return new Response("Invalid signature", { status: 401 });
145
+ if (body.event !== "ping") {
146
+ revalidateTag("blog");
147
+ for (const slug of [body.slug, ...body.previousSlugs]) if (slug) revalidatePath(`/blog/${slug}`);
148
+ }
149
+ return new Response(null, { status: 204 });
150
+ }
151
+ ```
152
+
153
+ `event` is `post.published`, `post.updated`, `post.unpublished`, `post.deleted` or `ping` (**Send a test**). `parseBlogWebhook` returns `null` for a wrong secret, a changed body, a malformed header or a signature older than five minutes. If you only need the check, use `verifyWebhookSignature(secret, rawBody, header)`, which resolves to `true` or `false`. It uses Web Crypto. On Node 18 pass `{ crypto: require("node:crypto").webcrypto }` as the fourth argument; Node 19 and later need nothing.
154
+
155
+ Answer with a `2xx` status; anything else is retried a few times.
156
+
157
+ ## Errors
158
+
159
+ Every failed request rejects with a `SolutionDeskError`:
160
+
161
+ ```ts
162
+ import { SolutionDeskError } from "@solutiondesk/blog";
163
+
164
+ try {
165
+ await blog.getPost("nope");
166
+ } catch (error) {
167
+ if (error instanceof SolutionDeskError) {
168
+ error.status; // 404 — unknown workspace, blog turned off or unknown post look the same on purpose
169
+ error.message; // the API's message, e.g. "This blog or article does not exist."
170
+ error.body; // the parsed error body
171
+ error.url; // the requested URL
172
+ }
173
+ }
174
+ ```
175
+
176
+ `status` is `0` when no answer arrived (network error, aborted request). `isNotFound` and `isRateLimited` (`429`) are shortcuts. Each address may be asked 600 times a minute per IP address, the preview 60 times.
177
+
178
+ ## Options
179
+
180
+ ```ts
181
+ createBlogClient({
182
+ workspace: "acme", // required
183
+ lang: "en", // default language
184
+ baseUrl: "https://api.solutiondesk.io", // the API origin
185
+ fetch: customFetch, // e.g. a fetch with tracing
186
+ headers: { "x-request-id": "…" }, // sent with every request
187
+ etagCache: true, // or your own Map
188
+ });
189
+ ```
190
+
191
+ Every method also takes request options as its last argument: `{ signal, headers, fetchOptions }`.
192
+
193
+ ## License
194
+
195
+ MIT © wemogy GmbH
package/dist/index.cjs ADDED
@@ -0,0 +1,355 @@
1
+ 'use strict';
2
+
3
+ var __defProp = Object.defineProperty;
4
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
5
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
6
+
7
+ // ../sdk-core/src/http.ts
8
+ var DEFAULT_BASE_URL = "https://api.solutiondesk.io";
9
+ var SolutionDeskError = class extends Error {
10
+ constructor(message, init) {
11
+ super(message);
12
+ /** The HTTP status; `0` when no answer arrived (network error, aborted). */
13
+ __publicField(this, "status");
14
+ /** The parsed JSON body of the error answer (usually `{ error: string }`), else its text. */
15
+ __publicField(this, "body");
16
+ /** The requested URL. */
17
+ __publicField(this, "url");
18
+ /** Messages per input field on a `400` (e.g. `{ email: "Enter a valid email address." }`). */
19
+ __publicField(this, "fields");
20
+ this.name = "SolutionDeskError";
21
+ this.status = init.status;
22
+ this.body = init.body ?? null;
23
+ this.url = init.url;
24
+ this.fields = fieldsOf(init.body);
25
+ if (init.cause !== void 0) {
26
+ Object.defineProperty(this, "cause", { value: init.cause, enumerable: false });
27
+ }
28
+ }
29
+ /** `404`: unknown workspace, module turned off or unknown item — on purpose indistinguishable. */
30
+ get isNotFound() {
31
+ return this.status === 404;
32
+ }
33
+ /** `429`: over the rate limit; try again later. */
34
+ get isRateLimited() {
35
+ return this.status === 429;
36
+ }
37
+ };
38
+ function fieldsOf(body) {
39
+ if (!body || typeof body !== "object") return {};
40
+ const fields = body.fields;
41
+ if (!fields || typeof fields !== "object") return {};
42
+ const out = {};
43
+ for (const [key, value] of Object.entries(fields)) {
44
+ if (typeof value === "string") out[key] = value;
45
+ }
46
+ return out;
47
+ }
48
+ function messageOf(body, status) {
49
+ if (body && typeof body === "object") {
50
+ const { error, message, statusCode } = body;
51
+ if (typeof statusCode === "number" && typeof message === "string" && message) return message;
52
+ if (typeof error === "string" && error) return error;
53
+ if (typeof message === "string" && message) return message;
54
+ if (Array.isArray(message) && typeof message[0] === "string") return message[0];
55
+ }
56
+ if (typeof body === "string" && body.trim() && body.length <= 300) return body.trim();
57
+ return `SolutionDesk answered ${status}.`;
58
+ }
59
+ function createMemoryEtagCache(maxEntries = 500) {
60
+ const entries = /* @__PURE__ */ new Map();
61
+ return {
62
+ get: (key) => entries.get(key),
63
+ set(key, value) {
64
+ entries.delete(key);
65
+ entries.set(key, value);
66
+ if (entries.size > maxEntries) {
67
+ const oldest = entries.keys().next().value;
68
+ if (oldest !== void 0) entries.delete(oldest);
69
+ }
70
+ }
71
+ };
72
+ }
73
+ function queryString(query) {
74
+ if (!query) return "";
75
+ const parts = [];
76
+ for (const [key, value] of Object.entries(query)) {
77
+ if (value === void 0 || value === null || value === "") continue;
78
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
79
+ }
80
+ return parts.length ? `?${parts.join("&")}` : "";
81
+ }
82
+ var seg = (value) => encodeURIComponent(String(value));
83
+ function parseBody(text) {
84
+ if (!text) return null;
85
+ try {
86
+ return JSON.parse(text);
87
+ } catch {
88
+ return text;
89
+ }
90
+ }
91
+ function createHttp(options = {}) {
92
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "").replace(/\/v1$/, "");
93
+ const fetchImpl = options.fetch ?? globalThis.fetch;
94
+ const cache = options.etagCache === true ? createMemoryEtagCache() : options.etagCache ? options.etagCache : void 0;
95
+ const url = (path, query) => `${baseUrl}/v1${path}${queryString(query)}`;
96
+ async function requestWithMeta(req) {
97
+ if (!fetchImpl) {
98
+ throw new Error("No global fetch available: pass `fetch` to the client.");
99
+ }
100
+ const target = url(req.path, req.query);
101
+ const headers = {
102
+ accept: "application/json",
103
+ ...options.headers,
104
+ ...req.options?.headers
105
+ };
106
+ if (req.body !== void 0) headers["content-type"] = "application/json";
107
+ if (req.apiKey) headers.authorization = `Bearer ${req.apiKey}`;
108
+ const cacheKey = req.method === "GET" && !req.apiKey ? target : void 0;
109
+ const cached = cacheKey && cache ? cache.get(cacheKey) : void 0;
110
+ if (cached && !Object.keys(headers).some((h) => h.toLowerCase() === "if-none-match")) {
111
+ headers["if-none-match"] = cached.etag;
112
+ }
113
+ const init = {
114
+ ...req.options?.fetchOptions,
115
+ method: req.method,
116
+ headers
117
+ };
118
+ if (req.body !== void 0) init.body = JSON.stringify(req.body);
119
+ if (req.options?.signal !== void 0) init.signal = req.options.signal;
120
+ let response;
121
+ try {
122
+ response = await fetchImpl(target, init);
123
+ } catch (cause) {
124
+ const reason = cause instanceof Error ? cause.message : String(cause);
125
+ throw new SolutionDeskError(`The request to SolutionDesk failed: ${reason}`, {
126
+ status: 0,
127
+ url: target,
128
+ cause
129
+ });
130
+ }
131
+ const finalUrl = response.url;
132
+ const meta = {
133
+ status: response.status,
134
+ finalUrl: typeof finalUrl === "string" && finalUrl ? finalUrl : null
135
+ };
136
+ if (response.status === 304 && cached) {
137
+ return { data: cached.body, ...meta };
138
+ }
139
+ const body = parseBody(await response.text());
140
+ if (!response.ok) {
141
+ throw new SolutionDeskError(messageOf(body, response.status), {
142
+ status: response.status,
143
+ body,
144
+ url: target
145
+ });
146
+ }
147
+ const etag = response.headers.get("etag");
148
+ if (cacheKey && cache && etag) cache.set(cacheKey, { etag, body });
149
+ return { data: body, ...meta };
150
+ }
151
+ return {
152
+ baseUrl,
153
+ url,
154
+ requestWithMeta,
155
+ request: async (req) => (await requestWithMeta(req)).data
156
+ };
157
+ }
158
+ function required(value, name) {
159
+ if (typeof value !== "string" || !value.trim()) {
160
+ throw new TypeError(`SolutionDesk: \`${name}\` is required.`);
161
+ }
162
+ return value;
163
+ }
164
+
165
+ // ../sdk-core/src/images.ts
166
+ var IMAGE_WIDTHS = [320, 640, 960, 1280, 1920];
167
+ function imageWidthFor(width) {
168
+ return IMAGE_WIDTHS.find((w) => w >= width) ?? 1920;
169
+ }
170
+ function imageUrl(url, width) {
171
+ if (width === void 0) return url;
172
+ const base = url.replace(/([?&])w=\d+(&|$)/, (_m, sep, end) => end ? sep : "");
173
+ const join = base.includes("?") ? /[?&]$/.test(base) ? "" : "&" : "?";
174
+ return `${base}${join}w=${imageWidthFor(width)}`;
175
+ }
176
+
177
+ // ../sdk-core/src/webhook.ts
178
+ var SIGNATURE_HEADER = "x-solutiondesk-signature";
179
+ var EVENT_HEADER = "x-solutiondesk-event";
180
+ function encoder() {
181
+ const Encoder = globalThis.TextEncoder;
182
+ if (!Encoder) throw new Error("TextEncoder is not available in this runtime.");
183
+ return new Encoder();
184
+ }
185
+ function bytesOf(body) {
186
+ if (typeof body === "string") return encoder().encode(body);
187
+ return body instanceof Uint8Array ? body : new Uint8Array(body);
188
+ }
189
+ function webhookBodyText(body) {
190
+ if (typeof body === "string") return body;
191
+ const Decoder = globalThis.TextDecoder;
192
+ if (!Decoder) throw new Error("TextDecoder is not available in this runtime.");
193
+ return new Decoder().decode(bytesOf(body));
194
+ }
195
+ function hex(buffer) {
196
+ let out = "";
197
+ for (const byte of new Uint8Array(buffer)) out += byte.toString(16).padStart(2, "0");
198
+ return out;
199
+ }
200
+ function constantTimeEqual(a, b) {
201
+ if (a.length !== b.length) return false;
202
+ let diff = 0;
203
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
204
+ return diff === 0;
205
+ }
206
+ function parseHeader(header) {
207
+ let t = Number.NaN;
208
+ const signatures = [];
209
+ for (const part of header.split(",")) {
210
+ const index = part.indexOf("=");
211
+ if (index < 0) continue;
212
+ const key = part.slice(0, index).trim();
213
+ const value = part.slice(index + 1).trim();
214
+ if (key === "t" && /^\d{1,12}$/.test(value)) t = Number(value);
215
+ else if (key === "v1" && /^[0-9a-f]{64}$/i.test(value)) signatures.push(value.toLowerCase());
216
+ }
217
+ return Number.isFinite(t) && signatures.length > 0 ? { t, signatures } : null;
218
+ }
219
+ async function verifyWebhookSignature(secret, rawBody, header, options = {}) {
220
+ if (!secret || !header || header.length > 1e3) return false;
221
+ const parsed = parseHeader(header);
222
+ if (!parsed) return false;
223
+ const now = options.now instanceof Date ? options.now.getTime() : options.now ?? Date.now();
224
+ const tolerance = options.toleranceSeconds ?? 300;
225
+ if (Math.abs(now / 1e3 - parsed.t) > tolerance) return false;
226
+ const subtle = options.crypto?.subtle ?? globalThis.crypto?.subtle;
227
+ if (!subtle) {
228
+ throw new Error(
229
+ "Web Crypto is not available: pass `crypto` (on Node 18: require('node:crypto').webcrypto)."
230
+ );
231
+ }
232
+ const text = encoder();
233
+ const key = await subtle.importKey(
234
+ "raw",
235
+ text.encode(secret),
236
+ { name: "HMAC", hash: "SHA-256" },
237
+ false,
238
+ ["sign"]
239
+ );
240
+ const prefix = text.encode(`${parsed.t}.`);
241
+ const body = bytesOf(rawBody);
242
+ const signed = new Uint8Array(prefix.length + body.length);
243
+ signed.set(prefix, 0);
244
+ signed.set(body, prefix.length);
245
+ const expected = hex(await subtle.sign("HMAC", key, signed));
246
+ let ok = false;
247
+ for (const signature of parsed.signatures) ok = constantTimeEqual(expected, signature) || ok;
248
+ return ok;
249
+ }
250
+ async function parseWebhook(secret, rawBody, header, options = {}) {
251
+ if (!await verifyWebhookSignature(secret, rawBody, header, options)) return null;
252
+ try {
253
+ return JSON.parse(webhookBodyText(rawBody));
254
+ } catch {
255
+ return null;
256
+ }
257
+ }
258
+
259
+ // src/client.ts
260
+ function createBlogClient(options) {
261
+ const workspace = required(options.workspace, "workspace");
262
+ const http = createHttp(options);
263
+ const base = `/blog/${seg(workspace)}`;
264
+ const langOf = (params) => params?.lang ?? options.lang;
265
+ const listPosts = (params = {}, requestOptions) => http.request({
266
+ method: "GET",
267
+ path: `${base}/posts`,
268
+ query: {
269
+ lang: langOf(params),
270
+ tag: params.tag,
271
+ author: params.author,
272
+ limit: params.limit,
273
+ cursor: params.cursor
274
+ },
275
+ options: requestOptions
276
+ });
277
+ const fetchPost = (slug, lang, requestOptions) => http.request({
278
+ method: "GET",
279
+ path: `${base}/posts/${seg(slug)}`,
280
+ query: { lang },
281
+ options: requestOptions
282
+ });
283
+ async function getPost(slug, params = {}, requestOptions) {
284
+ required(slug, "slug");
285
+ const lang = langOf(params);
286
+ const asked = slug.toLowerCase();
287
+ if (params.redirect === "manual") {
288
+ try {
289
+ return await fetchPost(slug, lang, {
290
+ ...requestOptions,
291
+ fetchOptions: { ...requestOptions?.fetchOptions, redirect: "manual" }
292
+ });
293
+ } catch (error) {
294
+ if (!(error instanceof SolutionDeskError)) throw error;
295
+ const redirectTo = error.body?.redirectTo;
296
+ if (error.status === 301 && typeof redirectTo === "string") return { redirectTo };
297
+ if (error.status !== 0) throw error;
298
+ }
299
+ const payload2 = await fetchPost(slug, lang, requestOptions);
300
+ return payload2.post.slug !== asked ? { redirectTo: payload2.post.slug } : payload2;
301
+ }
302
+ const payload = await fetchPost(slug, lang, requestOptions);
303
+ return { ...payload, movedFrom: payload.post.slug !== asked ? slug : null };
304
+ }
305
+ return {
306
+ workspace,
307
+ listPosts,
308
+ async *allPosts(params = {}, requestOptions) {
309
+ let cursor = null;
310
+ do {
311
+ const page = await listPosts(
312
+ { ...params, limit: params.limit ?? 50, cursor },
313
+ requestOptions
314
+ );
315
+ yield* page.posts;
316
+ cursor = page.nextCursor;
317
+ } while (cursor);
318
+ },
319
+ getPost,
320
+ listTags: async (params, requestOptions) => http.request({
321
+ method: "GET",
322
+ path: `${base}/tags`,
323
+ query: { lang: langOf(params) },
324
+ options: requestOptions
325
+ }),
326
+ listAuthors: async (params, requestOptions) => http.request({
327
+ method: "GET",
328
+ path: `${base}/authors`,
329
+ query: { lang: langOf(params) },
330
+ options: requestOptions
331
+ }),
332
+ getPreview: async (token, params, requestOptions) => http.request({
333
+ method: "GET",
334
+ path: `${base}/preview`,
335
+ query: { token: required(token, "token"), lang: langOf(params) },
336
+ options: requestOptions
337
+ }),
338
+ imageUrl
339
+ };
340
+ }
341
+ function parseBlogWebhook(secret, rawBody, signatureHeader, options) {
342
+ return parseWebhook(secret, rawBody, signatureHeader, options);
343
+ }
344
+
345
+ exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
346
+ exports.EVENT_HEADER = EVENT_HEADER;
347
+ exports.IMAGE_WIDTHS = IMAGE_WIDTHS;
348
+ exports.SIGNATURE_HEADER = SIGNATURE_HEADER;
349
+ exports.SolutionDeskError = SolutionDeskError;
350
+ exports.createBlogClient = createBlogClient;
351
+ exports.createMemoryEtagCache = createMemoryEtagCache;
352
+ exports.imageUrl = imageUrl;
353
+ exports.imageWidthFor = imageWidthFor;
354
+ exports.parseBlogWebhook = parseBlogWebhook;
355
+ exports.verifyWebhookSignature = verifyWebhookSignature;