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