chaingrow-blog 0.2.2 → 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.
@@ -20,12 +20,29 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/next/index.ts
21
21
  var next_exports = {};
22
22
  __export(next_exports, {
23
+ BlogPage: () => BlogPage,
24
+ generateBlogIndexMetadata: () => generateBlogIndexMetadata,
25
+ generateBlogMetadata: () => generateBlogMetadata,
26
+ generateBlogPageMetadata: () => generateBlogPageMetadata,
23
27
  getBlog: () => getBlog,
28
+ getBlogSitemapEntries: () => getBlogSitemapEntries,
24
29
  listBlogs: () => listBlogs
25
30
  });
26
31
  module.exports = __toCommonJS(next_exports);
27
32
 
28
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
+ }
29
46
  function createClient(opts) {
30
47
  const baseUrl = opts.apiUrl.replace(/\/$/, "");
31
48
  const doFetch = opts.fetch ?? fetch;
@@ -41,24 +58,33 @@ function createClient(opts) {
41
58
  }
42
59
  });
43
60
  let res;
44
- try {
45
- res = await doFetch(url, buildInit());
46
- if (res.status === 429) {
47
- const waitSec = parseInt(res.headers.get("Retry-After") ?? "1", 10);
48
- await new Promise(
49
- (resolve) => setTimeout(resolve, Math.max(0, waitSec) * 1e3)
50
- );
61
+ for (let attempt = 1; attempt <= NETWORK_ATTEMPTS; attempt++) {
62
+ try {
51
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;
52
84
  }
53
- } catch (err) {
54
- const cause = err instanceof Error ? err.cause : void 0;
55
- const code = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : void 0;
56
- const detail = code ? `${code}${err instanceof Error && err.message ? ` (${err.message})` : ""}` : err instanceof Error ? err.message : String(err);
57
- const e = new Error(
58
- `chaingrow-blog: could not reach ${method} ${url} \u2014 ${detail}. Set CHAINGROW_API_URL to override the API host (default https://api.chaingrow.de).`
59
- );
60
- e.cause = err;
61
- throw e;
85
+ }
86
+ if (!res) {
87
+ throw new Error(`chaingrow-blog: ${method} ${url} \u2014 no response`);
62
88
  }
63
89
  if (res.status === 404) return null;
64
90
  if (!res.ok) {
@@ -79,24 +105,661 @@ function createClient(opts) {
79
105
  return request(`/api/v1/blogs/${encodeURIComponent(id)}`);
80
106
  },
81
107
  async listBlogs(listOpts = {}) {
82
- const params = new URLSearchParams();
83
- if (listOpts.status) params.set("status", listOpts.status);
84
- const qs = params.toString();
85
- const response = await request(
86
- `/api/v1/blogs${qs ? `?${qs}` : ""}`
87
- );
88
- if (!response) return [];
89
- const blogs = response.data ?? [];
108
+ const collected = [];
109
+ const MAX_PAGES = 100;
110
+ for (let page = 0; page < MAX_PAGES; page++) {
111
+ const params = new URLSearchParams();
112
+ if (listOpts.status) params.set("status", listOpts.status);
113
+ if (listOpts.language) params.set("language", listOpts.language);
114
+ if (collected.length > 0)
115
+ params.set("offset", String(collected.length));
116
+ const qs = params.toString();
117
+ const response = await request(
118
+ `/api/v1/blogs${qs ? `?${qs}` : ""}`
119
+ );
120
+ if (!response) break;
121
+ const batch = response.data ?? [];
122
+ collected.push(...batch);
123
+ const total = typeof response.total === "number" ? response.total : collected.length;
124
+ if (batch.length === 0 || collected.length >= total) break;
125
+ }
90
126
  if (listOpts.language) {
91
- return blogs.filter((b) => b.language === listOpts.language);
127
+ return collected.filter((b) => b.language === listOpts.language);
92
128
  }
93
- return blogs;
129
+ return collected;
130
+ }
131
+ };
132
+ }
133
+
134
+ // src/next/metadata.ts
135
+ var trimTrailingSlash = (s) => s.replace(/\/+$/, "");
136
+ var normalizePathPrefix = (s) => {
137
+ let p = s.trim();
138
+ if (!p.startsWith("/")) p = "/" + p;
139
+ while (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
140
+ return p;
141
+ };
142
+ var blogUrlFor = (alternates, language, slug) => {
143
+ const base = trimTrailingSlash(alternates.siteUrl);
144
+ const rawPrefix = typeof alternates.pathPrefix === "function" ? alternates.pathPrefix(language) : alternates.pathPrefix ?? "/blog";
145
+ return `${base}${normalizePathPrefix(rawPrefix)}/${slug}`;
146
+ };
147
+ async function generateBlogMetadata(slug, language, opts) {
148
+ const blog = await getBlog(slug, language, opts);
149
+ if (!blog) return {};
150
+ const images = blog.og_image ? [blog.og_image] : void 0;
151
+ const description = blog.description ?? void 0;
152
+ const meta = {
153
+ title: blog.title,
154
+ ...description ? { description } : {},
155
+ openGraph: {
156
+ type: "article",
157
+ title: blog.title,
158
+ ...description ? { description } : {},
159
+ ...images ? { images } : {},
160
+ ...blog.published_at ? { publishedTime: blog.published_at } : {}
161
+ },
162
+ twitter: {
163
+ card: blog.og_image ? "summary_large_image" : "summary",
164
+ title: blog.title,
165
+ ...description ? { description } : {},
166
+ ...images ? { images } : {}
167
+ }
168
+ };
169
+ if (opts?.alternates) {
170
+ const alternates = opts.alternates;
171
+ const canonical = blogUrlFor(alternates, blog.language, blog.slug);
172
+ let languages;
173
+ try {
174
+ const all = await listBlogs({ status: "PUBLISHED" }, opts);
175
+ const variants = all.filter((b) => b.slug === blog.slug && b.language);
176
+ if (variants.length > 1) {
177
+ languages = {};
178
+ for (const variant of variants) {
179
+ languages[variant.language] = blogUrlFor(
180
+ alternates,
181
+ variant.language,
182
+ variant.slug
183
+ );
184
+ }
185
+ const xDefaultUrl = alternates.xDefaultLanguage ? languages[alternates.xDefaultLanguage] : void 0;
186
+ if (xDefaultUrl) languages["x-default"] = xDefaultUrl;
187
+ }
188
+ } catch (err) {
189
+ console.error(
190
+ "chaingrow-blog: hreflang variant lookup failed, emitting canonical only:",
191
+ err
192
+ );
193
+ }
194
+ meta.alternates = {
195
+ canonical,
196
+ ...languages ? { languages } : {}
197
+ };
198
+ }
199
+ return meta;
200
+ }
201
+ function generateBlogIndexMetadata(opts) {
202
+ const { title, description, canonical, ogImage } = opts;
203
+ const images = ogImage ? [ogImage] : void 0;
204
+ return {
205
+ title,
206
+ ...description ? { description } : {},
207
+ ...canonical ? { alternates: { canonical } } : {},
208
+ openGraph: {
209
+ type: "website",
210
+ title,
211
+ ...description ? { description } : {},
212
+ ...canonical ? { url: canonical } : {},
213
+ ...images ? { images } : {}
214
+ },
215
+ twitter: {
216
+ card: ogImage ? "summary_large_image" : "summary",
217
+ title,
218
+ ...description ? { description } : {},
219
+ ...images ? { images } : {}
220
+ }
221
+ };
222
+ }
223
+
224
+ // src/next/BlogPage.tsx
225
+ var import_navigation = require("next/navigation");
226
+
227
+ // src/media-placement.ts
228
+ function distributeMediaPositions(blockCount, mediaCount) {
229
+ if (mediaCount <= 0) return [];
230
+ if (blockCount <= 0) return Array.from({ length: mediaCount }, () => 0);
231
+ return Array.from(
232
+ { length: mediaCount },
233
+ (_, i) => Math.floor(i * blockCount / mediaCount)
234
+ );
235
+ }
236
+ function interleaveMedia(blocks, media) {
237
+ const mediaList = media ?? [];
238
+ const positions = distributeMediaPositions(blocks.length, mediaList.length);
239
+ const slots = [];
240
+ let mediaCursor = 0;
241
+ for (let b = 0; b <= blocks.length; b++) {
242
+ while (mediaCursor < mediaList.length && positions[mediaCursor] === b) {
243
+ const media2 = mediaList[mediaCursor];
244
+ slots.push({ kind: "media", media: media2, index: mediaCursor });
245
+ mediaCursor++;
246
+ }
247
+ if (b < blocks.length) {
248
+ const block = blocks[b];
249
+ slots.push({ kind: "block", block, index: b });
250
+ }
251
+ }
252
+ return slots;
253
+ }
254
+
255
+ // src/react/defaults.tsx
256
+ var import_jsx_runtime = require("react/jsx-runtime");
257
+ var DefaultHeading = ({
258
+ level,
259
+ children,
260
+ className
261
+ }) => {
262
+ if (level === 1) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { className, children });
263
+ if (level === 3) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className, children });
264
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className, children });
265
+ };
266
+ var DefaultParagraph = ({
267
+ children,
268
+ className
269
+ }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className, children });
270
+ var DefaultBulletList = ({
271
+ items,
272
+ className,
273
+ renderBlock
274
+ }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className, children: items.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: item.content.map((b, j) => renderBlock(b, j)) }, i)) });
275
+ var DefaultOrderedList = ({
276
+ items,
277
+ className,
278
+ renderBlock
279
+ }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ol", { className, children: items.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: item.content.map((b, j) => renderBlock(b, j)) }, i)) });
280
+ var DefaultBlockquote = ({
281
+ children,
282
+ className
283
+ }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("blockquote", { className, children });
284
+ var DefaultCodeBlock = ({
285
+ language,
286
+ code,
287
+ className
288
+ }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", { className, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { className: language ? `language-${language}` : void 0, children: code }) });
289
+ var DefaultImage = ({
290
+ src,
291
+ alt,
292
+ caption,
293
+ width,
294
+ height,
295
+ className
296
+ }) => {
297
+ const img = (
298
+ // eslint-disable-next-line @next/next/no-img-element
299
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src, alt, width, height })
300
+ );
301
+ if (caption) {
302
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("figure", { className, children: [
303
+ img,
304
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("figcaption", { children: caption })
305
+ ] });
306
+ }
307
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className, children: img });
308
+ };
309
+ var DefaultDivider = ({ className }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("hr", { className });
310
+ var DefaultMedia = ({ media }) => {
311
+ if (media.media_type === "VIDEO") {
312
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
313
+ "video",
314
+ {
315
+ src: media.media_url,
316
+ controls: true,
317
+ playsInline: true,
318
+ preload: "metadata"
319
+ }
320
+ );
321
+ }
322
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: media.media_url, alt: media.prompt });
323
+ };
324
+
325
+ // src/react/BlogRenderer.tsx
326
+ var import_jsx_runtime2 = require("react/jsx-runtime");
327
+ var MARK_ORDER = [
328
+ "bold",
329
+ "italic",
330
+ "strike",
331
+ "code",
332
+ "link"
333
+ ];
334
+ function InlineRenderer({ inline }) {
335
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: inline.map((run, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(InlineRun, { run }, i)) });
336
+ }
337
+ function InlineRun({ run }) {
338
+ const marks = run.marks ? [...run.marks].sort(
339
+ (a, b) => MARK_ORDER.indexOf(b.type) - MARK_ORDER.indexOf(a.type)
340
+ ) : [];
341
+ let node = run.text;
342
+ for (const mark of marks) {
343
+ switch (mark.type) {
344
+ case "bold":
345
+ node = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { children: node });
346
+ break;
347
+ case "italic":
348
+ node = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("em", { children: node });
349
+ break;
350
+ case "strike":
351
+ node = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("s", { children: node });
352
+ break;
353
+ case "code":
354
+ node = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("code", { children: node });
355
+ break;
356
+ case "link":
357
+ node = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("a", { href: mark.href, rel: "noopener noreferrer", children: node });
358
+ break;
359
+ }
360
+ }
361
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: node });
362
+ }
363
+ function BlockRenderer({
364
+ block,
365
+ components = {},
366
+ classNames = {},
367
+ renderBlock
368
+ }) {
369
+ const recurse = renderBlock ?? ((b, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
370
+ BlockRenderer,
371
+ {
372
+ block: b,
373
+ components,
374
+ classNames,
375
+ renderBlock: recurse
376
+ },
377
+ i
378
+ ));
379
+ switch (block.type) {
380
+ case "heading": {
381
+ const Heading = components.heading ?? DefaultHeading;
382
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Heading, { level: block.level, className: classNames.heading, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(InlineRenderer, { inline: block.content }) });
383
+ }
384
+ case "paragraph": {
385
+ const Paragraph = components.paragraph ?? DefaultParagraph;
386
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Paragraph, { className: classNames.paragraph, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(InlineRenderer, { inline: block.content }) });
387
+ }
388
+ case "bulletList": {
389
+ const BulletList = components.bulletList ?? DefaultBulletList;
390
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
391
+ BulletList,
392
+ {
393
+ items: block.items,
394
+ className: classNames.bulletList,
395
+ renderBlock: recurse
396
+ }
397
+ );
398
+ }
399
+ case "orderedList": {
400
+ const OrderedList = components.orderedList ?? DefaultOrderedList;
401
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
402
+ OrderedList,
403
+ {
404
+ items: block.items,
405
+ className: classNames.orderedList,
406
+ renderBlock: recurse
407
+ }
408
+ );
409
+ }
410
+ case "blockquote": {
411
+ const Blockquote = components.blockquote ?? DefaultBlockquote;
412
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Blockquote, { className: classNames.blockquote, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(InlineRenderer, { inline: block.content }) });
413
+ }
414
+ case "codeBlock": {
415
+ const CodeBlock = components.codeBlock ?? DefaultCodeBlock;
416
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
417
+ CodeBlock,
418
+ {
419
+ language: block.language,
420
+ code: block.code,
421
+ className: classNames.codeBlock
422
+ }
423
+ );
424
+ }
425
+ case "image": {
426
+ const ImageComp = components.image ?? DefaultImage;
427
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
428
+ ImageComp,
429
+ {
430
+ src: block.src,
431
+ alt: block.alt,
432
+ caption: block.caption,
433
+ width: block.width,
434
+ height: block.height,
435
+ mediaId: block.mediaId,
436
+ className: classNames.image
437
+ }
438
+ );
439
+ }
440
+ case "divider": {
441
+ const Divider = components.divider ?? DefaultDivider;
442
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Divider, { className: classNames.divider });
443
+ }
444
+ }
445
+ }
446
+ function BlogRenderer({
447
+ blog,
448
+ components = {},
449
+ classNames = {}
450
+ }) {
451
+ const Media = components.media ?? DefaultMedia;
452
+ const Wrapper = components.wrapper;
453
+ const slots = interleaveMedia(blog.blocks, blog.generatedMedia);
454
+ const content = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: slots.map(
455
+ (slot) => slot.kind === "block" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
456
+ BlockRenderer,
457
+ {
458
+ block: slot.block,
459
+ components,
460
+ classNames
461
+ },
462
+ `b:${slot.index}`
463
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Media, { media: slot.media }, `m:${slot.media.id}`)
464
+ ) });
465
+ if (Wrapper) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Wrapper, { blog, children: content });
466
+ return content;
467
+ }
468
+
469
+ // src/react/BlogDetail.tsx
470
+ var import_jsx_runtime3 = require("react/jsx-runtime");
471
+ var defaultFormatDate = (iso) => new Date(iso).toLocaleDateString();
472
+ function DefaultHeader({
473
+ blog,
474
+ formatDate
475
+ }) {
476
+ const hasInlineMedia = (blog.generatedMedia?.length ?? 0) > 0;
477
+ const heroImage = !hasInlineMedia && blog.og_image ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("img", { src: blog.og_image, alt: blog.title }) : null;
478
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
479
+ heroImage,
480
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { children: blog.title }),
481
+ blog.published_at && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("time", { dateTime: blog.published_at, children: formatDate(blog.published_at) }),
482
+ blog.description && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: blog.description })
483
+ ] });
484
+ }
485
+ function BlogDetail({
486
+ blog,
487
+ components,
488
+ formatDate = defaultFormatDate
489
+ }) {
490
+ const Header = components?.Header ?? DefaultHeader;
491
+ const Footer = components?.Footer;
492
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("article", { children: [
493
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Header, { blog, formatDate }),
494
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(BlogRenderer, { blog, components: components?.blocks }),
495
+ Footer ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Footer, { blog }) : null
496
+ ] });
497
+ }
498
+
499
+ // src/json-ld.ts
500
+ var Schema = Object.freeze({ context: "https://schema.org" });
501
+ function buildBlogPostingSchema(blog, opts = {}) {
502
+ const schema = {
503
+ "@type": "BlogPosting",
504
+ headline: blog.title,
505
+ datePublished: blog.published_at
506
+ };
507
+ if (blog.description) schema.description = blog.description;
508
+ if (blog.og_image) schema.image = blog.og_image;
509
+ if (opts.url) {
510
+ schema.url = opts.url;
511
+ schema.mainEntityOfPage = opts.url;
512
+ }
513
+ if (opts.author) {
514
+ schema.author = {
515
+ "@type": opts.author.type ?? "Organization",
516
+ name: opts.author.name,
517
+ ...opts.author.url ? { url: opts.author.url } : {}
518
+ };
519
+ }
520
+ if (opts.publisher) {
521
+ schema.publisher = {
522
+ "@type": "Organization",
523
+ name: opts.publisher.name,
524
+ ...opts.publisher.logo ? { logo: { "@type": "ImageObject", url: opts.publisher.logo } } : {}
525
+ };
526
+ }
527
+ return schema;
528
+ }
529
+ function buildFAQPageSchema(faq) {
530
+ if (!faq || faq.length === 0) return null;
531
+ return {
532
+ "@type": "FAQPage",
533
+ mainEntity: faq.map(({ question, answer }) => ({
534
+ "@type": "Question",
535
+ name: question,
536
+ acceptedAnswer: { "@type": "Answer", text: answer }
537
+ }))
538
+ };
539
+ }
540
+ function buildBlogJsonLd(blog, opts = {}) {
541
+ const items = [buildBlogPostingSchema(blog, opts)];
542
+ const faq = buildFAQPageSchema(blog.faq);
543
+ if (faq) items.push(faq);
544
+ items.push(...extractLlmJsonLd(blog.structured_data));
545
+ return items;
546
+ }
547
+ var SCHEMA_ORG_CONTEXT = Schema.context;
548
+ function extractLlmJsonLd(sd) {
549
+ if (!sd || typeof sd !== "object") return [];
550
+ const ld = sd.jsonLd;
551
+ if (!Array.isArray(ld)) return [];
552
+ return ld.filter(
553
+ (entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry)
554
+ );
555
+ }
556
+
557
+ // src/react/BlogJsonLd.tsx
558
+ var import_jsx_runtime4 = require("react/jsx-runtime");
559
+ function JsonLdGraph({
560
+ items
561
+ }) {
562
+ if (!items || items.length === 0) return null;
563
+ const payload = JSON.stringify({
564
+ "@context": SCHEMA_ORG_CONTEXT,
565
+ "@graph": items
566
+ });
567
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
568
+ "script",
569
+ {
570
+ type: "application/ld+json",
571
+ dangerouslySetInnerHTML: { __html: payload }
572
+ }
573
+ );
574
+ }
575
+ function BlogJsonLd({
576
+ blog,
577
+ extraSchemas,
578
+ ...opts
579
+ }) {
580
+ const items = [...extraSchemas ?? [], ...buildBlogJsonLd(blog, opts)];
581
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(JsonLdGraph, { items });
582
+ }
583
+
584
+ // src/next/BlogPage.tsx
585
+ var import_jsx_runtime5 = require("react/jsx-runtime");
586
+ async function BlogPage({
587
+ params
588
+ }) {
589
+ const { slug, locale } = await params;
590
+ let blog = null;
591
+ try {
592
+ blog = await getBlog(slug, locale);
593
+ } catch (err) {
594
+ console.error("chaingrow-blog: <BlogPage> getBlog failed:", err);
595
+ }
596
+ if (!blog) (0, import_navigation.notFound)();
597
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cg-blog-article", children: [
598
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("style", { dangerouslySetInnerHTML: { __html: ARTICLE_CSS } }),
599
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(BlogJsonLd, { blog }),
600
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(BlogDetail, { blog })
601
+ ] });
602
+ }
603
+ var ARTICLE_CSS = `
604
+ .cg-blog-article {
605
+ max-width: 720px;
606
+ margin: 0 auto;
607
+ padding: 4rem 1.5rem 6rem;
608
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
609
+ font-size: 1.0625rem;
610
+ line-height: 1.7;
611
+ color: inherit;
612
+ -webkit-font-smoothing: antialiased;
613
+ -moz-osx-font-smoothing: grayscale;
614
+ }
615
+ .cg-blog-article h1 {
616
+ font-size: clamp(2rem, 4vw, 2.625rem);
617
+ font-weight: 700;
618
+ line-height: 1.15;
619
+ letter-spacing: -0.022em;
620
+ color: inherit;
621
+ margin: 0 0 1rem;
622
+ }
623
+ .cg-blog-article time {
624
+ display: block;
625
+ font-size: 0.8125rem;
626
+ color: currentColor;
627
+ opacity: 0.55;
628
+ margin: 0 0 2rem;
629
+ font-weight: 400;
630
+ }
631
+ .cg-blog-article h2 {
632
+ font-size: 1.625rem;
633
+ font-weight: 700;
634
+ line-height: 1.25;
635
+ letter-spacing: -0.015em;
636
+ color: inherit;
637
+ margin: 3rem 0 1rem;
638
+ }
639
+ .cg-blog-article h3 {
640
+ font-size: 1.25rem;
641
+ font-weight: 700;
642
+ line-height: 1.3;
643
+ color: inherit;
644
+ margin: 2.25rem 0 0.75rem;
645
+ }
646
+ .cg-blog-article h4 {
647
+ font-size: 1.0625rem;
648
+ font-weight: 700;
649
+ color: inherit;
650
+ margin: 1.75rem 0 0.5rem;
651
+ }
652
+ .cg-blog-article p {
653
+ margin: 0 0 1.25rem;
654
+ color: inherit;
655
+ }
656
+ .cg-blog-article a {
657
+ color: inherit;
658
+ text-decoration: underline;
659
+ text-underline-offset: 3px;
660
+ text-decoration-thickness: 1px;
661
+ transition: opacity 150ms ease;
662
+ }
663
+ .cg-blog-article a:hover { opacity: 0.7; }
664
+ .cg-blog-article strong { color: inherit; font-weight: 700; }
665
+ .cg-blog-article em { font-style: italic; }
666
+ .cg-blog-article ul,
667
+ .cg-blog-article ol {
668
+ margin: 0 0 1.5rem;
669
+ padding-left: 1.5rem;
670
+ color: inherit;
671
+ }
672
+ .cg-blog-article li {
673
+ margin-bottom: 0.5rem;
674
+ line-height: 1.65;
675
+ }
676
+ .cg-blog-article li > p { margin: 0 0 0.5rem; }
677
+ .cg-blog-article li:last-child > p:last-child { margin-bottom: 0; }
678
+ .cg-blog-article blockquote {
679
+ margin: 2rem 0;
680
+ padding: 0.5rem 0 0.5rem 1.5rem;
681
+ border-left: 3px solid currentColor;
682
+ font-style: italic;
683
+ color: inherit;
684
+ font-size: 1.125rem;
685
+ line-height: 1.6;
686
+ }
687
+ .cg-blog-article blockquote p:last-child { margin-bottom: 0; }
688
+ .cg-blog-article img {
689
+ display: block;
690
+ width: 100%;
691
+ height: auto;
692
+ margin: 2.25rem 0;
693
+ border-radius: 6px;
694
+ background: rgba(128, 128, 128, 0.08);
695
+ }
696
+ .cg-blog-article video {
697
+ display: block;
698
+ width: 100%;
699
+ height: auto;
700
+ margin: 2.25rem 0;
701
+ border-radius: 6px;
702
+ background: rgba(128, 128, 128, 0.15);
703
+ }
704
+ .cg-blog-article hr {
705
+ border: 0;
706
+ height: 1px;
707
+ background: currentColor;
708
+ opacity: 0.15;
709
+ margin: 3rem 0;
710
+ }
711
+ .cg-blog-article code {
712
+ background: rgba(128, 128, 128, 0.15);
713
+ color: inherit;
714
+ padding: 0.15em 0.4em;
715
+ border-radius: 3px;
716
+ font-size: 0.9em;
717
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
718
+ }
719
+ .cg-blog-article pre {
720
+ background: rgba(128, 128, 128, 0.12);
721
+ color: inherit;
722
+ padding: 1.25rem 1.5rem;
723
+ border-radius: 6px;
724
+ overflow-x: auto;
725
+ font-size: 0.9375rem;
726
+ line-height: 1.6;
727
+ margin: 1.75rem 0;
728
+ }
729
+ .cg-blog-article pre code {
730
+ background: transparent;
731
+ color: inherit;
732
+ padding: 0;
733
+ border-radius: 0;
734
+ font-size: inherit;
735
+ }
736
+ @media (max-width: 640px) {
737
+ .cg-blog-article {
738
+ padding: 2.5rem 1.25rem 4rem;
739
+ font-size: 1rem;
740
+ }
741
+ .cg-blog-article h2 { margin-top: 2.25rem; }
742
+ .cg-blog-article h3 { margin-top: 1.75rem; }
743
+ }
744
+ `;
745
+ function generateBlogPageMetadata(opts) {
746
+ return async ({ params }) => {
747
+ const { slug, locale } = await params;
748
+ try {
749
+ return await generateBlogMetadata(slug, locale, opts);
750
+ } catch (err) {
751
+ console.error(
752
+ "chaingrow-blog: generateBlogPageMetadata failed:",
753
+ err
754
+ );
755
+ return {};
94
756
  }
95
757
  };
96
758
  }
97
759
 
98
760
  // src/next/index.ts
99
761
  var DEFAULT_API_URL = "https://api.chaingrow.de";
762
+ var DEFAULT_REVALIDATE_SECONDS = 60 * 60 * 24 * 7;
100
763
  var resolveEnv = (opts = {}) => {
101
764
  const apiUrl = opts.apiUrl ?? process.env.CHAINGROW_API_URL ?? DEFAULT_API_URL;
102
765
  const apiKey = opts.apiKey ?? process.env.CHAINGROW_API_KEY;
@@ -108,11 +771,12 @@ var resolveEnv = (opts = {}) => {
108
771
  return { apiUrl, apiKey };
109
772
  };
110
773
  var buildFetch = (tag, revalidate) => {
774
+ const effectiveRevalidate = revalidate === void 0 ? DEFAULT_REVALIDATE_SECONDS : revalidate;
111
775
  return ((input, init) => fetch(input, {
112
776
  ...init,
113
777
  next: {
114
778
  tags: ["chaingrow:blog", tag],
115
- ...revalidate === false ? { revalidate: false } : revalidate !== void 0 ? { revalidate } : {}
779
+ revalidate: effectiveRevalidate
116
780
  }
117
781
  }));
118
782
  };
@@ -134,9 +798,37 @@ async function listBlogs(listOpts = {}, opts = {}) {
134
798
  });
135
799
  return client.listBlogs(listOpts);
136
800
  }
801
+ var trimTrailingSlash2 = (s) => s.replace(/\/+$/, "");
802
+ var normalizePathPrefix2 = (s) => {
803
+ let p = s.trim();
804
+ if (!p.startsWith("/")) p = "/" + p;
805
+ while (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
806
+ return p;
807
+ };
808
+ async function getBlogSitemapEntries(opts) {
809
+ const baseUrl = trimTrailingSlash2(opts.siteUrl);
810
+ const pathPrefix = normalizePathPrefix2(opts.pathPrefix ?? "/blog");
811
+ const blogs = await listBlogs(
812
+ { status: "PUBLISHED", language: opts.language },
813
+ {
814
+ apiKey: opts.apiKey,
815
+ apiUrl: opts.apiUrl,
816
+ revalidate: opts.revalidate
817
+ }
818
+ );
819
+ return blogs.map((blog) => ({
820
+ url: `${baseUrl}${pathPrefix}/${blog.slug}`,
821
+ lastModified: new Date(blog.published_at)
822
+ }));
823
+ }
137
824
  // Annotate the CommonJS export names for ESM import in node:
138
825
  0 && (module.exports = {
826
+ BlogPage,
827
+ generateBlogIndexMetadata,
828
+ generateBlogMetadata,
829
+ generateBlogPageMetadata,
139
830
  getBlog,
831
+ getBlogSitemapEntries,
140
832
  listBlogs
141
833
  });
142
834
  //# sourceMappingURL=index.cjs.map