chaingrow-blog 0.7.0 → 0.7.1
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/dist/{chunk-K3HNYPEP.js → chunk-N7QETVCO.js} +38 -17
- package/dist/chunk-N7QETVCO.js.map +1 -0
- package/dist/{chunk-BXEQ6QKM.js → chunk-NJH5N7E4.js} +2 -2
- package/dist/index.cjs +37 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/next/index.cjs +37 -16
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.js +2 -2
- package/dist/react/index.cjs +37 -16
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.js +2 -2
- package/package.json +18 -5
- package/dist/chunk-K3HNYPEP.js.map +0 -1
- /package/dist/{chunk-BXEQ6QKM.js.map → chunk-NJH5N7E4.js.map} +0 -0
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
// src/client.ts
|
|
2
|
+
var TRANSIENT_NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
3
|
+
"EAI_AGAIN",
|
|
4
|
+
"ETIMEDOUT",
|
|
5
|
+
"ECONNRESET",
|
|
6
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
7
|
+
"UND_ERR_SOCKET"
|
|
8
|
+
]);
|
|
9
|
+
var NETWORK_ATTEMPTS = 3;
|
|
10
|
+
function causeCode(err) {
|
|
11
|
+
const cause = err instanceof Error ? err.cause : void 0;
|
|
12
|
+
return cause && typeof cause === "object" && "code" in cause ? String(cause.code) : void 0;
|
|
13
|
+
}
|
|
2
14
|
function createClient(opts) {
|
|
3
15
|
const baseUrl = opts.apiUrl.replace(/\/$/, "");
|
|
4
16
|
const doFetch = opts.fetch ?? fetch;
|
|
@@ -14,24 +26,33 @@ function createClient(opts) {
|
|
|
14
26
|
}
|
|
15
27
|
});
|
|
16
28
|
let res;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (res.status === 429) {
|
|
20
|
-
const waitSec = parseInt(res.headers.get("Retry-After") ?? "1", 10);
|
|
21
|
-
await new Promise(
|
|
22
|
-
(resolve) => setTimeout(resolve, Math.max(0, waitSec) * 1e3)
|
|
23
|
-
);
|
|
29
|
+
for (let attempt = 1; attempt <= NETWORK_ATTEMPTS; attempt++) {
|
|
30
|
+
try {
|
|
24
31
|
res = await doFetch(url, buildInit());
|
|
32
|
+
if (res.status === 429) {
|
|
33
|
+
const waitSec = parseInt(res.headers.get("Retry-After") ?? "1", 10);
|
|
34
|
+
await new Promise(
|
|
35
|
+
(resolve) => setTimeout(resolve, Math.max(0, waitSec) * 1e3)
|
|
36
|
+
);
|
|
37
|
+
res = await doFetch(url, buildInit());
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
const code = causeCode(err);
|
|
42
|
+
if (attempt < NETWORK_ATTEMPTS && code !== void 0 && TRANSIENT_NETWORK_CODES.has(code)) {
|
|
43
|
+
await new Promise((resolve) => setTimeout(resolve, 250 * attempt));
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const detail = code ? `${code}${err instanceof Error && err.message ? ` (${err.message})` : ""}` : err instanceof Error ? err.message : String(err);
|
|
47
|
+
const e = new Error(
|
|
48
|
+
`chaingrow-blog: could not reach ${method} ${url} \u2014 ${detail}. Set CHAINGROW_API_URL to override the API host (default https://api.chaingrow.de).`
|
|
49
|
+
);
|
|
50
|
+
e.cause = err;
|
|
51
|
+
throw e;
|
|
25
52
|
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const detail = code ? `${code}${err instanceof Error && err.message ? ` (${err.message})` : ""}` : err instanceof Error ? err.message : String(err);
|
|
30
|
-
const e = new Error(
|
|
31
|
-
`chaingrow-blog: could not reach ${method} ${url} \u2014 ${detail}. Set CHAINGROW_API_URL to override the API host (default https://api.chaingrow.de).`
|
|
32
|
-
);
|
|
33
|
-
e.cause = err;
|
|
34
|
-
throw e;
|
|
53
|
+
}
|
|
54
|
+
if (!res) {
|
|
55
|
+
throw new Error(`chaingrow-blog: ${method} ${url} \u2014 no response`);
|
|
35
56
|
}
|
|
36
57
|
if (res.status === 404) return null;
|
|
37
58
|
if (!res.ok) {
|
|
@@ -173,4 +194,4 @@ export {
|
|
|
173
194
|
distributeMediaPositions,
|
|
174
195
|
interleaveMedia
|
|
175
196
|
};
|
|
176
|
-
//# sourceMappingURL=chunk-
|
|
197
|
+
//# sourceMappingURL=chunk-N7QETVCO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/json-ld.ts","../src/media-placement.ts"],"sourcesContent":["import type { Blog } from './types';\n\nexport type ClientOptions = {\n apiUrl: string;\n apiKey: string;\n fetch?: typeof fetch;\n};\n\nexport type ListBlogsOptions = {\n status?: 'DRAFT' | 'PUBLISHED';\n language?: string;\n};\n\nexport type ChaingrowClient = {\n getBlog(slug: string, language?: string): Promise<Blog | null>;\n getBlogById(id: string): Promise<Blog | null>;\n listBlogs(opts?: ListBlogsOptions): Promise<Blog[]>;\n};\n\n// Error codes that indicate a transient network hiccup rather than a broken\n// setup: flaky DNS resolution (musl containers throw EAI_AGAIN under load),\n// dropped or timed-out connections. Anything else (bad host, TLS failure)\n// fails fast so misconfiguration stays visible.\nconst TRANSIENT_NETWORK_CODES = new Set([\n 'EAI_AGAIN',\n 'ETIMEDOUT',\n 'ECONNRESET',\n 'UND_ERR_CONNECT_TIMEOUT',\n 'UND_ERR_SOCKET',\n]);\n\nconst NETWORK_ATTEMPTS = 3;\n\nfunction causeCode(err: unknown): string | undefined {\n const cause = err instanceof Error ? err.cause : undefined;\n return cause && typeof cause === 'object' && 'code' in cause\n ? String((cause as { code?: unknown }).code)\n : undefined;\n}\n\nexport function createClient(opts: ClientOptions): ChaingrowClient {\n const baseUrl = opts.apiUrl.replace(/\\/$/, '');\n const doFetch = opts.fetch ?? fetch;\n\n const request = async <T>(\n path: string,\n init?: RequestInit,\n ): Promise<T | null> => {\n const url = `${baseUrl}${path}`;\n const method = init?.method ?? 'GET';\n const buildInit = (): RequestInit => ({\n ...init,\n headers: {\n 'X-API-Key': opts.apiKey,\n Accept: 'application/json',\n ...(init?.headers ?? {}),\n },\n });\n\n let res: Response | undefined;\n for (let attempt = 1; attempt <= NETWORK_ATTEMPTS; attempt++) {\n try {\n res = await doFetch(url, buildInit());\n\n // Transparent one-shot retry on 429, honoring Retry-After (seconds).\n if (res.status === 429) {\n const waitSec = parseInt(res.headers.get('Retry-After') ?? '1', 10);\n await new Promise((resolve) =>\n setTimeout(resolve, Math.max(0, waitSec) * 1000),\n );\n res = await doFetch(url, buildInit());\n }\n break;\n } catch (err) {\n const code = causeCode(err);\n if (\n attempt < NETWORK_ATTEMPTS &&\n code !== undefined &&\n TRANSIENT_NETWORK_CODES.has(code)\n ) {\n await new Promise((resolve) => setTimeout(resolve, 250 * attempt));\n continue;\n }\n\n // `fetch failed` / DNS / connection errors land here. Node's default\n // message buries the host in `err.cause`, which makes integration\n // debugging miserable. Surface the URL and the override env var on\n // the top-level message so it appears in the consumer's server log.\n const detail = code\n ? `${code}${err instanceof Error && err.message ? ` (${err.message})` : ''}`\n : err instanceof Error\n ? err.message\n : String(err);\n const e = new Error(\n `chaingrow-blog: could not reach ${method} ${url} — ${detail}. ` +\n `Set CHAINGROW_API_URL to override the API host (default https://api.chaingrow.de).`,\n );\n (e as Error & { cause?: unknown }).cause = err;\n throw e;\n }\n }\n if (!res) {\n throw new Error(`chaingrow-blog: ${method} ${url} — no response`);\n }\n\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(\n `chaingrow-blog: ${method} ${url} failed (${res.status})`,\n );\n }\n return (await res.json()) as T;\n };\n\n return {\n async getBlog(slug, language) {\n const qs = language ? `?language=${encodeURIComponent(language)}` : '';\n return request<Blog>(\n `/api/v1/blogs/by-slug/${encodeURIComponent(slug)}${qs}`,\n );\n },\n\n async getBlogById(id) {\n return request<Blog>(`/api/v1/blogs/${encodeURIComponent(id)}`);\n },\n\n async listBlogs(listOpts = {}) {\n const collected: Blog[] = [];\n // Page through until the server-reported `total` is reached. Backends\n // without pagination return everything on the first request\n // (total === data.length), so this loop runs exactly once there.\n const MAX_PAGES = 100;\n for (let page = 0; page < MAX_PAGES; page++) {\n const params = new URLSearchParams();\n if (listOpts.status) params.set('status', listOpts.status);\n if (listOpts.language) params.set('language', listOpts.language);\n if (collected.length > 0)\n params.set('offset', String(collected.length));\n const qs = params.toString();\n const response = await request<{ data: Blog[]; total: number }>(\n `/api/v1/blogs${qs ? `?${qs}` : ''}`,\n );\n if (!response) break;\n const batch = response.data ?? [];\n collected.push(...batch);\n const total =\n typeof response.total === 'number' ? response.total : collected.length;\n if (batch.length === 0 || collected.length >= total) break;\n }\n // The language filter also runs server-side (API builds that predate\n // it ignore the query param) — keep this client-side pass as a\n // fallback; against a filtering server it's a no-op.\n if (listOpts.language) {\n return collected.filter((b) => b.language === listOpts.language);\n }\n return collected;\n },\n };\n}\n","import type { Blog, FaqItem } from './types';\n\n/**\n * Consumer-supplied extras for BlogPosting. Everything is optional — the\n * SDK doesn't know the customer's base URL, organisation name, or author\n * identity, so it just omits what it isn't told. Schema.org treats\n * `author`, `publisher`, and `url` as recommended but not required.\n */\nexport type BlogJsonLdOptions = {\n /** Absolute URL of the blog page (e.g. `https://example.com/blog/my-post`). */\n url?: string;\n author?: { name: string; type?: 'Person' | 'Organization'; url?: string };\n publisher?: { name: string; logo?: string };\n};\n\nconst Schema = Object.freeze({ context: 'https://schema.org' });\n\n/**\n * Build a Schema.org `BlogPosting` JSON-LD object from a chaingrow Blog.\n * Fields that aren't present on the Blog (description, og_image, …) are\n * omitted entirely so the resulting JSON has no `undefined` junk.\n */\nexport function buildBlogPostingSchema(\n blog: Blog,\n opts: BlogJsonLdOptions = {},\n): Record<string, unknown> {\n const schema: Record<string, unknown> = {\n '@type': 'BlogPosting',\n headline: blog.title,\n datePublished: blog.published_at,\n };\n if (blog.description) schema.description = blog.description;\n if (blog.og_image) schema.image = blog.og_image;\n if (opts.url) {\n schema.url = opts.url;\n schema.mainEntityOfPage = opts.url;\n }\n if (opts.author) {\n schema.author = {\n '@type': opts.author.type ?? 'Organization',\n name: opts.author.name,\n ...(opts.author.url ? { url: opts.author.url } : {}),\n };\n }\n if (opts.publisher) {\n schema.publisher = {\n '@type': 'Organization',\n name: opts.publisher.name,\n ...(opts.publisher.logo\n ? { logo: { '@type': 'ImageObject', url: opts.publisher.logo } }\n : {}),\n };\n }\n return schema;\n}\n\n/**\n * Build a Schema.org `FAQPage` JSON-LD object from chaingrow's `{question, answer}[]`\n * pairs. Returns `null` for empty input so callers can skip it with a single\n * `if (faqSchema) items.push(faqSchema)` / `filter(Boolean)`.\n */\nexport function buildFAQPageSchema(\n faq: FaqItem[] | null | undefined,\n): Record<string, unknown> | null {\n if (!faq || faq.length === 0) return null;\n return {\n '@type': 'FAQPage',\n mainEntity: faq.map(({ question, answer }) => ({\n '@type': 'Question',\n name: question,\n acceptedAnswer: { '@type': 'Answer', text: answer },\n })),\n };\n}\n\n/**\n * Combines BlogPosting + FAQPage + any LLM-generated `structured_data.jsonLd`\n * extras into a flat array ready for `@graph`. Tolerant of missing/malformed\n * `structured_data` — if the backend ever sends something unexpected we just\n * skip it rather than crashing the consumer's page.\n */\nexport function buildBlogJsonLd(\n blog: Blog,\n opts: BlogJsonLdOptions = {},\n): Record<string, unknown>[] {\n const items: Record<string, unknown>[] = [buildBlogPostingSchema(blog, opts)];\n const faq = buildFAQPageSchema(blog.faq);\n if (faq) items.push(faq);\n items.push(...extractLlmJsonLd(blog.structured_data));\n return items;\n}\n\n/** Schema-org context string — exposed so `<JsonLdGraph>` and any\n * bring-your-own renderer use the same value. */\nexport const SCHEMA_ORG_CONTEXT = Schema.context;\n\n/**\n * Pull the JSON-LD array out of `blog.structured_data`. Per\n * `structuredDataOutputSchema` in @chaingrow/contracts the expected shape is\n * `{ jsonLd: Array<Record<string, unknown>> }`, but historical rows may be\n * `null`, a plain object, or even malformed — normalise to an empty array in\n * every degenerate case so the caller gets a safe spread target.\n */\nfunction extractLlmJsonLd(\n sd: Blog['structured_data'],\n): Record<string, unknown>[] {\n if (!sd || typeof sd !== 'object') return [];\n const ld = (sd as { jsonLd?: unknown }).jsonLd;\n if (!Array.isArray(ld)) return [];\n return ld.filter(\n (entry): entry is Record<string, unknown> =>\n typeof entry === 'object' && entry !== null && !Array.isArray(entry),\n );\n}\n","import type { Block, EmbeddedMedia } from './types';\n\n/**\n * Compute the block-index insertion point for each media item when\n * distributing `mediaCount` attachments across `blockCount` blocks.\n *\n * Formula: media `i` goes at insertion index `floor(i * blockCount / mediaCount)`.\n * An insertion index of `0` means \"before block 0\" (top of body); `blockCount`\n * means \"after the last block\".\n *\n * Examples (blockCount = 10):\n * mediaCount = 1 → [0] (single media at top)\n * mediaCount = 2 → [0, 5] (top + middle)\n * mediaCount = 3 → [0, 3, 6] (top, 1/3, 2/3)\n * mediaCount = 4 → [0, 2, 5, 7] (every quarter)\n *\n * When `blockCount` is 0 every media falls at position 0 and stacks at the\n * top. When two media round to the same position they both land there and\n * render consecutively; this is deterministic but not always visually ideal\n * for very small `blockCount / mediaCount` ratios — assume content has\n * enough blocks to spread across.\n */\nexport function distributeMediaPositions(\n blockCount: number,\n mediaCount: number,\n): number[] {\n if (mediaCount <= 0) return [];\n if (blockCount <= 0) return Array.from({ length: mediaCount }, () => 0);\n return Array.from({ length: mediaCount }, (_, i) =>\n Math.floor((i * blockCount) / mediaCount),\n );\n}\n\n/** One interleaved slot in a blog's body — either a block or a media item. */\nexport type InterleavedSlot =\n | { kind: 'block'; block: Block; index: number }\n | { kind: 'media'; media: EmbeddedMedia; index: number };\n\n/**\n * Walk `blocks` once, splicing in `media` at the positions returned by\n * `distributeMediaPositions`. Returns a flat array of typed slots that\n * `<BlogRenderer>` can map over directly. Exposed so consumers who need a\n * custom layout (e.g. wrapping each section in a `<section>`) can use the\n * same placement logic without re-implementing it.\n */\nexport function interleaveMedia(\n blocks: Block[],\n media: EmbeddedMedia[] | undefined,\n): InterleavedSlot[] {\n const mediaList = media ?? [];\n const positions = distributeMediaPositions(blocks.length, mediaList.length);\n\n const slots: InterleavedSlot[] = [];\n let mediaCursor = 0;\n for (let b = 0; b <= blocks.length; b++) {\n while (mediaCursor < mediaList.length && positions[mediaCursor] === b) {\n const media = mediaList[mediaCursor]!;\n slots.push({ kind: 'media', media, index: mediaCursor });\n mediaCursor++;\n }\n if (b < blocks.length) {\n const block = blocks[b]!;\n slots.push({ kind: 'block', block, index: b });\n }\n }\n return slots;\n}\n"],"mappings":"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","names":["media"]}
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
buildBlogJsonLd,
|
|
4
4
|
createClient,
|
|
5
5
|
interleaveMedia
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-N7QETVCO.js";
|
|
7
7
|
|
|
8
8
|
// src/next/metadata.ts
|
|
9
9
|
var trimTrailingSlash = (s) => s.replace(/\/+$/, "");
|
|
@@ -633,4 +633,4 @@ export {
|
|
|
633
633
|
listBlogs,
|
|
634
634
|
getBlogSitemapEntries
|
|
635
635
|
};
|
|
636
|
-
//# sourceMappingURL=chunk-
|
|
636
|
+
//# sourceMappingURL=chunk-NJH5N7E4.js.map
|
package/dist/index.cjs
CHANGED
|
@@ -37,6 +37,18 @@ __export(src_exports, {
|
|
|
37
37
|
module.exports = __toCommonJS(src_exports);
|
|
38
38
|
|
|
39
39
|
// src/client.ts
|
|
40
|
+
var TRANSIENT_NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
41
|
+
"EAI_AGAIN",
|
|
42
|
+
"ETIMEDOUT",
|
|
43
|
+
"ECONNRESET",
|
|
44
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
45
|
+
"UND_ERR_SOCKET"
|
|
46
|
+
]);
|
|
47
|
+
var NETWORK_ATTEMPTS = 3;
|
|
48
|
+
function causeCode(err) {
|
|
49
|
+
const cause = err instanceof Error ? err.cause : void 0;
|
|
50
|
+
return cause && typeof cause === "object" && "code" in cause ? String(cause.code) : void 0;
|
|
51
|
+
}
|
|
40
52
|
function createClient(opts) {
|
|
41
53
|
const baseUrl = opts.apiUrl.replace(/\/$/, "");
|
|
42
54
|
const doFetch = opts.fetch ?? fetch;
|
|
@@ -52,24 +64,33 @@ function createClient(opts) {
|
|
|
52
64
|
}
|
|
53
65
|
});
|
|
54
66
|
let res;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (res.status === 429) {
|
|
58
|
-
const waitSec = parseInt(res.headers.get("Retry-After") ?? "1", 10);
|
|
59
|
-
await new Promise(
|
|
60
|
-
(resolve) => setTimeout(resolve, Math.max(0, waitSec) * 1e3)
|
|
61
|
-
);
|
|
67
|
+
for (let attempt = 1; attempt <= NETWORK_ATTEMPTS; attempt++) {
|
|
68
|
+
try {
|
|
62
69
|
res = await doFetch(url, buildInit());
|
|
70
|
+
if (res.status === 429) {
|
|
71
|
+
const waitSec = parseInt(res.headers.get("Retry-After") ?? "1", 10);
|
|
72
|
+
await new Promise(
|
|
73
|
+
(resolve) => setTimeout(resolve, Math.max(0, waitSec) * 1e3)
|
|
74
|
+
);
|
|
75
|
+
res = await doFetch(url, buildInit());
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
const code = causeCode(err);
|
|
80
|
+
if (attempt < NETWORK_ATTEMPTS && code !== void 0 && TRANSIENT_NETWORK_CODES.has(code)) {
|
|
81
|
+
await new Promise((resolve) => setTimeout(resolve, 250 * attempt));
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const detail = code ? `${code}${err instanceof Error && err.message ? ` (${err.message})` : ""}` : err instanceof Error ? err.message : String(err);
|
|
85
|
+
const e = new Error(
|
|
86
|
+
`chaingrow-blog: could not reach ${method} ${url} \u2014 ${detail}. Set CHAINGROW_API_URL to override the API host (default https://api.chaingrow.de).`
|
|
87
|
+
);
|
|
88
|
+
e.cause = err;
|
|
89
|
+
throw e;
|
|
63
90
|
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const detail = code ? `${code}${err instanceof Error && err.message ? ` (${err.message})` : ""}` : err instanceof Error ? err.message : String(err);
|
|
68
|
-
const e = new Error(
|
|
69
|
-
`chaingrow-blog: could not reach ${method} ${url} \u2014 ${detail}. Set CHAINGROW_API_URL to override the API host (default https://api.chaingrow.de).`
|
|
70
|
-
);
|
|
71
|
-
e.cause = err;
|
|
72
|
-
throw e;
|
|
91
|
+
}
|
|
92
|
+
if (!res) {
|
|
93
|
+
throw new Error(`chaingrow-blog: ${method} ${url} \u2014 no response`);
|
|
73
94
|
}
|
|
74
95
|
if (res.status === 404) return null;
|
|
75
96
|
if (!res.ok) {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/json-ld.ts","../src/media-placement.ts","../src/sitemap-builder.ts","../src/blog-cards.ts"],"sourcesContent":["export * from './types';\nexport { createClient, type ChaingrowClient, type ClientOptions } from './client';\nexport {\n buildBlogPostingSchema,\n buildFAQPageSchema,\n buildBlogJsonLd,\n SCHEMA_ORG_CONTEXT,\n type BlogJsonLdOptions,\n} from './json-ld';\nexport {\n distributeMediaPositions,\n interleaveMedia,\n type InterleavedSlot,\n} from './media-placement';\nexport {\n buildSitemapXml,\n buildNewsSitemapXml,\n filterRecentForNews,\n escapeXml,\n type SitemapBuildOptions,\n type NewsSitemapBuildOptions,\n} from './sitemap-builder';\nexport {\n toBlogCard,\n mergeBlogs,\n type BlogCard,\n type ToBlogCardOptions,\n} from './blog-cards';\n","import type { Blog } from './types';\n\nexport type ClientOptions = {\n apiUrl: string;\n apiKey: string;\n fetch?: typeof fetch;\n};\n\nexport type ListBlogsOptions = {\n status?: 'DRAFT' | 'PUBLISHED';\n language?: string;\n};\n\nexport type ChaingrowClient = {\n getBlog(slug: string, language?: string): Promise<Blog | null>;\n getBlogById(id: string): Promise<Blog | null>;\n listBlogs(opts?: ListBlogsOptions): Promise<Blog[]>;\n};\n\nexport function createClient(opts: ClientOptions): ChaingrowClient {\n const baseUrl = opts.apiUrl.replace(/\\/$/, '');\n const doFetch = opts.fetch ?? fetch;\n\n const request = async <T>(\n path: string,\n init?: RequestInit,\n ): Promise<T | null> => {\n const url = `${baseUrl}${path}`;\n const method = init?.method ?? 'GET';\n const buildInit = (): RequestInit => ({\n ...init,\n headers: {\n 'X-API-Key': opts.apiKey,\n Accept: 'application/json',\n ...(init?.headers ?? {}),\n },\n });\n\n let res: Response;\n try {\n res = await doFetch(url, buildInit());\n\n // Transparent one-shot retry on 429, honoring Retry-After (seconds).\n if (res.status === 429) {\n const waitSec = parseInt(res.headers.get('Retry-After') ?? '1', 10);\n await new Promise((resolve) =>\n setTimeout(resolve, Math.max(0, waitSec) * 1000),\n );\n res = await doFetch(url, buildInit());\n }\n } catch (err) {\n // `fetch failed` / DNS / connection errors land here. Node's default\n // message buries the host in `err.cause`, which makes integration\n // debugging miserable. Surface the URL and the override env var on\n // the top-level message so it appears in the consumer's server log.\n const cause = err instanceof Error ? err.cause : undefined;\n const code =\n cause && typeof cause === 'object' && 'code' in cause\n ? String((cause as { code?: unknown }).code)\n : undefined;\n const detail = code\n ? `${code}${err instanceof Error && err.message ? ` (${err.message})` : ''}`\n : err instanceof Error\n ? err.message\n : String(err);\n const e = new Error(\n `chaingrow-blog: could not reach ${method} ${url} — ${detail}. ` +\n `Set CHAINGROW_API_URL to override the API host (default https://api.chaingrow.de).`,\n );\n (e as Error & { cause?: unknown }).cause = err;\n throw e;\n }\n\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(\n `chaingrow-blog: ${method} ${url} failed (${res.status})`,\n );\n }\n return (await res.json()) as T;\n };\n\n return {\n async getBlog(slug, language) {\n const qs = language ? `?language=${encodeURIComponent(language)}` : '';\n return request<Blog>(\n `/api/v1/blogs/by-slug/${encodeURIComponent(slug)}${qs}`,\n );\n },\n\n async getBlogById(id) {\n return request<Blog>(`/api/v1/blogs/${encodeURIComponent(id)}`);\n },\n\n async listBlogs(listOpts = {}) {\n const collected: Blog[] = [];\n // Page through until the server-reported `total` is reached. Backends\n // without pagination return everything on the first request\n // (total === data.length), so this loop runs exactly once there.\n const MAX_PAGES = 100;\n for (let page = 0; page < MAX_PAGES; page++) {\n const params = new URLSearchParams();\n if (listOpts.status) params.set('status', listOpts.status);\n if (listOpts.language) params.set('language', listOpts.language);\n if (collected.length > 0)\n params.set('offset', String(collected.length));\n const qs = params.toString();\n const response = await request<{ data: Blog[]; total: number }>(\n `/api/v1/blogs${qs ? `?${qs}` : ''}`,\n );\n if (!response) break;\n const batch = response.data ?? [];\n collected.push(...batch);\n const total =\n typeof response.total === 'number' ? response.total : collected.length;\n if (batch.length === 0 || collected.length >= total) break;\n }\n // The language filter also runs server-side (API builds that predate\n // it ignore the query param) — keep this client-side pass as a\n // fallback; against a filtering server it's a no-op.\n if (listOpts.language) {\n return collected.filter((b) => b.language === listOpts.language);\n }\n return collected;\n },\n };\n}\n","import type { Blog, FaqItem } from './types';\n\n/**\n * Consumer-supplied extras for BlogPosting. Everything is optional — the\n * SDK doesn't know the customer's base URL, organisation name, or author\n * identity, so it just omits what it isn't told. Schema.org treats\n * `author`, `publisher`, and `url` as recommended but not required.\n */\nexport type BlogJsonLdOptions = {\n /** Absolute URL of the blog page (e.g. `https://example.com/blog/my-post`). */\n url?: string;\n author?: { name: string; type?: 'Person' | 'Organization'; url?: string };\n publisher?: { name: string; logo?: string };\n};\n\nconst Schema = Object.freeze({ context: 'https://schema.org' });\n\n/**\n * Build a Schema.org `BlogPosting` JSON-LD object from a chaingrow Blog.\n * Fields that aren't present on the Blog (description, og_image, …) are\n * omitted entirely so the resulting JSON has no `undefined` junk.\n */\nexport function buildBlogPostingSchema(\n blog: Blog,\n opts: BlogJsonLdOptions = {},\n): Record<string, unknown> {\n const schema: Record<string, unknown> = {\n '@type': 'BlogPosting',\n headline: blog.title,\n datePublished: blog.published_at,\n };\n if (blog.description) schema.description = blog.description;\n if (blog.og_image) schema.image = blog.og_image;\n if (opts.url) {\n schema.url = opts.url;\n schema.mainEntityOfPage = opts.url;\n }\n if (opts.author) {\n schema.author = {\n '@type': opts.author.type ?? 'Organization',\n name: opts.author.name,\n ...(opts.author.url ? { url: opts.author.url } : {}),\n };\n }\n if (opts.publisher) {\n schema.publisher = {\n '@type': 'Organization',\n name: opts.publisher.name,\n ...(opts.publisher.logo\n ? { logo: { '@type': 'ImageObject', url: opts.publisher.logo } }\n : {}),\n };\n }\n return schema;\n}\n\n/**\n * Build a Schema.org `FAQPage` JSON-LD object from chaingrow's `{question, answer}[]`\n * pairs. Returns `null` for empty input so callers can skip it with a single\n * `if (faqSchema) items.push(faqSchema)` / `filter(Boolean)`.\n */\nexport function buildFAQPageSchema(\n faq: FaqItem[] | null | undefined,\n): Record<string, unknown> | null {\n if (!faq || faq.length === 0) return null;\n return {\n '@type': 'FAQPage',\n mainEntity: faq.map(({ question, answer }) => ({\n '@type': 'Question',\n name: question,\n acceptedAnswer: { '@type': 'Answer', text: answer },\n })),\n };\n}\n\n/**\n * Combines BlogPosting + FAQPage + any LLM-generated `structured_data.jsonLd`\n * extras into a flat array ready for `@graph`. Tolerant of missing/malformed\n * `structured_data` — if the backend ever sends something unexpected we just\n * skip it rather than crashing the consumer's page.\n */\nexport function buildBlogJsonLd(\n blog: Blog,\n opts: BlogJsonLdOptions = {},\n): Record<string, unknown>[] {\n const items: Record<string, unknown>[] = [buildBlogPostingSchema(blog, opts)];\n const faq = buildFAQPageSchema(blog.faq);\n if (faq) items.push(faq);\n items.push(...extractLlmJsonLd(blog.structured_data));\n return items;\n}\n\n/** Schema-org context string — exposed so `<JsonLdGraph>` and any\n * bring-your-own renderer use the same value. */\nexport const SCHEMA_ORG_CONTEXT = Schema.context;\n\n/**\n * Pull the JSON-LD array out of `blog.structured_data`. Per\n * `structuredDataOutputSchema` in @chaingrow/contracts the expected shape is\n * `{ jsonLd: Array<Record<string, unknown>> }`, but historical rows may be\n * `null`, a plain object, or even malformed — normalise to an empty array in\n * every degenerate case so the caller gets a safe spread target.\n */\nfunction extractLlmJsonLd(\n sd: Blog['structured_data'],\n): Record<string, unknown>[] {\n if (!sd || typeof sd !== 'object') return [];\n const ld = (sd as { jsonLd?: unknown }).jsonLd;\n if (!Array.isArray(ld)) return [];\n return ld.filter(\n (entry): entry is Record<string, unknown> =>\n typeof entry === 'object' && entry !== null && !Array.isArray(entry),\n );\n}\n","import type { Block, EmbeddedMedia } from './types';\n\n/**\n * Compute the block-index insertion point for each media item when\n * distributing `mediaCount` attachments across `blockCount` blocks.\n *\n * Formula: media `i` goes at insertion index `floor(i * blockCount / mediaCount)`.\n * An insertion index of `0` means \"before block 0\" (top of body); `blockCount`\n * means \"after the last block\".\n *\n * Examples (blockCount = 10):\n * mediaCount = 1 → [0] (single media at top)\n * mediaCount = 2 → [0, 5] (top + middle)\n * mediaCount = 3 → [0, 3, 6] (top, 1/3, 2/3)\n * mediaCount = 4 → [0, 2, 5, 7] (every quarter)\n *\n * When `blockCount` is 0 every media falls at position 0 and stacks at the\n * top. When two media round to the same position they both land there and\n * render consecutively; this is deterministic but not always visually ideal\n * for very small `blockCount / mediaCount` ratios — assume content has\n * enough blocks to spread across.\n */\nexport function distributeMediaPositions(\n blockCount: number,\n mediaCount: number,\n): number[] {\n if (mediaCount <= 0) return [];\n if (blockCount <= 0) return Array.from({ length: mediaCount }, () => 0);\n return Array.from({ length: mediaCount }, (_, i) =>\n Math.floor((i * blockCount) / mediaCount),\n );\n}\n\n/** One interleaved slot in a blog's body — either a block or a media item. */\nexport type InterleavedSlot =\n | { kind: 'block'; block: Block; index: number }\n | { kind: 'media'; media: EmbeddedMedia; index: number };\n\n/**\n * Walk `blocks` once, splicing in `media` at the positions returned by\n * `distributeMediaPositions`. Returns a flat array of typed slots that\n * `<BlogRenderer>` can map over directly. Exposed so consumers who need a\n * custom layout (e.g. wrapping each section in a `<section>`) can use the\n * same placement logic without re-implementing it.\n */\nexport function interleaveMedia(\n blocks: Block[],\n media: EmbeddedMedia[] | undefined,\n): InterleavedSlot[] {\n const mediaList = media ?? [];\n const positions = distributeMediaPositions(blocks.length, mediaList.length);\n\n const slots: InterleavedSlot[] = [];\n let mediaCursor = 0;\n for (let b = 0; b <= blocks.length; b++) {\n while (mediaCursor < mediaList.length && positions[mediaCursor] === b) {\n const media = mediaList[mediaCursor]!;\n slots.push({ kind: 'media', media, index: mediaCursor });\n mediaCursor++;\n }\n if (b < blocks.length) {\n const block = blocks[b]!;\n slots.push({ kind: 'block', block, index: b });\n }\n }\n return slots;\n}\n","import type { Blog, EmbeddedMedia } from './types';\n\nconst NEWS_WINDOW_MS = 48 * 60 * 60 * 1000;\nconst NEWS_MAX_ENTRIES = 1000;\n\nexport type SitemapBuildOptions = {\n baseUrl: string;\n pathPrefix?: string;\n};\n\nexport type NewsSitemapBuildOptions = SitemapBuildOptions & {\n publicationName: string;\n defaultLanguage?: string;\n now?: Date;\n};\n\nexport function escapeXml(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nconst trimTrailingSlash = (s: string): string => s.replace(/\\/+$/, '');\n\nconst normalizePathPrefix = (s: string): string => {\n let p = s.trim();\n if (!p.startsWith('/')) p = '/' + p;\n while (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1);\n return p;\n};\n\nconst buildBlogUrl = (\n baseUrl: string,\n pathPrefix: string,\n slug: string,\n): string => `${baseUrl}${pathPrefix}/${slug}`;\n\nconst isImageMedia = (m: EmbeddedMedia): boolean => m.media_type === 'IMAGE';\n\nconst renderImageEntry = (m: EmbeddedMedia): string =>\n ` <image:image>\\n` +\n ` <image:loc>${escapeXml(m.media_url)}</image:loc>\\n` +\n ` </image:image>\\n`;\n\nconst lastmodFor = (blog: Blog): string => blog.published_at;\n\nexport function buildSitemapXml(\n blogs: Blog[],\n opts: SitemapBuildOptions,\n): string {\n const baseUrl = trimTrailingSlash(opts.baseUrl);\n const pathPrefix = normalizePathPrefix(opts.pathPrefix ?? '/blog');\n\n const entries = blogs\n .map((blog) => {\n const url = buildBlogUrl(baseUrl, pathPrefix, blog.slug);\n const images = (blog.generatedMedia ?? [])\n .filter(isImageMedia)\n .map(renderImageEntry)\n .join('');\n return (\n ` <url>\\n` +\n ` <loc>${escapeXml(url)}</loc>\\n` +\n ` <lastmod>${escapeXml(lastmodFor(blog))}</lastmod>\\n` +\n images +\n ` </url>\\n`\n );\n })\n .join('');\n\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n` +\n `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" ` +\n `xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\">\\n` +\n entries +\n `</urlset>\\n`\n );\n}\n\n/** Generic over anything carrying `published_at` so custom blog shapes\n * (or intersections with extra fields) pass through without a cast. */\nexport function filterRecentForNews<T extends Pick<Blog, 'published_at'>>(\n blogs: T[],\n now: Date = new Date(),\n): T[] {\n const cutoff = now.getTime() - NEWS_WINDOW_MS;\n const recent = blogs.filter((b) => {\n const t = Date.parse(b.published_at);\n return Number.isFinite(t) && t >= cutoff;\n });\n recent.sort(\n (a, b) => Date.parse(b.published_at) - Date.parse(a.published_at),\n );\n return recent.slice(0, NEWS_MAX_ENTRIES);\n}\n\nexport function buildNewsSitemapXml(\n blogs: Blog[],\n opts: NewsSitemapBuildOptions,\n): string {\n const baseUrl = trimTrailingSlash(opts.baseUrl);\n const pathPrefix = normalizePathPrefix(opts.pathPrefix ?? '/blog');\n const publicationName = opts.publicationName;\n const defaultLanguage = opts.defaultLanguage ?? 'en';\n\n const recent = filterRecentForNews(blogs, opts.now);\n\n const entries = recent\n .map((blog) => {\n const url = buildBlogUrl(baseUrl, pathPrefix, blog.slug);\n const language = blog.language || defaultLanguage;\n return (\n ` <url>\\n` +\n ` <loc>${escapeXml(url)}</loc>\\n` +\n ` <news:news>\\n` +\n ` <news:publication>\\n` +\n ` <news:name>${escapeXml(publicationName)}</news:name>\\n` +\n ` <news:language>${escapeXml(language)}</news:language>\\n` +\n ` </news:publication>\\n` +\n ` <news:publication_date>${escapeXml(blog.published_at)}</news:publication_date>\\n` +\n ` <news:title>${escapeXml(blog.title)}</news:title>\\n` +\n ` </news:news>\\n` +\n ` </url>\\n`\n );\n })\n .join('');\n\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n` +\n `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" ` +\n `xmlns:news=\"http://www.google.com/schemas/sitemap-news/0.9\">\\n` +\n entries +\n `</urlset>\\n`\n );\n}\n","import type { Blog } from './types';\n\nexport type BlogCard = {\n id: string;\n href: string;\n title: string;\n description: string | null;\n og_image: string | null;\n published_at: string | null;\n language?: string;\n};\n\nexport type ToBlogCardOptions = {\n /** URL prefix for the generated `href`. Defaults to `/blog`. */\n pathPrefix?: string;\n /**\n * Full control over the generated `href` — takes precedence over\n * `pathPrefix`. Use this when the route depends on more than a static\n * prefix, e.g. locale-prefixed routes:\n *\n * ```ts\n * hrefFor: (blog) =>\n * blog.language === 'de' ? `/blog/${blog.slug}` : `/${blog.language}/blog/${blog.slug}`\n * ```\n */\n hrefFor?: (blog: Blog) => string;\n};\n\nconst normalizePathPrefix = (s: string): string => {\n let p = s.trim();\n if (!p.startsWith('/')) p = '/' + p;\n while (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1);\n return p;\n};\n\n/**\n * Project a chaingrow `Blog` into the small `BlogCard` shape used by\n * `<BlogIndex>` and `mergeBlogs`. Hybrid integrators can map their own\n * legacy posts into the same shape (`extra` prop on `<BlogIndex>`) and\n * the two sources render through the same Card slot.\n */\nexport function toBlogCard(blog: Blog, opts: ToBlogCardOptions = {}): BlogCard {\n const prefix = normalizePathPrefix(opts.pathPrefix ?? '/blog');\n return {\n id: blog.id,\n href: opts.hrefFor ? opts.hrefFor(blog) : `${prefix}/${blog.slug}`,\n title: blog.title,\n description: blog.description,\n og_image: blog.og_image,\n published_at: blog.published_at,\n language: blog.language,\n };\n}\n\n/**\n * Merge a list of chaingrow cards with extra (legacy) cards and return a\n * single list sorted newest-first by `published_at`. Cards without a\n * `published_at` sort to the end.\n */\nexport function mergeBlogs(\n chaingrow: BlogCard[],\n extra: BlogCard[] = [],\n): BlogCard[] {\n const combined = [...chaingrow, ...extra];\n combined.sort((a, b) => {\n const ta = a.published_at ? Date.parse(a.published_at) : 0;\n const tb = b.published_at ? Date.parse(b.published_at) : 0;\n return tb - ta;\n });\n return combined;\n}\n"],"mappings":"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","names":["media","normalizePathPrefix"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/json-ld.ts","../src/media-placement.ts","../src/sitemap-builder.ts","../src/blog-cards.ts"],"sourcesContent":["export * from './types';\nexport { createClient, type ChaingrowClient, type ClientOptions } from './client';\nexport {\n buildBlogPostingSchema,\n buildFAQPageSchema,\n buildBlogJsonLd,\n SCHEMA_ORG_CONTEXT,\n type BlogJsonLdOptions,\n} from './json-ld';\nexport {\n distributeMediaPositions,\n interleaveMedia,\n type InterleavedSlot,\n} from './media-placement';\nexport {\n buildSitemapXml,\n buildNewsSitemapXml,\n filterRecentForNews,\n escapeXml,\n type SitemapBuildOptions,\n type NewsSitemapBuildOptions,\n} from './sitemap-builder';\nexport {\n toBlogCard,\n mergeBlogs,\n type BlogCard,\n type ToBlogCardOptions,\n} from './blog-cards';\n","import type { Blog } from './types';\n\nexport type ClientOptions = {\n apiUrl: string;\n apiKey: string;\n fetch?: typeof fetch;\n};\n\nexport type ListBlogsOptions = {\n status?: 'DRAFT' | 'PUBLISHED';\n language?: string;\n};\n\nexport type ChaingrowClient = {\n getBlog(slug: string, language?: string): Promise<Blog | null>;\n getBlogById(id: string): Promise<Blog | null>;\n listBlogs(opts?: ListBlogsOptions): Promise<Blog[]>;\n};\n\n// Error codes that indicate a transient network hiccup rather than a broken\n// setup: flaky DNS resolution (musl containers throw EAI_AGAIN under load),\n// dropped or timed-out connections. Anything else (bad host, TLS failure)\n// fails fast so misconfiguration stays visible.\nconst TRANSIENT_NETWORK_CODES = new Set([\n 'EAI_AGAIN',\n 'ETIMEDOUT',\n 'ECONNRESET',\n 'UND_ERR_CONNECT_TIMEOUT',\n 'UND_ERR_SOCKET',\n]);\n\nconst NETWORK_ATTEMPTS = 3;\n\nfunction causeCode(err: unknown): string | undefined {\n const cause = err instanceof Error ? err.cause : undefined;\n return cause && typeof cause === 'object' && 'code' in cause\n ? String((cause as { code?: unknown }).code)\n : undefined;\n}\n\nexport function createClient(opts: ClientOptions): ChaingrowClient {\n const baseUrl = opts.apiUrl.replace(/\\/$/, '');\n const doFetch = opts.fetch ?? fetch;\n\n const request = async <T>(\n path: string,\n init?: RequestInit,\n ): Promise<T | null> => {\n const url = `${baseUrl}${path}`;\n const method = init?.method ?? 'GET';\n const buildInit = (): RequestInit => ({\n ...init,\n headers: {\n 'X-API-Key': opts.apiKey,\n Accept: 'application/json',\n ...(init?.headers ?? {}),\n },\n });\n\n let res: Response | undefined;\n for (let attempt = 1; attempt <= NETWORK_ATTEMPTS; attempt++) {\n try {\n res = await doFetch(url, buildInit());\n\n // Transparent one-shot retry on 429, honoring Retry-After (seconds).\n if (res.status === 429) {\n const waitSec = parseInt(res.headers.get('Retry-After') ?? '1', 10);\n await new Promise((resolve) =>\n setTimeout(resolve, Math.max(0, waitSec) * 1000),\n );\n res = await doFetch(url, buildInit());\n }\n break;\n } catch (err) {\n const code = causeCode(err);\n if (\n attempt < NETWORK_ATTEMPTS &&\n code !== undefined &&\n TRANSIENT_NETWORK_CODES.has(code)\n ) {\n await new Promise((resolve) => setTimeout(resolve, 250 * attempt));\n continue;\n }\n\n // `fetch failed` / DNS / connection errors land here. Node's default\n // message buries the host in `err.cause`, which makes integration\n // debugging miserable. Surface the URL and the override env var on\n // the top-level message so it appears in the consumer's server log.\n const detail = code\n ? `${code}${err instanceof Error && err.message ? ` (${err.message})` : ''}`\n : err instanceof Error\n ? err.message\n : String(err);\n const e = new Error(\n `chaingrow-blog: could not reach ${method} ${url} — ${detail}. ` +\n `Set CHAINGROW_API_URL to override the API host (default https://api.chaingrow.de).`,\n );\n (e as Error & { cause?: unknown }).cause = err;\n throw e;\n }\n }\n if (!res) {\n throw new Error(`chaingrow-blog: ${method} ${url} — no response`);\n }\n\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(\n `chaingrow-blog: ${method} ${url} failed (${res.status})`,\n );\n }\n return (await res.json()) as T;\n };\n\n return {\n async getBlog(slug, language) {\n const qs = language ? `?language=${encodeURIComponent(language)}` : '';\n return request<Blog>(\n `/api/v1/blogs/by-slug/${encodeURIComponent(slug)}${qs}`,\n );\n },\n\n async getBlogById(id) {\n return request<Blog>(`/api/v1/blogs/${encodeURIComponent(id)}`);\n },\n\n async listBlogs(listOpts = {}) {\n const collected: Blog[] = [];\n // Page through until the server-reported `total` is reached. Backends\n // without pagination return everything on the first request\n // (total === data.length), so this loop runs exactly once there.\n const MAX_PAGES = 100;\n for (let page = 0; page < MAX_PAGES; page++) {\n const params = new URLSearchParams();\n if (listOpts.status) params.set('status', listOpts.status);\n if (listOpts.language) params.set('language', listOpts.language);\n if (collected.length > 0)\n params.set('offset', String(collected.length));\n const qs = params.toString();\n const response = await request<{ data: Blog[]; total: number }>(\n `/api/v1/blogs${qs ? `?${qs}` : ''}`,\n );\n if (!response) break;\n const batch = response.data ?? [];\n collected.push(...batch);\n const total =\n typeof response.total === 'number' ? response.total : collected.length;\n if (batch.length === 0 || collected.length >= total) break;\n }\n // The language filter also runs server-side (API builds that predate\n // it ignore the query param) — keep this client-side pass as a\n // fallback; against a filtering server it's a no-op.\n if (listOpts.language) {\n return collected.filter((b) => b.language === listOpts.language);\n }\n return collected;\n },\n };\n}\n","import type { Blog, FaqItem } from './types';\n\n/**\n * Consumer-supplied extras for BlogPosting. Everything is optional — the\n * SDK doesn't know the customer's base URL, organisation name, or author\n * identity, so it just omits what it isn't told. Schema.org treats\n * `author`, `publisher`, and `url` as recommended but not required.\n */\nexport type BlogJsonLdOptions = {\n /** Absolute URL of the blog page (e.g. `https://example.com/blog/my-post`). */\n url?: string;\n author?: { name: string; type?: 'Person' | 'Organization'; url?: string };\n publisher?: { name: string; logo?: string };\n};\n\nconst Schema = Object.freeze({ context: 'https://schema.org' });\n\n/**\n * Build a Schema.org `BlogPosting` JSON-LD object from a chaingrow Blog.\n * Fields that aren't present on the Blog (description, og_image, …) are\n * omitted entirely so the resulting JSON has no `undefined` junk.\n */\nexport function buildBlogPostingSchema(\n blog: Blog,\n opts: BlogJsonLdOptions = {},\n): Record<string, unknown> {\n const schema: Record<string, unknown> = {\n '@type': 'BlogPosting',\n headline: blog.title,\n datePublished: blog.published_at,\n };\n if (blog.description) schema.description = blog.description;\n if (blog.og_image) schema.image = blog.og_image;\n if (opts.url) {\n schema.url = opts.url;\n schema.mainEntityOfPage = opts.url;\n }\n if (opts.author) {\n schema.author = {\n '@type': opts.author.type ?? 'Organization',\n name: opts.author.name,\n ...(opts.author.url ? { url: opts.author.url } : {}),\n };\n }\n if (opts.publisher) {\n schema.publisher = {\n '@type': 'Organization',\n name: opts.publisher.name,\n ...(opts.publisher.logo\n ? { logo: { '@type': 'ImageObject', url: opts.publisher.logo } }\n : {}),\n };\n }\n return schema;\n}\n\n/**\n * Build a Schema.org `FAQPage` JSON-LD object from chaingrow's `{question, answer}[]`\n * pairs. Returns `null` for empty input so callers can skip it with a single\n * `if (faqSchema) items.push(faqSchema)` / `filter(Boolean)`.\n */\nexport function buildFAQPageSchema(\n faq: FaqItem[] | null | undefined,\n): Record<string, unknown> | null {\n if (!faq || faq.length === 0) return null;\n return {\n '@type': 'FAQPage',\n mainEntity: faq.map(({ question, answer }) => ({\n '@type': 'Question',\n name: question,\n acceptedAnswer: { '@type': 'Answer', text: answer },\n })),\n };\n}\n\n/**\n * Combines BlogPosting + FAQPage + any LLM-generated `structured_data.jsonLd`\n * extras into a flat array ready for `@graph`. Tolerant of missing/malformed\n * `structured_data` — if the backend ever sends something unexpected we just\n * skip it rather than crashing the consumer's page.\n */\nexport function buildBlogJsonLd(\n blog: Blog,\n opts: BlogJsonLdOptions = {},\n): Record<string, unknown>[] {\n const items: Record<string, unknown>[] = [buildBlogPostingSchema(blog, opts)];\n const faq = buildFAQPageSchema(blog.faq);\n if (faq) items.push(faq);\n items.push(...extractLlmJsonLd(blog.structured_data));\n return items;\n}\n\n/** Schema-org context string — exposed so `<JsonLdGraph>` and any\n * bring-your-own renderer use the same value. */\nexport const SCHEMA_ORG_CONTEXT = Schema.context;\n\n/**\n * Pull the JSON-LD array out of `blog.structured_data`. Per\n * `structuredDataOutputSchema` in @chaingrow/contracts the expected shape is\n * `{ jsonLd: Array<Record<string, unknown>> }`, but historical rows may be\n * `null`, a plain object, or even malformed — normalise to an empty array in\n * every degenerate case so the caller gets a safe spread target.\n */\nfunction extractLlmJsonLd(\n sd: Blog['structured_data'],\n): Record<string, unknown>[] {\n if (!sd || typeof sd !== 'object') return [];\n const ld = (sd as { jsonLd?: unknown }).jsonLd;\n if (!Array.isArray(ld)) return [];\n return ld.filter(\n (entry): entry is Record<string, unknown> =>\n typeof entry === 'object' && entry !== null && !Array.isArray(entry),\n );\n}\n","import type { Block, EmbeddedMedia } from './types';\n\n/**\n * Compute the block-index insertion point for each media item when\n * distributing `mediaCount` attachments across `blockCount` blocks.\n *\n * Formula: media `i` goes at insertion index `floor(i * blockCount / mediaCount)`.\n * An insertion index of `0` means \"before block 0\" (top of body); `blockCount`\n * means \"after the last block\".\n *\n * Examples (blockCount = 10):\n * mediaCount = 1 → [0] (single media at top)\n * mediaCount = 2 → [0, 5] (top + middle)\n * mediaCount = 3 → [0, 3, 6] (top, 1/3, 2/3)\n * mediaCount = 4 → [0, 2, 5, 7] (every quarter)\n *\n * When `blockCount` is 0 every media falls at position 0 and stacks at the\n * top. When two media round to the same position they both land there and\n * render consecutively; this is deterministic but not always visually ideal\n * for very small `blockCount / mediaCount` ratios — assume content has\n * enough blocks to spread across.\n */\nexport function distributeMediaPositions(\n blockCount: number,\n mediaCount: number,\n): number[] {\n if (mediaCount <= 0) return [];\n if (blockCount <= 0) return Array.from({ length: mediaCount }, () => 0);\n return Array.from({ length: mediaCount }, (_, i) =>\n Math.floor((i * blockCount) / mediaCount),\n );\n}\n\n/** One interleaved slot in a blog's body — either a block or a media item. */\nexport type InterleavedSlot =\n | { kind: 'block'; block: Block; index: number }\n | { kind: 'media'; media: EmbeddedMedia; index: number };\n\n/**\n * Walk `blocks` once, splicing in `media` at the positions returned by\n * `distributeMediaPositions`. Returns a flat array of typed slots that\n * `<BlogRenderer>` can map over directly. Exposed so consumers who need a\n * custom layout (e.g. wrapping each section in a `<section>`) can use the\n * same placement logic without re-implementing it.\n */\nexport function interleaveMedia(\n blocks: Block[],\n media: EmbeddedMedia[] | undefined,\n): InterleavedSlot[] {\n const mediaList = media ?? [];\n const positions = distributeMediaPositions(blocks.length, mediaList.length);\n\n const slots: InterleavedSlot[] = [];\n let mediaCursor = 0;\n for (let b = 0; b <= blocks.length; b++) {\n while (mediaCursor < mediaList.length && positions[mediaCursor] === b) {\n const media = mediaList[mediaCursor]!;\n slots.push({ kind: 'media', media, index: mediaCursor });\n mediaCursor++;\n }\n if (b < blocks.length) {\n const block = blocks[b]!;\n slots.push({ kind: 'block', block, index: b });\n }\n }\n return slots;\n}\n","import type { Blog, EmbeddedMedia } from './types';\n\nconst NEWS_WINDOW_MS = 48 * 60 * 60 * 1000;\nconst NEWS_MAX_ENTRIES = 1000;\n\nexport type SitemapBuildOptions = {\n baseUrl: string;\n pathPrefix?: string;\n};\n\nexport type NewsSitemapBuildOptions = SitemapBuildOptions & {\n publicationName: string;\n defaultLanguage?: string;\n now?: Date;\n};\n\nexport function escapeXml(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nconst trimTrailingSlash = (s: string): string => s.replace(/\\/+$/, '');\n\nconst normalizePathPrefix = (s: string): string => {\n let p = s.trim();\n if (!p.startsWith('/')) p = '/' + p;\n while (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1);\n return p;\n};\n\nconst buildBlogUrl = (\n baseUrl: string,\n pathPrefix: string,\n slug: string,\n): string => `${baseUrl}${pathPrefix}/${slug}`;\n\nconst isImageMedia = (m: EmbeddedMedia): boolean => m.media_type === 'IMAGE';\n\nconst renderImageEntry = (m: EmbeddedMedia): string =>\n ` <image:image>\\n` +\n ` <image:loc>${escapeXml(m.media_url)}</image:loc>\\n` +\n ` </image:image>\\n`;\n\nconst lastmodFor = (blog: Blog): string => blog.published_at;\n\nexport function buildSitemapXml(\n blogs: Blog[],\n opts: SitemapBuildOptions,\n): string {\n const baseUrl = trimTrailingSlash(opts.baseUrl);\n const pathPrefix = normalizePathPrefix(opts.pathPrefix ?? '/blog');\n\n const entries = blogs\n .map((blog) => {\n const url = buildBlogUrl(baseUrl, pathPrefix, blog.slug);\n const images = (blog.generatedMedia ?? [])\n .filter(isImageMedia)\n .map(renderImageEntry)\n .join('');\n return (\n ` <url>\\n` +\n ` <loc>${escapeXml(url)}</loc>\\n` +\n ` <lastmod>${escapeXml(lastmodFor(blog))}</lastmod>\\n` +\n images +\n ` </url>\\n`\n );\n })\n .join('');\n\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n` +\n `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" ` +\n `xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\">\\n` +\n entries +\n `</urlset>\\n`\n );\n}\n\n/** Generic over anything carrying `published_at` so custom blog shapes\n * (or intersections with extra fields) pass through without a cast. */\nexport function filterRecentForNews<T extends Pick<Blog, 'published_at'>>(\n blogs: T[],\n now: Date = new Date(),\n): T[] {\n const cutoff = now.getTime() - NEWS_WINDOW_MS;\n const recent = blogs.filter((b) => {\n const t = Date.parse(b.published_at);\n return Number.isFinite(t) && t >= cutoff;\n });\n recent.sort(\n (a, b) => Date.parse(b.published_at) - Date.parse(a.published_at),\n );\n return recent.slice(0, NEWS_MAX_ENTRIES);\n}\n\nexport function buildNewsSitemapXml(\n blogs: Blog[],\n opts: NewsSitemapBuildOptions,\n): string {\n const baseUrl = trimTrailingSlash(opts.baseUrl);\n const pathPrefix = normalizePathPrefix(opts.pathPrefix ?? '/blog');\n const publicationName = opts.publicationName;\n const defaultLanguage = opts.defaultLanguage ?? 'en';\n\n const recent = filterRecentForNews(blogs, opts.now);\n\n const entries = recent\n .map((blog) => {\n const url = buildBlogUrl(baseUrl, pathPrefix, blog.slug);\n const language = blog.language || defaultLanguage;\n return (\n ` <url>\\n` +\n ` <loc>${escapeXml(url)}</loc>\\n` +\n ` <news:news>\\n` +\n ` <news:publication>\\n` +\n ` <news:name>${escapeXml(publicationName)}</news:name>\\n` +\n ` <news:language>${escapeXml(language)}</news:language>\\n` +\n ` </news:publication>\\n` +\n ` <news:publication_date>${escapeXml(blog.published_at)}</news:publication_date>\\n` +\n ` <news:title>${escapeXml(blog.title)}</news:title>\\n` +\n ` </news:news>\\n` +\n ` </url>\\n`\n );\n })\n .join('');\n\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n` +\n `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" ` +\n `xmlns:news=\"http://www.google.com/schemas/sitemap-news/0.9\">\\n` +\n entries +\n `</urlset>\\n`\n );\n}\n","import type { Blog } from './types';\n\nexport type BlogCard = {\n id: string;\n href: string;\n title: string;\n description: string | null;\n og_image: string | null;\n published_at: string | null;\n language?: string;\n};\n\nexport type ToBlogCardOptions = {\n /** URL prefix for the generated `href`. Defaults to `/blog`. */\n pathPrefix?: string;\n /**\n * Full control over the generated `href` — takes precedence over\n * `pathPrefix`. Use this when the route depends on more than a static\n * prefix, e.g. locale-prefixed routes:\n *\n * ```ts\n * hrefFor: (blog) =>\n * blog.language === 'de' ? `/blog/${blog.slug}` : `/${blog.language}/blog/${blog.slug}`\n * ```\n */\n hrefFor?: (blog: Blog) => string;\n};\n\nconst normalizePathPrefix = (s: string): string => {\n let p = s.trim();\n if (!p.startsWith('/')) p = '/' + p;\n while (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1);\n return p;\n};\n\n/**\n * Project a chaingrow `Blog` into the small `BlogCard` shape used by\n * `<BlogIndex>` and `mergeBlogs`. Hybrid integrators can map their own\n * legacy posts into the same shape (`extra` prop on `<BlogIndex>`) and\n * the two sources render through the same Card slot.\n */\nexport function toBlogCard(blog: Blog, opts: ToBlogCardOptions = {}): BlogCard {\n const prefix = normalizePathPrefix(opts.pathPrefix ?? '/blog');\n return {\n id: blog.id,\n href: opts.hrefFor ? opts.hrefFor(blog) : `${prefix}/${blog.slug}`,\n title: blog.title,\n description: blog.description,\n og_image: blog.og_image,\n published_at: blog.published_at,\n language: blog.language,\n };\n}\n\n/**\n * Merge a list of chaingrow cards with extra (legacy) cards and return a\n * single list sorted newest-first by `published_at`. Cards without a\n * `published_at` sort to the end.\n */\nexport function mergeBlogs(\n chaingrow: BlogCard[],\n extra: BlogCard[] = [],\n): BlogCard[] {\n const combined = [...chaingrow, ...extra];\n combined.sort((a, b) => {\n const ta = a.published_at ? Date.parse(a.published_at) : 0;\n const tb = b.published_at ? Date.parse(b.published_at) : 0;\n return tb - ta;\n });\n return combined;\n}\n"],"mappings":"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","names":["media","normalizePathPrefix"]}
|
package/dist/index.js
CHANGED
package/dist/next/index.cjs
CHANGED
|
@@ -31,6 +31,18 @@ __export(next_exports, {
|
|
|
31
31
|
module.exports = __toCommonJS(next_exports);
|
|
32
32
|
|
|
33
33
|
// src/client.ts
|
|
34
|
+
var TRANSIENT_NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
35
|
+
"EAI_AGAIN",
|
|
36
|
+
"ETIMEDOUT",
|
|
37
|
+
"ECONNRESET",
|
|
38
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
39
|
+
"UND_ERR_SOCKET"
|
|
40
|
+
]);
|
|
41
|
+
var NETWORK_ATTEMPTS = 3;
|
|
42
|
+
function causeCode(err) {
|
|
43
|
+
const cause = err instanceof Error ? err.cause : void 0;
|
|
44
|
+
return cause && typeof cause === "object" && "code" in cause ? String(cause.code) : void 0;
|
|
45
|
+
}
|
|
34
46
|
function createClient(opts) {
|
|
35
47
|
const baseUrl = opts.apiUrl.replace(/\/$/, "");
|
|
36
48
|
const doFetch = opts.fetch ?? fetch;
|
|
@@ -46,24 +58,33 @@ function createClient(opts) {
|
|
|
46
58
|
}
|
|
47
59
|
});
|
|
48
60
|
let res;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (res.status === 429) {
|
|
52
|
-
const waitSec = parseInt(res.headers.get("Retry-After") ?? "1", 10);
|
|
53
|
-
await new Promise(
|
|
54
|
-
(resolve) => setTimeout(resolve, Math.max(0, waitSec) * 1e3)
|
|
55
|
-
);
|
|
61
|
+
for (let attempt = 1; attempt <= NETWORK_ATTEMPTS; attempt++) {
|
|
62
|
+
try {
|
|
56
63
|
res = await doFetch(url, buildInit());
|
|
64
|
+
if (res.status === 429) {
|
|
65
|
+
const waitSec = parseInt(res.headers.get("Retry-After") ?? "1", 10);
|
|
66
|
+
await new Promise(
|
|
67
|
+
(resolve) => setTimeout(resolve, Math.max(0, waitSec) * 1e3)
|
|
68
|
+
);
|
|
69
|
+
res = await doFetch(url, buildInit());
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const code = causeCode(err);
|
|
74
|
+
if (attempt < NETWORK_ATTEMPTS && code !== void 0 && TRANSIENT_NETWORK_CODES.has(code)) {
|
|
75
|
+
await new Promise((resolve) => setTimeout(resolve, 250 * attempt));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const detail = code ? `${code}${err instanceof Error && err.message ? ` (${err.message})` : ""}` : err instanceof Error ? err.message : String(err);
|
|
79
|
+
const e = new Error(
|
|
80
|
+
`chaingrow-blog: could not reach ${method} ${url} \u2014 ${detail}. Set CHAINGROW_API_URL to override the API host (default https://api.chaingrow.de).`
|
|
81
|
+
);
|
|
82
|
+
e.cause = err;
|
|
83
|
+
throw e;
|
|
57
84
|
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const detail = code ? `${code}${err instanceof Error && err.message ? ` (${err.message})` : ""}` : err instanceof Error ? err.message : String(err);
|
|
62
|
-
const e = new Error(
|
|
63
|
-
`chaingrow-blog: could not reach ${method} ${url} \u2014 ${detail}. Set CHAINGROW_API_URL to override the API host (default https://api.chaingrow.de).`
|
|
64
|
-
);
|
|
65
|
-
e.cause = err;
|
|
66
|
-
throw e;
|
|
85
|
+
}
|
|
86
|
+
if (!res) {
|
|
87
|
+
throw new Error(`chaingrow-blog: ${method} ${url} \u2014 no response`);
|
|
67
88
|
}
|
|
68
89
|
if (res.status === 404) return null;
|
|
69
90
|
if (!res.ok) {
|