@roottale/cms-renderer-next 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,9 +1,45 @@
1
1
  # @roottale/cms-renderer-next
2
2
 
3
+ ## 0.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 39fbe3d: ACF 커스텀 필드 (ADR-0041) M2·M3-D
8
+ - cms-core: Block Bindings 헬퍼 — `applyBlockBindings` + `createFieldBindingResolver`
9
+ (`roottale/field` + `acf/field` alias) + `chainResolvers`.
10
+ - cms-renderer-next/-astro: `renderBoundBlocks(blocks, { fields })` — 바인딩 적용 후 렌더.
11
+ - cms-client: `CmsPostContent.fields` — 서버가 포맷한 커스텀 필드 값
12
+ (image/file/gallery 는 media variant URL 객체). 구 서버는 null.
13
+
14
+ - f313e76: 필드 바인딩 라이브 렌더 (ADR-0041 D3·D4, 바인딩 PR-2)
15
+ - cms-core: `boundFieldValue` / `bindingTextValue` / `bindingSrcValue` — Tiptap
16
+ 라이브 경로의 바인딩 해석 헬퍼 (값은 항상 text, raw HTML 진입 금지).
17
+ - cms-renderer-next: `RenderTiptap` export + `fields` prop — 블록 바인딩을
18
+ 공개 API `post.fields` 값으로 치환(값 없으면 본문 fallback). `RootTaleBlogPost`
19
+ 가 자동 전달 — 고객 사이트 코드 변경 없이 동작.
20
+ - cms-renderer-astro: `renderTiptapDoc(doc, { fields })` 동등 표면 + blog 경로 자동 전달.
21
+
22
+ ### Patch Changes
23
+
24
+ - Updated dependencies [39fbe3d]
25
+ - Updated dependencies [f313e76]
26
+ - @roottale/cms-core@0.18.0
27
+ - @roottale/cms-client@0.18.0
28
+
29
+ ## 0.17.0
30
+
31
+ ### Patch Changes
32
+
33
+ - Updated dependencies [a3c913f]
34
+ - @roottale/cms-client@0.17.0
35
+
3
36
  ## 0.16.0
4
37
 
5
38
  ### Patch Changes
6
39
 
40
+ - 카테고리 필터 기본 URL을 SEO 친화 경로(`/blog/categories/<slug>`)로 변경하고
41
+ `RootTaleBlogCategories`를 추가. sitemap/revalidate route도 카테고리
42
+ 인덱스와 개별 카테고리 URL을 포함.
7
43
  - Updated dependencies [5a86552]
8
44
  - @roottale/cms-client@0.16.0
9
45
 
@@ -14,9 +50,7 @@
14
50
  - d7fb4be: `RootTaleBlogList` 카테고리 필터 (SSR-only, client JS 불요).
15
51
 
16
52
  외부 사이트가 자체 카테고리 필터 UI 때문에 `RootTaleBlogList` 채택을 못 하던
17
- gap 해소(ADR-0040 P1). `showCategoryFilter` 로 글 목록 위에 카테고리 칩을 렌더 —
18
- 칩은 `<a href="?category=<slug>">` 서버 링크이고 `activeCategory`(consumer 가
19
- searchParams 에서 주입)로 서버 필터. hydration 없이 SEO 친화 + 공유 가능 URL.
53
+ gap 해소(ADR-0040 P1). `showCategoryFilter` 로 글 목록 위에 카테고리 칩을 렌더.
20
54
  - 새 props: `showCategoryFilter`, `activeCategory`, `categoryHref`,
21
55
  `allCategoriesLabel`. 기본 off → 기존 동작 불변.
22
56
  - 순수 헬퍼 `collectCategories` / `filterPostsByCategory` / `defaultCategoryHref`
package/README.md CHANGED
@@ -75,6 +75,7 @@ export default function BlogPage() {
75
75
  <RootTaleBlogList
76
76
  apiKey={process.env.ROOTTALE_API_KEY!}
77
77
  limit={20}
78
+ showCategoryFilter
78
79
  postHref={(post) => `/blog/${post.slug}`}
79
80
  />
80
81
  </main>
@@ -82,6 +83,37 @@ export default function BlogPage() {
82
83
  }
83
84
  ```
84
85
 
86
+ ## Blog categories — `app/blog/categories/page.tsx`
87
+
88
+ ```tsx
89
+ import { RootTaleBlogCategories } from "@roottale/cms-renderer-next/server";
90
+
91
+ export default function CategoriesPage() {
92
+ return <RootTaleBlogCategories apiKey={process.env.ROOTTALE_API_KEY!} />;
93
+ }
94
+ ```
95
+
96
+ ## Blog category — `app/blog/categories/[category]/page.tsx`
97
+
98
+ ```tsx
99
+ import { RootTaleBlogList } from "@roottale/cms-renderer-next/server";
100
+
101
+ export default async function CategoryPage({
102
+ params,
103
+ }: {
104
+ params: Promise<{ category: string }>;
105
+ }) {
106
+ const { category } = await params;
107
+ return (
108
+ <RootTaleBlogList
109
+ apiKey={process.env.ROOTTALE_API_KEY!}
110
+ showCategoryFilter
111
+ activeCategory={decodeURIComponent(category)}
112
+ />
113
+ );
114
+ }
115
+ ```
116
+
85
117
  ## Blog post — `app/blog/[slug]/page.tsx`
86
118
 
87
119
  ```tsx
@@ -0,0 +1,42 @@
1
+ // src/blog-filter.ts
2
+ function categoryTerms(post) {
3
+ return (post.terms ?? []).filter((t) => t.taxonomy === "category");
4
+ }
5
+ function collectCategories(posts) {
6
+ return collectCategoriesWithCounts(posts).map(({ name, slug }) => ({
7
+ name,
8
+ slug
9
+ }));
10
+ }
11
+ function collectCategoriesWithCounts(posts) {
12
+ const bySlug = /* @__PURE__ */ new Map();
13
+ const out = [];
14
+ for (const post of posts) {
15
+ for (const t of categoryTerms(post)) {
16
+ const current = bySlug.get(t.slug);
17
+ if (current) {
18
+ current.count += 1;
19
+ } else {
20
+ const category = { name: t.name, slug: t.slug, count: 1 };
21
+ bySlug.set(t.slug, category);
22
+ out.push(category);
23
+ }
24
+ }
25
+ }
26
+ return out;
27
+ }
28
+ function filterPostsByCategory(posts, activeSlug) {
29
+ if (!activeSlug) return posts;
30
+ return posts.filter((p) => categoryTerms(p).some((t) => t.slug === activeSlug));
31
+ }
32
+ function defaultCategoryHref(slug) {
33
+ return slug ? `/blog/categories/${encodeURIComponent(slug)}` : "/blog";
34
+ }
35
+
36
+ export {
37
+ collectCategories,
38
+ collectCategoriesWithCounts,
39
+ filterPostsByCategory,
40
+ defaultCategoryHref
41
+ };
42
+ //# sourceMappingURL=chunk-QWBFHYB6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/blog-filter.ts"],"sourcesContent":["/**\n * `RootTaleBlogList` 카테고리 필터 — 순수 헬퍼(서버 필터, client JS 불요).\n *\n * SSR-only 정책 유지: 필터는 `?category=<slug>` URL 파라미터로 동작하고(칩 =\n * 서버 렌더 `<a>` 링크), consumer 가 searchParams 의 category 를 `activeCategory`\n * 로 주입한다. hydration 없이 SEO 친화 + 공유 가능한 URL.\n */\n\nimport type { CmsPostContent, CmsTermRef } from \"@roottale/cms-client/server\";\n\nexport interface BlogCategory {\n name: string;\n slug: string;\n}\n\nexport interface BlogCategoryWithCount extends BlogCategory {\n count: number;\n}\n\nfunction categoryTerms(post: CmsPostContent): CmsTermRef[] {\n return (post.terms ?? []).filter((t) => t.taxonomy === \"category\");\n}\n\n/**\n * 글 목록에 등장하는 카테고리를 slug 기준 unique 로 수집. 서버가 보내는 정렬\n * (category → name asc)을 보존하기 위해 처음 등장 순서를 유지한다.\n */\nexport function collectCategories(posts: CmsPostContent[]): BlogCategory[] {\n return collectCategoriesWithCounts(posts).map(({ name, slug }) => ({\n name,\n slug,\n }));\n}\n\nexport function collectCategoriesWithCounts(\n posts: CmsPostContent[],\n): BlogCategoryWithCount[] {\n const bySlug = new Map<string, BlogCategoryWithCount>();\n const out: BlogCategoryWithCount[] = [];\n for (const post of posts) {\n for (const t of categoryTerms(post)) {\n const current = bySlug.get(t.slug);\n if (current) {\n current.count += 1;\n } else {\n const category = { name: t.name, slug: t.slug, count: 1 };\n bySlug.set(t.slug, category);\n out.push(category);\n }\n }\n }\n return out;\n}\n\n/** activeSlug 가 비면 전체 반환. 아니면 그 카테고리를 가진 글만. */\nexport function filterPostsByCategory(\n posts: CmsPostContent[],\n activeSlug: string | null | undefined,\n): CmsPostContent[] {\n if (!activeSlug) return posts;\n return posts.filter((p) => categoryTerms(p).some((t) => t.slug === activeSlug));\n}\n\n/** 기본 카테고리 링크: 전체 → `/blog`, 특정 → `/blog/categories/<slug>`. */\nexport function defaultCategoryHref(slug: string | null): string {\n return slug ? `/blog/categories/${encodeURIComponent(slug)}` : \"/blog\";\n}\n"],"mappings":";AAmBA,SAAS,cAAc,MAAoC;AACzD,UAAQ,KAAK,SAAS,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,aAAa,UAAU;AACnE;AAMO,SAAS,kBAAkB,OAAyC;AACzE,SAAO,4BAA4B,KAAK,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO;AAAA,IACjE;AAAA,IACA;AAAA,EACF,EAAE;AACJ;AAEO,SAAS,4BACd,OACyB;AACzB,QAAM,SAAS,oBAAI,IAAmC;AACtD,QAAM,MAA+B,CAAC;AACtC,aAAW,QAAQ,OAAO;AACxB,eAAW,KAAK,cAAc,IAAI,GAAG;AACnC,YAAM,UAAU,OAAO,IAAI,EAAE,IAAI;AACjC,UAAI,SAAS;AACX,gBAAQ,SAAS;AAAA,MACnB,OAAO;AACL,cAAM,WAAW,EAAE,MAAM,EAAE,MAAM,MAAM,EAAE,MAAM,OAAO,EAAE;AACxD,eAAO,IAAI,EAAE,MAAM,QAAQ;AAC3B,YAAI,KAAK,QAAQ;AAAA,MACnB;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,sBACd,OACA,YACkB;AAClB,MAAI,CAAC,WAAY,QAAO;AACxB,SAAO,MAAM,OAAO,CAAC,MAAM,cAAc,CAAC,EAAE,KAAK,CAAC,MAAM,EAAE,SAAS,UAAU,CAAC;AAChF;AAGO,SAAS,oBAAoB,MAA6B;AAC/D,SAAO,OAAO,oBAAoB,mBAAmB,IAAI,CAAC,KAAK;AACjE;","names":[]}
@@ -23,7 +23,7 @@
23
23
  list-style: none;
24
24
  }
25
25
 
26
- /* Category filter — chips are <a> links (?category=slug), SSR-only, no JS. */
26
+ /* Category filter — chips are <a> links (/blog/categories/slug), SSR-only, no JS. */
27
27
  [data-roottale-cms] :where(.rt-cms-filter) {
28
28
  display: flex;
29
29
  flex-wrap: wrap;
@@ -62,6 +62,62 @@
62
62
  color: var(--rt-color-primary-foreground, #ffffff);
63
63
  }
64
64
 
65
+ [data-roottale-cms] :where(.rt-cms-category-list) {
66
+ display: grid;
67
+ gap: var(--rt-spacing-3, 0.75rem);
68
+ padding: 0;
69
+ margin: 0;
70
+ list-style: none;
71
+ }
72
+
73
+ [data-roottale-cms] :where(.rt-cms-category-list-item) {
74
+ min-inline-size: 0;
75
+ }
76
+
77
+ [data-roottale-cms] :where(.rt-cms-category-link) {
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: space-between;
81
+ gap: var(--rt-spacing-3, 0.75rem);
82
+ padding: 0.875rem 1rem;
83
+ border: 1px solid color-mix(in srgb, var(--rt-color-border, #e5e7eb) 82%, transparent);
84
+ border-radius: var(--rt-radius-md, 0.5rem);
85
+ background: color-mix(in srgb, var(--rt-color-muted, #f8fafc) 58%, white);
86
+ color: var(--rt-color-foreground, #111827);
87
+ font-family: var(--rt-font-body, system-ui, sans-serif);
88
+ text-decoration: none;
89
+ transition:
90
+ border-color 160ms ease,
91
+ background-color 160ms ease;
92
+ }
93
+
94
+ [data-roottale-cms] :where(.rt-cms-category-link:hover),
95
+ [data-roottale-cms] :where(.rt-cms-category-link:focus-visible) {
96
+ border-color: color-mix(in srgb, var(--rt-color-primary, #4f46e5) 40%, var(--rt-color-border, #e5e7eb));
97
+ background: #fff;
98
+ }
99
+
100
+ [data-roottale-cms] :where(.rt-cms-category-name) {
101
+ min-inline-size: 0;
102
+ overflow-wrap: anywhere;
103
+ font-size: 0.9375rem;
104
+ font-weight: 700;
105
+ line-height: 1.35;
106
+ }
107
+
108
+ [data-roottale-cms] :where(.rt-cms-category-count) {
109
+ flex: 0 0 auto;
110
+ min-inline-size: 1.75rem;
111
+ padding: 0.1875rem 0.5rem;
112
+ border-radius: 999px;
113
+ background: var(--rt-color-primary, #4f46e5);
114
+ color: var(--rt-color-primary-foreground, #ffffff);
115
+ font-size: 0.75rem;
116
+ font-weight: 800;
117
+ line-height: 1.2;
118
+ text-align: center;
119
+ }
120
+
65
121
  [data-roottale-cms] :where(.rt-cms-list-item) {
66
122
  min-inline-size: 0;
67
123
  }
@@ -251,6 +307,10 @@
251
307
 
252
308
  [data-roottale-cms] :where(.rt-cms-body) {
253
309
  min-inline-size: 0;
310
+ font-family: var(--rt-font-body, system-ui, sans-serif);
311
+ font-size: var(--rt-font-size-base, 1rem);
312
+ line-height: 1.7;
313
+ color: var(--rt-color-foreground, #111827);
254
314
  }
255
315
 
256
316
  [data-roottale-cms] :where(.rt-cms-post-header) {
@@ -287,57 +347,133 @@
287
347
  );
288
348
  }
289
349
 
290
- [data-roottale-cms] :where(.rt-cms-article h2),
291
- [data-roottale-cms] :where(.rt-cms-article h3),
292
- [data-roottale-cms] :where(.rt-cms-article h4) {
350
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] > * + *) {
351
+ margin-block-start: 0.65em;
352
+ }
353
+
354
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] p) {
355
+ max-inline-size: 72ch;
356
+ margin: 0;
357
+ line-height: 1.7;
358
+ }
359
+
360
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] h2),
361
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] h3),
362
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] h4) {
293
363
  font-family: var(--rt-font-display, system-ui, sans-serif);
294
364
  color: var(--rt-color-foreground, #111827);
295
365
  letter-spacing: 0;
296
- margin-top: var(--rt-spacing-7, 2rem);
297
- margin-bottom: var(--rt-spacing-3, 0.75rem);
366
+ line-height: 1.4;
298
367
  }
299
368
 
300
- [data-roottale-cms] :where(.rt-cms-article p) {
301
- margin: 0 0 var(--rt-spacing-4, 1rem);
302
- line-height: 1.7;
369
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] h2) {
370
+ margin: 1.3em 0 0.45em;
371
+ font-size: var(--rt-font-size-3xl, 1.875rem);
372
+ font-weight: var(--rt-font-weight-bold, 700);
373
+ }
374
+
375
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] h3) {
376
+ margin: 1.05em 0 0.35em;
377
+ font-family: var(--rt-font-body, system-ui, sans-serif);
378
+ font-size: var(--rt-font-size-xl, 1.25rem);
379
+ font-weight: var(--rt-font-weight-semibold, 600);
380
+ }
381
+
382
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] h4) {
383
+ margin: 0.9em 0 0.25em;
384
+ font-family: var(--rt-font-body, system-ui, sans-serif);
385
+ font-size: var(--rt-font-size-lg, 1.125rem);
386
+ font-weight: var(--rt-font-weight-semibold, 600);
387
+ }
388
+
389
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] h2:first-child),
390
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] h3:first-child),
391
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] h4:first-child) {
392
+ margin-block-start: 0;
393
+ }
394
+
395
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] h2 + p),
396
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] h3 + p),
397
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] h4 + p) {
398
+ margin-block-start: 0.35em;
399
+ }
400
+
401
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] ul),
402
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] ol) {
403
+ padding-inline-start: 1.5em;
404
+ margin: 0;
303
405
  }
304
406
 
305
- [data-roottale-cms] :where(.rt-cms-article a) {
407
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] li > p) {
408
+ margin: 0;
409
+ }
410
+
411
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] blockquote) {
412
+ border-inline-start: 3px solid var(--rt-color-border, #e5e7eb);
413
+ padding-inline-start: var(--rt-spacing-3, 0.75rem);
414
+ color: var(--rt-color-muted-foreground, #64748b);
415
+ font-style: italic;
416
+ }
417
+
418
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] a) {
306
419
  color: var(--rt-color-primary, #0070f3);
307
420
  text-decoration: underline;
308
- text-underline-offset: 0.16em;
421
+ text-underline-offset: 0.15em;
309
422
  }
310
423
 
311
- [data-roottale-cms] :where(.rt-cms-article mark) {
312
- border-radius: 0.2em;
313
- padding-inline: 0.08em;
424
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] mark) {
425
+ border-radius: var(--rt-radius-sm, 0.25rem);
426
+ padding-inline: 0.1em;
314
427
  background: var(--rt-color-highlight, #fef3c7);
428
+ color: inherit;
315
429
  box-decoration-break: clone;
316
430
  -webkit-box-decoration-break: clone;
317
431
  }
318
432
 
319
- [data-roottale-cms] :where(.rt-cms-article u) {
433
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] u) {
320
434
  text-underline-offset: 0.16em;
321
435
  text-decoration-thickness: 0.08em;
322
436
  }
323
437
 
324
- [data-roottale-cms] :where(.rt-cms-article s) {
438
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] s) {
325
439
  text-decoration-thickness: 0.08em;
326
440
  }
327
441
 
328
- [data-roottale-cms] :where(.rt-cms-article code) {
329
- border-radius: 0.25rem;
330
- background: color-mix(in srgb, var(--rt-color-muted, #f8fafc) 72%, var(--rt-color-foreground, #111827) 6%);
331
- padding: 0.1em 0.3em;
332
- font-size: 0.92em;
442
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] code) {
443
+ border-radius: var(--rt-radius-sm, 0.25rem);
444
+ background: var(--rt-color-muted, #f8fafc);
445
+ padding-inline: 0.25em;
446
+ font-family: var(--rt-font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);
447
+ font-size: 0.9em;
333
448
  }
334
449
 
335
- [data-roottale-cms] :where(.rt-cms-article pre code) {
450
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] pre) {
451
+ overflow-x: auto;
452
+ border-radius: var(--rt-radius-md, 0.5rem);
453
+ background: var(--rt-color-muted, #f8fafc);
454
+ padding: var(--rt-spacing-3, 0.75rem);
455
+ font-family: var(--rt-font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);
456
+ font-size: 0.9em;
457
+ }
458
+
459
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] pre code) {
336
460
  display: block;
337
461
  padding: 0;
338
462
  background: transparent;
339
463
  }
340
464
 
465
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] img) {
466
+ max-inline-size: 100%;
467
+ block-size: auto;
468
+ border-radius: var(--rt-radius-md, 0.5rem);
469
+ }
470
+
471
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] hr) {
472
+ border: 0;
473
+ border-block-start: 1px solid var(--rt-color-border, #e5e7eb);
474
+ margin-block: var(--rt-spacing-4, 1rem);
475
+ }
476
+
341
477
  /* RootTalePostCard */
342
478
  [data-roottale-cms] :where(.rt-cms-card) {
343
479
  display: block;
package/dist/routes.js CHANGED
@@ -1,3 +1,7 @@
1
+ import {
2
+ collectCategories
3
+ } from "./chunk-QWBFHYB6.js";
4
+
1
5
  // src/routes.ts
2
6
  import {
3
7
  fetchPosts,
@@ -58,7 +62,20 @@ function createSitemap(config, staticRoutes = []) {
58
62
  changeFrequency: "weekly",
59
63
  priority: 0.6
60
64
  }));
61
- return [...staticRoutes, ...postEntries];
65
+ const categories = collectCategories(posts);
66
+ const categoryEntries = categories.length ? [
67
+ {
68
+ url: `${siteUrl}${blogBase}/categories`,
69
+ changeFrequency: "weekly",
70
+ priority: 0.5
71
+ },
72
+ ...categories.map((category) => ({
73
+ url: `${siteUrl}${blogBase}/categories/${category.slug}`,
74
+ changeFrequency: "weekly",
75
+ priority: 0.5
76
+ }))
77
+ ] : [];
78
+ return [...staticRoutes, ...postEntries, ...categoryEntries];
62
79
  };
63
80
  }
64
81
  function pathsFromBody(rawBody) {
@@ -76,7 +93,8 @@ function createRevalidateRoute(config) {
76
93
  const alsoRevalidate = config.alsoRevalidate ?? [
77
94
  "/feed.xml",
78
95
  "/sitemap.xml",
79
- blogBase
96
+ blogBase,
97
+ `${blogBase}/categories`
80
98
  ];
81
99
  return async function POST(req) {
82
100
  const rawBody = await req.text();
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/routes.ts"],"sourcesContent":["/**\n * `@roottale/cms-renderer-next/routes` — Next.js App Router 통합 라우트 팩토리.\n *\n * 외부 고객사 사이트가 매번 직접 작성하던 공통 라우트(RSS feed / sitemap /\n * revalidate webhook)를 1줄 팩토리로 표준화. 블로그 조회와 *동일한* 단일\n * customer API key(`ROOTTALE_API_KEY`)를 사용한다.\n *\n * 설계 원칙:\n * - feed/sitemap 은 `fetchPosts` 실패를 swallow → 라우트가 *항상 200* (빌드 시\n * 정적 생성이 throw 해서 404 나는 사고 방지).\n * - revalidate 는 admin(mysite.roottale.com)에서 글/카테고리/미디어 변경 시 보내는\n * 서명 webhook 을 검증하고, 본문 paths + 파생 경로(feed·sitemap·블로그 인덱스)를\n * 모두 revalidate → 각 사이트에 자동 반영.\n * - `next` 에 직접 의존하지 않음: revalidate 콜백(`revalidatePath`)을 주입받는다.\n *\n * 사용 예는 roottale-docs `services/external-site-integration.md` 참조.\n */\n\nimport {\n fetchPosts,\n generateRssXml,\n rssItemsFromPosts,\n type CmsPostContent,\n} from \"@roottale/cms-client/server\";\nimport {\n verifyRootTaleWebhook,\n type VerifyRootTaleWebhookResult,\n} from \"@roottale/cms-client/webhook\";\n\nexport interface RootTaleSiteConfig {\n /** `rtlk_cust_*` — 블로그 조회와 같은 키. 서버에서만(절대 브라우저 금지). */\n apiKey: string;\n /** API base 오버라이드. 기본 `https://api.roottale.com`. */\n apiBase?: string;\n /** 사이트 절대 URL (뒤 슬래시 없이). 예: `https://kjmtax.roottale.app`. */\n siteUrl: string;\n /** 피드/사이트 제목. */\n title: string;\n description?: string;\n /** 기본 \"ko-KR\". */\n language?: string;\n /** 블로그 base path. 기본 \"/blog\". */\n blogBasePath?: string;\n}\n\nfunction trimSlash(u: string): string {\n return u.replace(/\\/+$/, \"\");\n}\n\nasync function safeFetchPosts(\n config: RootTaleSiteConfig,\n limit: number,\n): Promise<CmsPostContent[]> {\n try {\n const page = await fetchPosts({\n apiKey: config.apiKey,\n baseUrl: config.apiBase,\n type: \"post\",\n limit,\n });\n return page.items;\n } catch (err) {\n // 빌드/런타임 fetch 실패 시 빈 목록 — 라우트는 항상 정상 응답(404 방지).\n console.error(\"[@roottale/cms-renderer-next/routes] fetchPosts 실패:\", err);\n return [];\n }\n}\n\n/**\n * `app/feed.xml/route.ts` 용 GET 핸들러. RSS 2.0.\n *\n * ```ts\n * import { createFeedRoute } from \"@roottale/cms-renderer-next/routes\";\n * export const dynamic = \"force-dynamic\";\n * export const GET = createFeedRoute({\n * apiKey: process.env.ROOTTALE_API_KEY!,\n * siteUrl: \"https://example.com\",\n * title: \"Example 블로그\",\n * description: \"...\",\n * });\n * ```\n */\nexport function createFeedRoute(\n config: RootTaleSiteConfig,\n): () => Promise<Response> {\n return async function GET(): Promise<Response> {\n const siteUrl = trimSlash(config.siteUrl);\n const blogBase = config.blogBasePath ?? \"/blog\";\n const posts = await safeFetchPosts(config, 50);\n const xml = generateRssXml({\n title: config.title,\n link: `${siteUrl}${blogBase}`,\n feedUrl: `${siteUrl}/feed.xml`,\n description: config.description,\n language: config.language,\n items: rssItemsFromPosts(posts, {\n postUrl: (p) => `${siteUrl}${blogBase}/${p.slug}`,\n }),\n });\n return new Response(xml, {\n headers: {\n \"content-type\": \"application/rss+xml; charset=utf-8\",\n \"cache-control\": \"public, max-age=1800, s-maxage=1800\",\n },\n });\n };\n}\n\n/** Next `MetadataRoute.Sitemap[number]` 와 구조 호환(타입 결합 회피). */\nexport interface SitemapEntry {\n url: string;\n lastModified?: string | Date;\n changeFrequency?:\n | \"always\"\n | \"hourly\"\n | \"daily\"\n | \"weekly\"\n | \"monthly\"\n | \"yearly\"\n | \"never\";\n priority?: number;\n}\n\n/**\n * `app/sitemap.ts` default export. 정적 경로 + 글 경로를 합친다.\n *\n * ```ts\n * import { createSitemap } from \"@roottale/cms-renderer-next/routes\";\n * export default createSitemap(\n * { apiKey: process.env.ROOTTALE_API_KEY!, siteUrl: \"https://example.com\", title: \"...\" },\n * [{ url: \"https://example.com\", changeFrequency: \"weekly\", priority: 1 }],\n * );\n * ```\n */\nexport function createSitemap(\n config: RootTaleSiteConfig,\n staticRoutes: SitemapEntry[] = [],\n): () => Promise<SitemapEntry[]> {\n return async function sitemap(): Promise<SitemapEntry[]> {\n const siteUrl = trimSlash(config.siteUrl);\n const blogBase = config.blogBasePath ?? \"/blog\";\n const posts = await safeFetchPosts(config, 200);\n const postEntries: SitemapEntry[] = posts.map((p) => ({\n url: `${siteUrl}${blogBase}/${p.slug}`,\n lastModified: p.updatedAt ?? p.publishedAt ?? undefined,\n changeFrequency: \"weekly\",\n priority: 0.6,\n }));\n return [...staticRoutes, ...postEntries];\n };\n}\n\nexport interface RevalidateRouteConfig {\n /** `rtlk_cust_*` — 블로그 조회/webhook 검증 공통 키. */\n apiKey: string;\n apiBase?: string;\n /** Next 의 `revalidatePath` (또는 동등 콜백) 주입. */\n revalidate: (path: string) => void | Promise<void>;\n /** 블로그 base path. 기본 \"/blog\". */\n blogBasePath?: string;\n /**\n * 모든 글/분류/미디어 변경에 대해 *항상 함께* revalidate 할 파생 경로.\n * 피드·사이트맵·블로그 인덱스처럼 \"글 목록\"에 의존하는 경로 — admin 변경이\n * 자동 반영되려면 필수. 기본: [\"/feed.xml\", \"/sitemap.xml\", blogBase].\n */\n alsoRevalidate?: string[];\n /** (옵션) jti replay 방지 hook (cms-client verify 로 전달). */\n consumeJti?: (jti: string, exp: number) => Promise<\"new\" | \"seen\">;\n}\n\n/** webhook 본문에서 paths[] 추출(서명 검증 후 호출 — body_hash 가 binding). */\nfunction pathsFromBody(rawBody: string): string[] {\n try {\n const body = JSON.parse(rawBody) as { paths?: unknown };\n if (Array.isArray(body.paths)) {\n return body.paths.filter((p): p is string => typeof p === \"string\");\n }\n } catch {\n /* 비-JSON 본문 — 파생 경로만 revalidate */\n }\n return [];\n}\n\n/**\n * `app/api/revalidate/route.ts` 용 POST 핸들러.\n *\n * admin(mysite.roottale.com)이 글/카테고리/미디어 변경 시 보내는 서명 webhook 을\n * 검증하고, 본문 paths + 파생 경로(feed·sitemap·블로그 인덱스)를 revalidate.\n *\n * ```ts\n * import { revalidatePath } from \"next/cache\";\n * import { createRevalidateRoute } from \"@roottale/cms-renderer-next/routes\";\n * export const POST = createRevalidateRoute({\n * apiKey: process.env.ROOTTALE_API_KEY!,\n * revalidate: revalidatePath,\n * });\n * ```\n */\nexport function createRevalidateRoute(\n config: RevalidateRouteConfig,\n): (req: Request) => Promise<Response> {\n const blogBase = config.blogBasePath ?? \"/blog\";\n const alsoRevalidate = config.alsoRevalidate ?? [\n \"/feed.xml\",\n \"/sitemap.xml\",\n blogBase,\n ];\n\n return async function POST(req: Request): Promise<Response> {\n const rawBody = await req.text();\n const result: VerifyRootTaleWebhookResult = await verifyRootTaleWebhook({\n rawBody,\n headers: req.headers,\n apiKey: config.apiKey,\n apiBase: config.apiBase,\n consumeJti: config.consumeJti,\n });\n if (!result.ok) {\n return Response.json({ ok: false, reason: result.reason }, { status: 401 });\n }\n\n // 본문 paths + 파생 경로(항상) — 중복 제거.\n const paths = [...new Set([...pathsFromBody(rawBody), ...alsoRevalidate])];\n for (const p of paths) {\n await config.revalidate(p);\n }\n\n return Response.json({\n ok: true,\n event: result.event,\n deliveryId: result.deliveryId,\n revalidated: paths,\n });\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/routes.ts"],"sourcesContent":["/**\n * `@roottale/cms-renderer-next/routes` — Next.js App Router 통합 라우트 팩토리.\n *\n * 외부 고객사 사이트가 매번 직접 작성하던 공통 라우트(RSS feed / sitemap /\n * revalidate webhook)를 1줄 팩토리로 표준화. 블로그 조회와 *동일한* 단일\n * customer API key(`ROOTTALE_API_KEY`)를 사용한다.\n *\n * 설계 원칙:\n * - feed/sitemap 은 `fetchPosts` 실패를 swallow → 라우트가 *항상 200* (빌드 시\n * 정적 생성이 throw 해서 404 나는 사고 방지).\n * - revalidate 는 admin(mysite.roottale.com)에서 글/카테고리/미디어 변경 시 보내는\n * 서명 webhook 을 검증하고, 본문 paths + 파생 경로(feed·sitemap·블로그 인덱스)를\n * 모두 revalidate → 각 사이트에 자동 반영.\n * - `next` 에 직접 의존하지 않음: revalidate 콜백(`revalidatePath`)을 주입받는다.\n *\n * 사용 예는 roottale-docs `services/external-site-integration.md` 참조.\n */\n\nimport {\n fetchPosts,\n generateRssXml,\n rssItemsFromPosts,\n type CmsPostContent,\n} from \"@roottale/cms-client/server\";\nimport {\n verifyRootTaleWebhook,\n type VerifyRootTaleWebhookResult,\n} from \"@roottale/cms-client/webhook\";\nimport { collectCategories } from \"./blog-filter.js\";\n\nexport interface RootTaleSiteConfig {\n /** `rtlk_cust_*` — 블로그 조회와 같은 키. 서버에서만(절대 브라우저 금지). */\n apiKey: string;\n /** API base 오버라이드. 기본 `https://api.roottale.com`. */\n apiBase?: string;\n /** 사이트 절대 URL (뒤 슬래시 없이). 예: `https://kjmtax.roottale.app`. */\n siteUrl: string;\n /** 피드/사이트 제목. */\n title: string;\n description?: string;\n /** 기본 \"ko-KR\". */\n language?: string;\n /** 블로그 base path. 기본 \"/blog\". */\n blogBasePath?: string;\n}\n\nfunction trimSlash(u: string): string {\n return u.replace(/\\/+$/, \"\");\n}\n\nasync function safeFetchPosts(\n config: RootTaleSiteConfig,\n limit: number,\n): Promise<CmsPostContent[]> {\n try {\n const page = await fetchPosts({\n apiKey: config.apiKey,\n baseUrl: config.apiBase,\n type: \"post\",\n limit,\n });\n return page.items;\n } catch (err) {\n // 빌드/런타임 fetch 실패 시 빈 목록 — 라우트는 항상 정상 응답(404 방지).\n console.error(\"[@roottale/cms-renderer-next/routes] fetchPosts 실패:\", err);\n return [];\n }\n}\n\n/**\n * `app/feed.xml/route.ts` 용 GET 핸들러. RSS 2.0.\n *\n * ```ts\n * import { createFeedRoute } from \"@roottale/cms-renderer-next/routes\";\n * export const dynamic = \"force-dynamic\";\n * export const GET = createFeedRoute({\n * apiKey: process.env.ROOTTALE_API_KEY!,\n * siteUrl: \"https://example.com\",\n * title: \"Example 블로그\",\n * description: \"...\",\n * });\n * ```\n */\nexport function createFeedRoute(\n config: RootTaleSiteConfig,\n): () => Promise<Response> {\n return async function GET(): Promise<Response> {\n const siteUrl = trimSlash(config.siteUrl);\n const blogBase = config.blogBasePath ?? \"/blog\";\n const posts = await safeFetchPosts(config, 50);\n const xml = generateRssXml({\n title: config.title,\n link: `${siteUrl}${blogBase}`,\n feedUrl: `${siteUrl}/feed.xml`,\n description: config.description,\n language: config.language,\n items: rssItemsFromPosts(posts, {\n postUrl: (p) => `${siteUrl}${blogBase}/${p.slug}`,\n }),\n });\n return new Response(xml, {\n headers: {\n \"content-type\": \"application/rss+xml; charset=utf-8\",\n \"cache-control\": \"public, max-age=1800, s-maxage=1800\",\n },\n });\n };\n}\n\n/** Next `MetadataRoute.Sitemap[number]` 와 구조 호환(타입 결합 회피). */\nexport interface SitemapEntry {\n url: string;\n lastModified?: string | Date;\n changeFrequency?:\n | \"always\"\n | \"hourly\"\n | \"daily\"\n | \"weekly\"\n | \"monthly\"\n | \"yearly\"\n | \"never\";\n priority?: number;\n}\n\n/**\n * `app/sitemap.ts` default export. 정적 경로 + 글 경로를 합친다.\n *\n * ```ts\n * import { createSitemap } from \"@roottale/cms-renderer-next/routes\";\n * export default createSitemap(\n * { apiKey: process.env.ROOTTALE_API_KEY!, siteUrl: \"https://example.com\", title: \"...\" },\n * [{ url: \"https://example.com\", changeFrequency: \"weekly\", priority: 1 }],\n * );\n * ```\n */\nexport function createSitemap(\n config: RootTaleSiteConfig,\n staticRoutes: SitemapEntry[] = [],\n): () => Promise<SitemapEntry[]> {\n return async function sitemap(): Promise<SitemapEntry[]> {\n const siteUrl = trimSlash(config.siteUrl);\n const blogBase = config.blogBasePath ?? \"/blog\";\n const posts = await safeFetchPosts(config, 200);\n const postEntries: SitemapEntry[] = posts.map((p) => ({\n url: `${siteUrl}${blogBase}/${p.slug}`,\n lastModified: p.updatedAt ?? p.publishedAt ?? undefined,\n changeFrequency: \"weekly\",\n priority: 0.6,\n }));\n const categories = collectCategories(posts);\n const categoryEntries: SitemapEntry[] = categories.length\n ? [\n {\n url: `${siteUrl}${blogBase}/categories`,\n changeFrequency: \"weekly\",\n priority: 0.5,\n },\n ...categories.map((category) => ({\n url: `${siteUrl}${blogBase}/categories/${category.slug}`,\n changeFrequency: \"weekly\" as const,\n priority: 0.5,\n })),\n ]\n : [];\n return [...staticRoutes, ...postEntries, ...categoryEntries];\n };\n}\n\nexport interface RevalidateRouteConfig {\n /** `rtlk_cust_*` — 블로그 조회/webhook 검증 공통 키. */\n apiKey: string;\n apiBase?: string;\n /** Next 의 `revalidatePath` (또는 동등 콜백) 주입. */\n revalidate: (path: string) => void | Promise<void>;\n /** 블로그 base path. 기본 \"/blog\". */\n blogBasePath?: string;\n /**\n * 모든 글/분류/미디어 변경에 대해 *항상 함께* revalidate 할 파생 경로.\n * 피드·사이트맵·블로그 인덱스처럼 \"글 목록\"에 의존하는 경로 — admin 변경이\n * 자동 반영되려면 필수. 기본: [\"/feed.xml\", \"/sitemap.xml\", blogBase].\n */\n alsoRevalidate?: string[];\n /** (옵션) jti replay 방지 hook (cms-client verify 로 전달). */\n consumeJti?: (jti: string, exp: number) => Promise<\"new\" | \"seen\">;\n}\n\n/** webhook 본문에서 paths[] 추출(서명 검증 후 호출 — body_hash 가 binding). */\nfunction pathsFromBody(rawBody: string): string[] {\n try {\n const body = JSON.parse(rawBody) as { paths?: unknown };\n if (Array.isArray(body.paths)) {\n return body.paths.filter((p): p is string => typeof p === \"string\");\n }\n } catch {\n /* 비-JSON 본문 — 파생 경로만 revalidate */\n }\n return [];\n}\n\n/**\n * `app/api/revalidate/route.ts` 용 POST 핸들러.\n *\n * admin(mysite.roottale.com)이 글/카테고리/미디어 변경 시 보내는 서명 webhook 을\n * 검증하고, 본문 paths + 파생 경로(feed·sitemap·블로그 인덱스)를 revalidate.\n *\n * ```ts\n * import { revalidatePath } from \"next/cache\";\n * import { createRevalidateRoute } from \"@roottale/cms-renderer-next/routes\";\n * export const POST = createRevalidateRoute({\n * apiKey: process.env.ROOTTALE_API_KEY!,\n * revalidate: revalidatePath,\n * });\n * ```\n */\nexport function createRevalidateRoute(\n config: RevalidateRouteConfig,\n): (req: Request) => Promise<Response> {\n const blogBase = config.blogBasePath ?? \"/blog\";\n const alsoRevalidate = config.alsoRevalidate ?? [\n \"/feed.xml\",\n \"/sitemap.xml\",\n blogBase,\n `${blogBase}/categories`,\n ];\n\n return async function POST(req: Request): Promise<Response> {\n const rawBody = await req.text();\n const result: VerifyRootTaleWebhookResult = await verifyRootTaleWebhook({\n rawBody,\n headers: req.headers,\n apiKey: config.apiKey,\n apiBase: config.apiBase,\n consumeJti: config.consumeJti,\n });\n if (!result.ok) {\n return Response.json({ ok: false, reason: result.reason }, { status: 401 });\n }\n\n // 본문 paths + 파생 경로(항상) — 중복 제거.\n const paths = [...new Set([...pathsFromBody(rawBody), ...alsoRevalidate])];\n for (const p of paths) {\n await config.revalidate(p);\n }\n\n return Response.json({\n ok: true,\n event: result.event,\n deliveryId: result.deliveryId,\n revalidated: paths,\n });\n };\n}\n"],"mappings":"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","names":[]}
package/dist/server.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ReactElement, ReactNode } from 'react';
2
- import { CmsPostContent, CmsPostType, RootTaleTheme } from '@roottale/cms-client/server';
2
+ import { CmsPostContent, RootTaleTheme, CmsPostType } from '@roottale/cms-client/server';
3
3
  export { RootTaleTheme } from '@roottale/cms-client/server';
4
- import { Block } from '@roottale/cms-core';
4
+ import { Block, BindingResolver } from '@roottale/cms-core';
5
5
 
6
6
  /**
7
7
  * `<RootTalePostCard>` — server-only summary card for a single CmsPostContent.
@@ -159,6 +159,15 @@ declare function RootTaleLeadForm(props: RootTaleLeadFormProps): ReactElement;
159
159
  declare function renderBlock(block: Block, key?: number): ReactElement;
160
160
  /** Block tree 전체를 React element 배열로 직렬화. */
161
161
  declare function renderBlocks(blocks: readonly Block[]): ReactElement[];
162
+ /**
163
+ * ADR-0041 §7 — Block Bindings 를 먼저 적용한 뒤 렌더.
164
+ * `metadata.bindings` 의 `roottale/field`/`acf/field` 를 `fields`(acf 값)로
165
+ * 치환. 바인딩 없으면 renderBlocks 와 동일.
166
+ */
167
+ declare function renderBoundBlocks(blocks: readonly Block[], opts?: {
168
+ fields?: Record<string, unknown> | null;
169
+ resolver?: BindingResolver;
170
+ }): ReactElement[];
162
171
 
163
172
  /**
164
173
  * `RootTaleBlogList` 카테고리 필터 — 순수 헬퍼(서버 필터, client JS 불요).
@@ -172,14 +181,18 @@ interface BlogCategory {
172
181
  name: string;
173
182
  slug: string;
174
183
  }
184
+ interface BlogCategoryWithCount extends BlogCategory {
185
+ count: number;
186
+ }
175
187
  /**
176
188
  * 글 목록에 등장하는 카테고리를 slug 기준 unique 로 수집. 서버가 보내는 정렬
177
189
  * (category → name asc)을 보존하기 위해 처음 등장 순서를 유지한다.
178
190
  */
179
191
  declare function collectCategories(posts: CmsPostContent[]): BlogCategory[];
192
+ declare function collectCategoriesWithCounts(posts: CmsPostContent[]): BlogCategoryWithCount[];
180
193
  /** activeSlug 가 비면 전체 반환. 아니면 그 카테고리를 가진 글만. */
181
194
  declare function filterPostsByCategory(posts: CmsPostContent[], activeSlug: string | null | undefined): CmsPostContent[];
182
- /** 기본 칩 링크: 전체 → `?`, 특정 → `?category=<slug>`. */
195
+ /** 기본 카테고리 링크: 전체 → `/blog`, 특정 → `/blog/categories/<slug>`. */
183
196
  declare function defaultCategoryHref(slug: string | null): string;
184
197
 
185
198
  /**
@@ -264,6 +277,18 @@ type RootTaleBlogListProps = {
264
277
  theme?: RootTaleThemeInput;
265
278
  };
266
279
  declare function RootTaleBlogList(props: RootTaleBlogListProps): Promise<ReactElement>;
280
+ type RootTaleBlogCategoriesProps = {
281
+ apiKey: string;
282
+ baseUrl?: string;
283
+ limit?: number;
284
+ /** Build a customer-side URL for a category. Defaults to `/blog/categories/<slug>`. */
285
+ categoryHref?: (slug: string) => string;
286
+ /** Rendered when no categories are attached to published posts yet. */
287
+ emptyMessage?: string;
288
+ /** See `RootTaleBlogListProps['theme']`. */
289
+ theme?: RootTaleThemeInput;
290
+ };
291
+ declare function RootTaleBlogCategories(props: RootTaleBlogCategoriesProps): Promise<ReactElement>;
267
292
  type RootTaleBlogPostProps = {
268
293
  apiKey: string;
269
294
  slugOrId: string;
@@ -282,5 +307,24 @@ type RootTaleBlogPostProps = {
282
307
  theme?: RootTaleThemeInput;
283
308
  };
284
309
  declare function RootTaleBlogPost(props: RootTaleBlogPostProps): Promise<ReactElement>;
310
+ /**
311
+ * Minimal Tiptap doc renderer. Walks the Tiptap doc tree and emits semantic
312
+ * HTML. This is intentionally tiny — richer extensions (image asset URL
313
+ * resolution, TableOfContents, FloatingCta) belong in followup §5.7 #5.
314
+ *
315
+ * Browser-side sanitization is unnecessary here because output is rendered
316
+ * server-side from data the customer-trusted API returned. If the customer
317
+ * site federates with untrusted authors later, layer a sanitizer in front
318
+ * of this component.
319
+ */
320
+ declare function RenderTiptap({ doc, fields, }: {
321
+ doc: Record<string, unknown>;
322
+ /**
323
+ * ADR-0041 D3 — 공개 API `post.fields` (포맷된 커스텀 필드 값). 전달 시
324
+ * 블록 바인딩(`roottale/field`)을 필드 값으로 치환 — 값 없으면 본문이
325
+ * fallback 으로 그대로 렌더. astro `renderTiptapDoc(doc, { fields })` 동등.
326
+ */
327
+ fields?: Record<string, unknown> | null;
328
+ }): ReactElement;
285
329
 
286
- export { type BlogCategory, type CtaButton, type CtaButtonType, type FloatingCtaPosition, type LeadFormVertical, RootTaleBlogList, type RootTaleBlogListProps, RootTaleBlogPost, type RootTaleBlogPostProps, RootTaleFloatingCta, type RootTaleFloatingCtaProps, RootTaleLeadForm, type RootTaleLeadFormProps, RootTalePostCard, type RootTalePostCardProps, RootTaleTableOfContents, type RootTaleTableOfContentsProps, type RootTaleThemeInput, RootTaleThemeProvider, type TocEntry, attachHeadingIds, collectCategories, defaultCategoryHref, extractToc, filterPostsByCategory, headingToId, renderBlock, renderBlocks };
330
+ export { type BlogCategory, type BlogCategoryWithCount, type CtaButton, type CtaButtonType, type FloatingCtaPosition, type LeadFormVertical, RenderTiptap, RootTaleBlogCategories, type RootTaleBlogCategoriesProps, RootTaleBlogList, type RootTaleBlogListProps, RootTaleBlogPost, type RootTaleBlogPostProps, RootTaleFloatingCta, type RootTaleFloatingCtaProps, RootTaleLeadForm, type RootTaleLeadFormProps, RootTalePostCard, type RootTalePostCardProps, RootTaleTableOfContents, type RootTaleTableOfContentsProps, type RootTaleThemeInput, RootTaleThemeProvider, type TocEntry, attachHeadingIds, collectCategories, collectCategoriesWithCounts, defaultCategoryHref, extractToc, filterPostsByCategory, headingToId, renderBlock, renderBlocks, renderBoundBlocks };
package/dist/server.js CHANGED
@@ -1,3 +1,10 @@
1
+ import {
2
+ collectCategories,
3
+ collectCategoriesWithCounts,
4
+ defaultCategoryHref,
5
+ filterPostsByCategory
6
+ } from "./chunk-QWBFHYB6.js";
7
+
1
8
  // src/server.tsx
2
9
  import {
3
10
  DEFAULT_BLOG_SETTINGS,
@@ -7,7 +14,12 @@ import {
7
14
  fetchTheme,
8
15
  resolvePostDisplay
9
16
  } from "@roottale/cms-client/server";
10
- import { isAllowedEmbedSrc } from "@roottale/cms-core";
17
+ import {
18
+ bindingSrcValue,
19
+ bindingTextValue,
20
+ boundFieldValue,
21
+ isAllowedEmbedSrc
22
+ } from "@roottale/cms-core";
11
23
 
12
24
  // src/PostCard.tsx
13
25
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -87,31 +99,6 @@ function RootTaleTableOfContents(props) {
87
99
  ] }) });
88
100
  }
89
101
 
90
- // src/blog-filter.ts
91
- function categoryTerms(post) {
92
- return (post.terms ?? []).filter((t) => t.taxonomy === "category");
93
- }
94
- function collectCategories(posts) {
95
- const seen = /* @__PURE__ */ new Set();
96
- const out = [];
97
- for (const post of posts) {
98
- for (const t of categoryTerms(post)) {
99
- if (!seen.has(t.slug)) {
100
- seen.add(t.slug);
101
- out.push({ name: t.name, slug: t.slug });
102
- }
103
- }
104
- }
105
- return out;
106
- }
107
- function filterPostsByCategory(posts, activeSlug) {
108
- if (!activeSlug) return posts;
109
- return posts.filter((p) => categoryTerms(p).some((t) => t.slug === activeSlug));
110
- }
111
- function defaultCategoryHref(slug) {
112
- return slug ? `?category=${encodeURIComponent(slug)}` : "?";
113
- }
114
-
115
102
  // src/toc.ts
116
103
  function tocLevel(rawLevel) {
117
104
  if (rawLevel < 1 || rawLevel > 4) return null;
@@ -329,7 +316,9 @@ function RootTaleLeadForm(props) {
329
316
  name: "phone",
330
317
  required: true,
331
318
  autoComplete: "tel",
332
- placeholder: "010-0000-0000",
319
+ inputMode: "tel",
320
+ pattern: "0\\d{1,2}-?\\d{3,4}-?\\d{4}",
321
+ placeholder: "010-1234-5678",
333
322
  className: "rt-cms-field-input"
334
323
  }
335
324
  )
@@ -375,7 +364,11 @@ function RootTaleLeadForm(props) {
375
364
  }
376
365
 
377
366
  // src/block-to-react.tsx
378
- import { sanitizeHtml } from "@roottale/cms-core";
367
+ import {
368
+ applyBlockBindings,
369
+ createFieldBindingResolver,
370
+ sanitizeHtml
371
+ } from "@roottale/cms-core";
379
372
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
380
373
  function isString(value) {
381
374
  return typeof value === "string";
@@ -511,9 +504,18 @@ function renderBlock(block, key = 0) {
511
504
  function renderBlocks(blocks) {
512
505
  return blocks.map((b, i) => renderBlock(b, i));
513
506
  }
507
+ function renderBoundBlocks(blocks, opts = {}) {
508
+ const resolver = opts.resolver ?? createFieldBindingResolver(opts.fields);
509
+ return renderBlocks(applyBlockBindings(blocks, resolver));
510
+ }
514
511
 
515
512
  // src/server.tsx
516
513
  import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
514
+ function authorImageRadiusCss(radius) {
515
+ if (radius === "circle") return "999px";
516
+ if (radius === "rounded") return "0.875rem";
517
+ return "0";
518
+ }
517
519
  function RootTaleThemeProvider(props) {
518
520
  const style = themeToCssVars(props.theme ?? null);
519
521
  if (!style) return /* @__PURE__ */ jsx6(Fragment2, { children: props.children });
@@ -577,6 +579,41 @@ async function RootTaleBlogList(props) {
577
579
  ) }, post.id)) })
578
580
  ] });
579
581
  }
582
+ async function RootTaleBlogCategories(props) {
583
+ const {
584
+ apiKey,
585
+ baseUrl,
586
+ limit,
587
+ categoryHref = (slug) => defaultCategoryHref(slug),
588
+ emptyMessage = "\uC544\uC9C1 \uCE74\uD14C\uACE0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
589
+ theme: themeProp
590
+ } = props;
591
+ const [page, themeStyle] = await Promise.all([
592
+ fetchPosts({ apiKey, baseUrl, limit, type: "post" }),
593
+ resolveThemeStyle({ themeProp, apiKey, baseUrl })
594
+ ]);
595
+ const categories = collectCategoriesWithCounts(page.items);
596
+ return /* @__PURE__ */ jsx6(
597
+ "nav",
598
+ {
599
+ "data-roottale-cms": "categories",
600
+ className: "rt-cms-categories",
601
+ "aria-label": "\uBE14\uB85C\uADF8 \uCE74\uD14C\uACE0\uB9AC",
602
+ style: themeStyle ?? void 0,
603
+ children: categories.length === 0 ? /* @__PURE__ */ jsx6("p", { className: "rt-cms-empty", children: emptyMessage }) : /* @__PURE__ */ jsx6("ul", { className: "rt-cms-category-list", children: categories.map((category) => /* @__PURE__ */ jsx6("li", { className: "rt-cms-category-list-item", children: /* @__PURE__ */ jsxs6(
604
+ "a",
605
+ {
606
+ className: "rt-cms-category-link",
607
+ href: categoryHref(category.slug),
608
+ children: [
609
+ /* @__PURE__ */ jsx6("span", { className: "rt-cms-category-name", children: category.name }),
610
+ /* @__PURE__ */ jsx6("span", { className: "rt-cms-category-count", children: category.count })
611
+ ]
612
+ }
613
+ ) }, category.slug)) })
614
+ }
615
+ );
616
+ }
580
617
  async function RootTaleBlogPost(props) {
581
618
  const {
582
619
  apiKey,
@@ -607,9 +644,14 @@ async function RootTaleBlogPost(props) {
607
644
  const postAuthorBio = post.authorBio;
608
645
  const publishedDate = display.showDate ? formatPublishedDate2(post.publishedAt) : "";
609
646
  const showTopAuthor = display.showAuthor && Boolean(authorName);
610
- const displayAuthorName = display.authorProfileName ?? authorName;
611
- const authorBio = display.authorProfileBio ?? postAuthorBio ?? display.authorCardDescription;
612
- const authorImageUrlForCard = display.authorProfileImageUrl ?? authorImageUrl;
647
+ const displayAuthorName = authorName ?? display.authorProfileName;
648
+ const authorBio = postAuthorBio ?? display.authorProfileBio ?? display.authorCardDescription;
649
+ const authorImageUrlForCard = authorImageUrl ?? display.authorProfileImageUrl;
650
+ const authorImageAlt = `${displayAuthorName ?? ""} \uD504\uB85C\uD544 \uC0AC\uC9C4`.trim();
651
+ const authorImageStyle = {
652
+ borderRadius: authorImageRadiusCss(display.authorProfileImageRadius),
653
+ objectPosition: `${display.authorProfileImagePositionX}% ${display.authorProfileImagePositionY}%`
654
+ };
613
655
  const showAuthorCard = display.showAuthorCard && Boolean(displayAuthorName);
614
656
  return /* @__PURE__ */ jsxs6(
615
657
  "article",
@@ -630,15 +672,16 @@ async function RootTaleBlogPost(props) {
630
672
  post.excerpt ? /* @__PURE__ */ jsx6("p", { className: "rt-cms-excerpt", children: post.excerpt }) : null
631
673
  ] }),
632
674
  toc.length > 0 ? /* @__PURE__ */ jsx6(RootTaleTableOfContents, { entries: toc, title: display.tocTitle }) : null,
633
- /* @__PURE__ */ jsx6("div", { "data-roottale-cms": "body", className: "rt-cms-body", children: /* @__PURE__ */ jsx6(RenderTiptap, { doc: renderDoc }) }),
675
+ /* @__PURE__ */ jsx6("div", { "data-roottale-cms": "body", className: "rt-cms-body", children: /* @__PURE__ */ jsx6(RenderTiptap, { doc: renderDoc, fields: post.fields }) }),
634
676
  showAuthorCard && displayAuthorName ? /* @__PURE__ */ jsxs6("section", { className: "rt-cms-author-card", "aria-label": "\uC791\uC131\uC790", children: [
635
677
  authorImageUrlForCard ? /* @__PURE__ */ jsx6(
636
678
  "img",
637
679
  {
638
680
  className: "rt-cms-author-card__image",
639
681
  src: authorImageUrlForCard,
640
- alt: "",
641
- loading: "lazy"
682
+ alt: authorImageAlt,
683
+ loading: "lazy",
684
+ style: authorImageStyle
642
685
  }
643
686
  ) : /* @__PURE__ */ jsx6("div", { className: "rt-cms-author-card__avatar", "aria-hidden": "true", children: authorInitials(displayAuthorName) }),
644
687
  /* @__PURE__ */ jsxs6("div", { className: "rt-cms-author-card__body", children: [
@@ -702,10 +745,18 @@ function formatPublishedDate2(iso) {
702
745
  day: "2-digit"
703
746
  }).format(d);
704
747
  }
705
- function RenderTiptap({ doc }) {
748
+ function RenderTiptap({
749
+ doc,
750
+ fields
751
+ }) {
706
752
  const rawContent = doc.content;
707
753
  const content = Array.isArray(rawContent) ? rawContent : [];
708
- return /* @__PURE__ */ jsx6(Fragment2, { children: content.map((node, i) => renderNode(node, i)) });
754
+ return /* @__PURE__ */ jsx6(Fragment2, { children: content.map((node, i) => renderNode(node, i, fields ?? null)) });
755
+ }
756
+ function boundContentText(node, fields) {
757
+ return bindingTextValue(
758
+ boundFieldValue(node.attrs?.bindings, "content", fields)
759
+ );
709
760
  }
710
761
  function isSafeHref(value) {
711
762
  if (typeof value !== "string") return false;
@@ -722,21 +773,24 @@ function alignStyle(node) {
722
773
  }
723
774
  return void 0;
724
775
  }
725
- function renderNode(node, key) {
776
+ function renderNode(node, key, fields = null) {
726
777
  if (!node || typeof node !== "object") return null;
727
- const children = Array.isArray(node.content) ? node.content.map((child, i) => renderNode(child, i)) : void 0;
778
+ const children = Array.isArray(node.content) ? node.content.map((child, i) => renderNode(child, i, fields)) : void 0;
728
779
  switch (node.type) {
729
- case "paragraph":
730
- return /* @__PURE__ */ jsx6("p", { style: alignStyle(node), children }, key);
780
+ case "paragraph": {
781
+ const bound = boundContentText(node, fields);
782
+ return /* @__PURE__ */ jsx6("p", { style: alignStyle(node), children: bound ?? children }, key);
783
+ }
731
784
  case "heading": {
732
785
  const rawLevel = Number(node.attrs?.level ?? 2);
733
786
  const level = Math.min(Math.max(Number.isFinite(rawLevel) ? rawLevel : 2, 2), 4);
734
787
  const idAttr = node.attrs?.id;
735
788
  const id = typeof idAttr === "string" && idAttr.length > 0 ? idAttr : void 0;
736
789
  const style = alignStyle(node);
737
- if (level === 2) return /* @__PURE__ */ jsx6("h2", { id, style, children }, key);
738
- if (level === 3) return /* @__PURE__ */ jsx6("h3", { id, style, children }, key);
739
- return /* @__PURE__ */ jsx6("h4", { id, style, children }, key);
790
+ const inner = boundContentText(node, fields) ?? children;
791
+ if (level === 2) return /* @__PURE__ */ jsx6("h2", { id, style, children: inner }, key);
792
+ if (level === 3) return /* @__PURE__ */ jsx6("h3", { id, style, children: inner }, key);
793
+ return /* @__PURE__ */ jsx6("h4", { id, style, children: inner }, key);
740
794
  }
741
795
  case "bulletList":
742
796
  return /* @__PURE__ */ jsx6("ul", { children }, key);
@@ -753,7 +807,10 @@ function renderNode(node, key) {
753
807
  case "hardBreak":
754
808
  return /* @__PURE__ */ jsx6("br", {}, key);
755
809
  case "image": {
756
- const src = node.attrs?.src;
810
+ const boundSrc = bindingSrcValue(
811
+ boundFieldValue(node.attrs?.bindings, "src", fields)
812
+ );
813
+ const src = boundSrc ?? node.attrs?.src;
757
814
  if (typeof src !== "string" || src.length === 0) return null;
758
815
  const alt = typeof node.attrs?.alt === "string" ? node.attrs.alt : "";
759
816
  const title = typeof node.attrs?.title === "string" ? node.attrs.title : void 0;
@@ -846,6 +903,8 @@ function applyMark(mark, child) {
846
903
  }
847
904
  }
848
905
  export {
906
+ RenderTiptap,
907
+ RootTaleBlogCategories,
849
908
  RootTaleBlogList,
850
909
  RootTaleBlogPost,
851
910
  RootTaleFloatingCta,
@@ -855,11 +914,13 @@ export {
855
914
  RootTaleThemeProvider,
856
915
  attachHeadingIds,
857
916
  collectCategories,
917
+ collectCategoriesWithCounts,
858
918
  defaultCategoryHref,
859
919
  extractToc,
860
920
  filterPostsByCategory,
861
921
  headingToId,
862
922
  renderBlock,
863
- renderBlocks
923
+ renderBlocks,
924
+ renderBoundBlocks
864
925
  };
865
926
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.tsx","../src/PostCard.tsx","../src/TableOfContents.tsx","../src/blog-filter.ts","../src/toc.ts","../src/FloatingCta.tsx","../src/LeadForm.tsx","../src/block-to-react.tsx"],"sourcesContent":["/**\n * `@roottale/cms-renderer-next/server` — RootTale CMS public-render React Server Components.\n *\n * Drop into any RSC-capable framework (Next.js App Router, Astro server islands,\n * React Router server). Single-line import, SSR-only, no `'use client'` boundary.\n *\n * ```tsx\n * import { RootTaleBlogList } from \"@roottale/cms-renderer-next/server\";\n *\n * export default function BlogPage() {\n * return <RootTaleBlogList apiKey={process.env.ROOTTALE_API_KEY!} />;\n * }\n * ```\n *\n * Customer site MUST keep `apiKey` server-side. Browser bundle ships zero\n * RootTale credentials (ADR-0023 §5.1 #15).\n *\n * Design tokens auto-apply via the CSS import below. Customers can override\n * any class with their own CSS or Tailwind utilities — scoped selectors use\n * `:where()` to keep specificity at zero (ADR-0023 §5.1 #10).\n */\n\nimport type { CSSProperties, ReactElement, ReactNode } from \"react\";\nimport {\n type CmsPostContent,\n type CmsPostType,\n type RootTaleTheme,\n DEFAULT_BLOG_SETTINGS,\n fetchBlogSettings,\n fetchPost,\n fetchPosts,\n fetchTheme,\n resolvePostDisplay,\n} from \"@roottale/cms-client/server\";\n// CSS는 customer 가 root layout 에서 명시적으로 import:\n// `import \"@roottale/cms-renderer-next/styles\";`\n// 본 파일에서 side-effect import 하지 않음 — tsup dts build 가 .css\n// side-effect import 처리 못 함 + customer 측 framework 마다 CSS 처리\n// 방식 다르므로 명시적 import 가 명확. README 참조.\n\nimport { isAllowedEmbedSrc } from \"@roottale/cms-core\";\n\nimport { RootTalePostCard } from \"./PostCard.js\";\nimport { RootTaleTableOfContents } from \"./TableOfContents.js\";\nimport {\n collectCategories,\n defaultCategoryHref,\n filterPostsByCategory,\n} from \"./blog-filter.js\";\nimport { attachHeadingIds, extractToc } from \"./toc.js\";\n\ntype CmsPostWithAuthor = CmsPostContent & {\n authorName?: string | null;\n authorImageUrl?: string | null;\n authorBio?: string | null;\n};\n\nexport { RootTalePostCard, type RootTalePostCardProps } from \"./PostCard.js\";\nexport {\n RootTaleTableOfContents,\n type RootTaleTableOfContentsProps,\n} from \"./TableOfContents.js\";\nexport {\n RootTaleFloatingCta,\n type RootTaleFloatingCtaProps,\n type CtaButton,\n type CtaButtonType,\n type FloatingCtaPosition,\n} from \"./FloatingCta.js\";\nexport {\n RootTaleLeadForm,\n type RootTaleLeadFormProps,\n type LeadFormVertical,\n} from \"./LeadForm.js\";\nexport { renderBlock, renderBlocks } from \"./block-to-react.js\";\nexport {\n collectCategories,\n filterPostsByCategory,\n defaultCategoryHref,\n type BlogCategory,\n} from \"./blog-filter.js\";\nexport {\n attachHeadingIds,\n extractToc,\n headingToId,\n type TocEntry,\n} from \"./toc.js\";\nexport type { RootTaleTheme } from \"@roottale/cms-client/server\";\n\n/**\n * RootTaleBlogList/Post 가 받는 theme prop 의 평가 결과:\n * - `RootTaleTheme` 객체 → 그대로 CSS 변수로 주입\n * - `null` → override 없음 (cms-public.css fallback 그대로)\n * - `undefined` → API key 로 자동 fetch (mysite.roottale.com 의 admin /design)\n */\nexport type RootTaleThemeInput = RootTaleTheme | null | undefined;\n\n/**\n * RootTale 디자인 토큰을 CSS 변수로 자식 트리에 주입한다. 외부 사이트가\n * 여러 RootTale 컴포넌트를 한 레이아웃에 묶을 때, 컴포넌트마다 같은 토큰을\n * fetch 하지 않도록 부모에서 한 번만 fetch 해서 전달하는 용도.\n *\n * ```tsx\n * const theme = await fetchTheme({ apiKey });\n * <RootTaleThemeProvider theme={theme}>\n * <RootTaleBlogList apiKey={apiKey} theme={null} />\n * <RootTaleBlogPost apiKey={apiKey} slugOrId=\"...\" theme={null} />\n * </RootTaleThemeProvider>\n * ```\n *\n * 자식 RootTale 컴포넌트가 `theme={null}` 을 받으면 자체 주입을 건너뛰고\n * 부모의 변수를 그대로 상속. 자식이 `theme` 을 지정하면 자식 변수가 우선.\n */\nexport function RootTaleThemeProvider(props: {\n theme: RootTaleTheme | null | undefined;\n children: ReactNode;\n}): ReactElement {\n const style = themeToCssVars(props.theme ?? null);\n if (!style) return <>{props.children}</>;\n return (\n <div data-roottale-cms=\"theme-root\" style={style}>\n {props.children}\n </div>\n );\n}\n\nexport type RootTaleBlogListProps = {\n apiKey: string;\n baseUrl?: string;\n limit?: number;\n /** Filter by post type (`post` or `page`). Default = `post` only (blog list 의도). */\n type?: CmsPostType;\n /** Build a customer-side URL for a single post. Defaults to `/blog/${slug}`. */\n postHref?: (post: CmsPostContent) => string;\n /** Rendered when the tenant has no published posts yet. */\n emptyMessage?: string;\n /**\n * 카테고리 필터 칩을 목록 위에 렌더. SSR-only — 칩은 `<a>` 링크이고\n * `?category=<slug>` URL 파라미터로 동작한다(client JS 불요). consumer 는\n * searchParams 의 category 를 `activeCategory` 로 주입.\n */\n showCategoryFilter?: boolean;\n /** 현재 선택 카테고리 slug. 미지정/빈 값 = 전체. searchParams 에서 주입. */\n activeCategory?: string;\n /**\n * 카테고리 칩 링크 URL 빌더. `null` = \"전체\" 칩. 기본 `?category=<slug>`\n * (전체 = `?`). 별도 라우트(`/blog/category/<slug>`)를 쓰려면 override.\n */\n categoryHref?: (slug: string | null) => string;\n /** \"전체\" 칩 라벨. 기본 \"전체\". */\n allCategoriesLabel?: string;\n /**\n * RootTale 디자인 토큰. 생략 시 admin /design 에서 저장한 값을 자동 fetch.\n * `null` 을 명시하면 자동 fetch 도 건너뛰고 CSS fallback 사용 (부모\n * `<RootTaleThemeProvider>` 가 변수를 이미 설정한 경우 유용).\n */\n theme?: RootTaleThemeInput;\n};\n\nexport async function RootTaleBlogList(\n props: RootTaleBlogListProps,\n): Promise<ReactElement> {\n const {\n apiKey,\n baseUrl,\n limit,\n type = \"post\",\n postHref = defaultPostHref,\n emptyMessage = \"아직 발행된 글이 없습니다.\",\n showCategoryFilter = false,\n activeCategory,\n categoryHref = defaultCategoryHref,\n allCategoriesLabel = \"전체\",\n theme: themeProp,\n } = props;\n\n const [page, themeStyle, blogSettings] = await Promise.all([\n fetchPosts({ apiKey, baseUrl, limit, type }),\n resolveThemeStyle({ themeProp, apiKey, baseUrl }),\n fetchBlogSettings({ apiKey, baseUrl }).catch(() => DEFAULT_BLOG_SETTINGS),\n ]);\n\n if (page.items.length === 0) {\n return (\n <div data-roottale-cms=\"list\" style={themeStyle ?? undefined}>\n <p className=\"rt-cms-empty\">{emptyMessage}</p>\n </div>\n );\n }\n\n // 필터 칩은 *전체* 글의 카테고리에서 도출(선택 카테고리로 좁혀도 칩은 고정).\n const categories = showCategoryFilter ? collectCategories(page.items) : [];\n const visiblePosts = showCategoryFilter\n ? filterPostsByCategory(page.items, activeCategory)\n : page.items;\n\n return (\n <div data-roottale-cms=\"list\" style={themeStyle ?? undefined}>\n {showCategoryFilter && categories.length > 0 ? (\n <nav className=\"rt-cms-filter\" aria-label=\"카테고리 필터\">\n <a\n className=\"rt-cms-filter-chip\"\n href={categoryHref(null)}\n data-active={!activeCategory ? \"\" : undefined}\n aria-current={!activeCategory ? \"true\" : undefined}\n >\n {allCategoriesLabel}\n </a>\n {categories.map((c) => (\n <a\n key={c.slug}\n className=\"rt-cms-filter-chip\"\n href={categoryHref(c.slug)}\n data-active={activeCategory === c.slug ? \"\" : undefined}\n aria-current={activeCategory === c.slug ? \"true\" : undefined}\n >\n {c.name}\n </a>\n ))}\n </nav>\n ) : null}\n {visiblePosts.length === 0 ? (\n <p className=\"rt-cms-empty\">{emptyMessage}</p>\n ) : (\n <ul className=\"rt-cms-list\">\n {visiblePosts.map((post) => (\n <li key={post.id} className=\"rt-cms-list-item\">\n <RootTalePostCard\n post={post}\n href={postHref}\n display={resolvePostDisplay(post, blogSettings)}\n />\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nexport type RootTaleBlogPostProps = {\n apiKey: string;\n slugOrId: string;\n baseUrl?: string;\n /** Rendered when the slug is not found or not published. */\n notFoundElement?: ReactElement;\n /**\n * When true, prepends an auto-derived `<RootTaleTableOfContents>` before\n * the article body (H2-H4 headings only). Default: false — keep markup\n * stable for customers who already control their own layout.\n */\n showTableOfContents?: boolean;\n /** Title shown above the TOC (only when `showTableOfContents`). */\n tableOfContentsTitle?: string;\n /** See `RootTaleBlogListProps['theme']`. */\n theme?: RootTaleThemeInput;\n};\n\nexport async function RootTaleBlogPost(\n props: RootTaleBlogPostProps,\n): Promise<ReactElement> {\n const {\n apiKey,\n slugOrId,\n baseUrl,\n notFoundElement,\n showTableOfContents,\n tableOfContentsTitle,\n theme: themeProp,\n } = props;\n const [post, themeStyle, blogSettings] = await Promise.all([\n fetchPost({ apiKey, slugOrId, baseUrl }),\n resolveThemeStyle({ themeProp, apiKey, baseUrl }),\n // 사이트 기본 표시 설정 — 실패해도 렌더는 진행 (안전 기본값).\n fetchBlogSettings({ apiKey, baseUrl }).catch(() => DEFAULT_BLOG_SETTINGS),\n ]);\n\n if (!post) {\n return (\n notFoundElement ?? (\n <div data-roottale-cms=\"post-missing\" style={themeStyle ?? undefined}>\n <p className=\"rt-cms-empty\">글을 찾을 수 없습니다.</p>\n </div>\n )\n );\n }\n\n // 표시 결정: prop > 글별 override(metaJson.display) > 사이트 기본값.\n const display = resolvePostDisplay(post, blogSettings, {\n showTableOfContents,\n tableOfContentsTitle,\n });\n const toc = display.showTableOfContents ? extractToc(post.bodyJson) : [];\n const renderDoc =\n toc.length > 0 ? attachHeadingIds(post.bodyJson, toc) : post.bodyJson;\n const authorName = (post as CmsPostWithAuthor).authorName;\n const authorImageUrl = (post as CmsPostWithAuthor).authorImageUrl;\n const postAuthorBio = (post as CmsPostWithAuthor).authorBio;\n const publishedDate = display.showDate\n ? formatPublishedDate(post.publishedAt)\n : \"\";\n const showTopAuthor = display.showAuthor && Boolean(authorName);\n const displayAuthorName = display.authorProfileName ?? authorName;\n const authorBio =\n display.authorProfileBio ?? postAuthorBio ?? display.authorCardDescription;\n const authorImageUrlForCard =\n display.authorProfileImageUrl ?? authorImageUrl;\n const showAuthorCard = display.showAuthorCard && Boolean(displayAuthorName);\n\n return (\n <article\n data-roottale-cms=\"post\"\n className=\"rt-cms-article\"\n style={themeStyle ?? undefined}\n >\n <header className=\"rt-cms-post-header\">\n {publishedDate || showTopAuthor ? (\n <p className=\"rt-cms-meta\">\n {publishedDate ? (\n <span className=\"rt-cms-meta-item\" data-rt-cms-meta=\"date\">\n {publishedDate}\n </span>\n ) : null}\n {showTopAuthor ? (\n <span className=\"rt-cms-meta-item\" data-rt-cms-meta=\"author\">\n 작성자 {authorName}\n </span>\n ) : null}\n </p>\n ) : null}\n <h1 className=\"rt-cms-title\">{post.title}</h1>\n {post.excerpt ? <p className=\"rt-cms-excerpt\">{post.excerpt}</p> : null}\n </header>\n {toc.length > 0 ? (\n <RootTaleTableOfContents entries={toc} title={display.tocTitle} />\n ) : null}\n <div data-roottale-cms=\"body\" className=\"rt-cms-body\">\n <RenderTiptap doc={renderDoc} />\n </div>\n {showAuthorCard && displayAuthorName ? (\n <section className=\"rt-cms-author-card\" aria-label=\"작성자\">\n {authorImageUrlForCard ? (\n <img\n className=\"rt-cms-author-card__image\"\n src={authorImageUrlForCard}\n alt=\"\"\n loading=\"lazy\"\n />\n ) : (\n <div className=\"rt-cms-author-card__avatar\" aria-hidden=\"true\">\n {authorInitials(displayAuthorName)}\n </div>\n )}\n <div className=\"rt-cms-author-card__body\">\n <p className=\"rt-cms-author-card__eyebrow\">\n {display.authorCardEyebrow}\n </p>\n <p className=\"rt-cms-author-card__name\">{displayAuthorName}</p>\n {authorBio ? (\n <p className=\"rt-cms-author-card__description\">\n {authorBio}\n </p>\n ) : null}\n </div>\n </section>\n ) : null}\n </article>\n );\n}\n\nfunction authorInitials(name: string): string {\n const trimmed = name.trim();\n if (!trimmed) return \"\";\n const parts = trimmed.split(/\\s+/).filter(Boolean);\n if (parts.length >= 2) {\n return `${parts[0]?.[0] ?? \"\"}${parts[1]?.[0] ?? \"\"}`.toUpperCase();\n }\n return [...trimmed].slice(0, 2).join(\"\").toUpperCase();\n}\n\n/**\n * theme prop 의 3가지 입력값 (object / null / undefined) 을 React inline style\n * 로 변환. `null` 또는 빈 객체는 style 자체를 생략 → 부모에서 이미 변수를\n * 설정했다면 자식이 덮어쓰지 않음.\n */\nasync function resolveThemeStyle(input: {\n themeProp: RootTaleThemeInput;\n apiKey: string;\n baseUrl?: string;\n}): Promise<CSSProperties | null> {\n const { themeProp, apiKey, baseUrl } = input;\n if (themeProp === null) return null;\n if (themeProp !== undefined) return themeToCssVars(themeProp);\n // undefined → auto-fetch. theme fetch 실패는 비치명적 — 빈 토큰으로 fallback.\n try {\n const fetched = await fetchTheme({ apiKey, baseUrl });\n return themeToCssVars(fetched);\n } catch {\n return null;\n }\n}\n\nfunction themeToCssVars(theme: RootTaleTheme | null): CSSProperties | null {\n if (!theme) return null;\n const vars: Record<string, string> = {};\n const c = theme.colors ?? {};\n if (c.primary) vars[\"--rt-color-primary\"] = c.primary;\n if (c.primaryForeground) vars[\"--rt-color-primary-foreground\"] = c.primaryForeground;\n if (c.foreground) vars[\"--rt-color-foreground\"] = c.foreground;\n if (c.background) vars[\"--rt-color-background\"] = c.background;\n if (c.muted) vars[\"--rt-color-muted\"] = c.muted;\n if (c.mutedForeground) vars[\"--rt-color-muted-foreground\"] = c.mutedForeground;\n if (c.border) vars[\"--rt-color-border\"] = c.border;\n const f = theme.fonts ?? {};\n if (f.body) vars[\"--rt-font-body\"] = f.body;\n if (f.display) vars[\"--rt-font-display\"] = f.display;\n const r = theme.radius ?? {};\n if (r.md) vars[\"--rt-radius-md\"] = r.md;\n if (Object.keys(vars).length === 0) return null;\n return vars as CSSProperties;\n}\n\nfunction defaultPostHref(post: CmsPostContent): string {\n return `/blog/${post.slug}`;\n}\n\nfunction formatPublishedDate(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n return new Intl.DateTimeFormat(\"ko-KR\", {\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n }).format(d);\n}\n\n/**\n * Minimal Tiptap doc renderer. Walks the Tiptap doc tree and emits semantic\n * HTML. This is intentionally tiny — richer extensions (image asset URL\n * resolution, TableOfContents, FloatingCta) belong in followup §5.7 #5.\n *\n * Browser-side sanitization is unnecessary here because output is rendered\n * server-side from data the customer-trusted API returned. If the customer\n * site federates with untrusted authors later, layer a sanitizer in front\n * of this component.\n */\nfunction RenderTiptap({ doc }: { doc: Record<string, unknown> }): ReactElement {\n const rawContent = (doc as TiptapDoc).content;\n const content: TiptapNode[] = Array.isArray(rawContent) ? rawContent : [];\n return <>{content.map((node, i) => renderNode(node, i))}</>;\n}\n\ntype TiptapDoc = { type?: string; content?: TiptapNode[] };\n\ntype TiptapMark = { type?: string; attrs?: Record<string, unknown> };\n\ntype TiptapNode = {\n type?: string;\n text?: string;\n attrs?: Record<string, unknown>;\n content?: TiptapNode[];\n marks?: TiptapMark[];\n};\n\n/**\n * Defense-in-depth: even though admin LinkButton.normalizeUrl filters input,\n * legacy imports / direct DB / external tenants can still emit unsafe href.\n * Allow only http(s)/mailto/tel/root-relative/fragment/query — falls back to \"#\".\n */\nfunction isSafeHref(value: unknown): value is string {\n if (typeof value !== \"string\") return false;\n const v = value.trim();\n if (v.length === 0) return false;\n if (v.startsWith(\"/\") || v.startsWith(\"#\") || v.startsWith(\"?\")) return true;\n return /^(https?:|mailto:|tel:)/i.test(v);\n}\n\nfunction alignStyle(node: TiptapNode): CSSProperties | undefined {\n const align = node.attrs?.textAlign;\n if (typeof align !== \"string\" || align === \"\" || align === \"left\") return undefined;\n if (align === \"center\" || align === \"right\" || align === \"justify\") {\n return { textAlign: align };\n }\n return undefined;\n}\n\nfunction renderNode(node: TiptapNode, key: number): ReactElement | string | null {\n if (!node || typeof node !== \"object\") return null;\n const children = Array.isArray(node.content)\n ? node.content.map((child, i) => renderNode(child, i))\n : undefined;\n\n switch (node.type) {\n case \"paragraph\":\n return <p key={key} style={alignStyle(node)}>{children}</p>;\n case \"heading\": {\n const rawLevel = Number(node.attrs?.level ?? 2);\n const level = Math.min(Math.max(Number.isFinite(rawLevel) ? rawLevel : 2, 2), 4);\n const idAttr = node.attrs?.id;\n const id = typeof idAttr === \"string\" && idAttr.length > 0 ? idAttr : undefined;\n const style = alignStyle(node);\n if (level === 2) return <h2 key={key} id={id} style={style}>{children}</h2>;\n if (level === 3) return <h3 key={key} id={id} style={style}>{children}</h3>;\n return <h4 key={key} id={id} style={style}>{children}</h4>;\n }\n case \"bulletList\":\n return <ul key={key}>{children}</ul>;\n case \"orderedList\":\n return <ol key={key}>{children}</ol>;\n case \"listItem\":\n return <li key={key}>{children}</li>;\n case \"blockquote\":\n return <blockquote key={key}>{children}</blockquote>;\n case \"codeBlock\":\n return (\n <pre key={key}>\n <code>{children}</code>\n </pre>\n );\n case \"horizontalRule\":\n return <hr key={key} />;\n case \"hardBreak\":\n return <br key={key} />;\n case \"image\": {\n const src = node.attrs?.src;\n if (typeof src !== \"string\" || src.length === 0) return null;\n const alt = typeof node.attrs?.alt === \"string\" ? node.attrs.alt : \"\";\n const title = typeof node.attrs?.title === \"string\" ? node.attrs.title : undefined;\n // eslint-disable-next-line @next/next/no-img-element\n return <img key={key} src={src} alt={alt} title={title} loading=\"lazy\" />;\n }\n case \"columns\":\n return <div key={key} className=\"rt-columns\">{children}</div>;\n case \"column\":\n return <div key={key} className=\"rt-column\">{children}</div>;\n case \"embed\":\n return renderEmbed(node, key);\n case \"text\":\n return renderText(node, key);\n default:\n return children ? <span key={key}>{children}</span> : null;\n }\n}\n\n/**\n * 영상 임베드 노드 렌더. `isAllowedEmbedSrc` 로 allowlist host(https) 만 iframe\n * 출력 — legacy/import/외부 테넌트가 남긴 임의 src 는 원본 링크로 fallback.\n */\nfunction renderEmbed(node: TiptapNode, key: number): ReactElement | null {\n const src = node.attrs?.src;\n if (isAllowedEmbedSrc(src)) {\n const provider =\n typeof node.attrs?.provider === \"string\" ? node.attrs.provider : undefined;\n return (\n <div\n key={key}\n className=\"rt-embed\"\n data-provider={provider}\n style={{ aspectRatio: \"16 / 9\" }}\n >\n <iframe\n src={src}\n title=\"임베드된 영상\"\n loading=\"lazy\"\n allowFullScreen\n referrerPolicy=\"strict-origin-when-cross-origin\"\n allow=\"accelerometer; clipboard-write; encrypted-media; picture-in-picture; web-share\"\n style={{ width: \"100%\", height: \"100%\", border: 0 }}\n />\n </div>\n );\n }\n const orig = node.attrs?.originalUrl;\n if (isSafeHref(orig)) {\n return (\n <p key={key} className=\"rt-embed-fallback\">\n <a href={orig} rel=\"noopener noreferrer\" target=\"_blank\">\n {orig}\n </a>\n </p>\n );\n }\n return null;\n}\n\nfunction renderText(node: TiptapNode, key: number): ReactElement | string {\n const text = node.text ?? \"\";\n const marks = node.marks ?? [];\n if (marks.length === 0) return text;\n let element: ReactElement | string = text;\n for (const mark of marks) {\n element = applyMark(mark, element);\n }\n return <span key={key}>{element}</span>;\n}\n\nfunction applyMark(mark: TiptapMark, child: ReactElement | string): ReactElement {\n switch (mark.type) {\n case \"bold\":\n return <strong>{child}</strong>;\n case \"italic\":\n return <em>{child}</em>;\n case \"underline\":\n return <u>{child}</u>;\n case \"strike\":\n return <s>{child}</s>;\n case \"code\":\n return <code>{child}</code>;\n case \"highlight\": {\n const color = mark.attrs?.color;\n const style =\n typeof color === \"string\" && color.length > 0\n ? { background: color }\n : undefined;\n return <mark style={style}>{child}</mark>;\n }\n case \"textStyle\": {\n const color = mark.attrs?.color;\n const style =\n typeof color === \"string\" && color.length > 0 ? { color } : undefined;\n if (!style) return <>{child}</>;\n return <span style={style}>{child}</span>;\n }\n case \"link\": {\n const raw = mark.attrs?.href;\n const href = isSafeHref(raw) ? raw : \"#\";\n return (\n <a href={href} rel=\"noopener noreferrer\" target=\"_blank\">\n {child}\n </a>\n );\n }\n default:\n return <span>{child}</span>;\n }\n}\n","/**\n * `<RootTalePostCard>` — server-only summary card for a single CmsPostContent.\n *\n * Building block for `<RootTaleBlogList>` (and customer-side custom grids).\n * Pure SSR — featured image + category + title + meta + excerpt. Featured\n * image uses `CmsPostContent.featuredImageUrl` (CF Images delivery URL exposed\n * since cms-client 0.12); category is the first `terms` entry whose taxonomy is\n * `category`. Both render only when present, so text-only posts are unchanged.\n */\n\nimport type { ReactElement } from \"react\";\nimport type { CmsPostContent, CmsTermRef } from \"@roottale/cms-client/server\";\n\ntype CmsPostWithAuthor = CmsPostContent & { authorName?: string | null };\n\nexport interface RootTalePostCardProps {\n post: CmsPostContent;\n /** Customer-side URL builder. Defaults to `/blog/${slug}`. */\n href?: (post: CmsPostContent) => string;\n /**\n * 표시 옵션 (플랫폼 blog-settings + 글별 override resolve 결과). 미지정 시\n * 발행일만 표시(작성자 없음) — 직접 사용하는 커스텀 그리드 하위호환.\n * `showImage`/`showCategory` 는 데이터가 있을 때 노출 여부(기본 노출).\n */\n display?: {\n showDate: boolean;\n showAuthor: boolean;\n showImage?: boolean;\n showCategory?: boolean;\n };\n}\n\nfunction firstCategoryName(terms: CmsTermRef[] | undefined | null): string | null {\n if (!terms) return null;\n const cat = terms.find((t) => t.taxonomy === \"category\");\n return cat ? cat.name : null;\n}\n\nexport function RootTalePostCard(props: RootTalePostCardProps): ReactElement {\n const { post, href = defaultHref, display } = props;\n const showDate = display ? display.showDate : true;\n const showAuthor = display ? display.showAuthor : false;\n const showImage = display?.showImage !== false;\n const showCategory = display?.showCategory !== false;\n const authorName = (post as CmsPostWithAuthor).authorName;\n const publishedDate = showDate ? formatPublishedDate(post.publishedAt) : \"\";\n const imageUrl = showImage ? post.featuredImageUrl : null;\n const category = showCategory ? firstCategoryName(post.terms) : null;\n return (\n <article data-roottale-cms=\"card\" className=\"rt-cms-card\">\n <a className=\"rt-cms-card-link\" href={href(post)}>\n {imageUrl ? (\n // eslint-disable-next-line @next/next/no-img-element -- SSR string URL, framework-agnostic\n <img\n className=\"rt-cms-card-image\"\n src={imageUrl}\n alt=\"\"\n loading=\"lazy\"\n decoding=\"async\"\n />\n ) : null}\n {category ? (\n <span className=\"rt-cms-card-category\">{category}</span>\n ) : null}\n {publishedDate || (showAuthor && authorName) ? (\n <p className=\"rt-cms-meta\">\n {publishedDate ? (\n <span className=\"rt-cms-meta-item\" data-rt-cms-meta=\"date\">\n {publishedDate}\n </span>\n ) : null}\n {showAuthor && authorName ? (\n <span className=\"rt-cms-meta-item\" data-rt-cms-meta=\"author\">\n 작성자 {authorName}\n </span>\n ) : null}\n </p>\n ) : null}\n <h2 className=\"rt-cms-title\">{post.title}</h2>\n {post.excerpt ? <p className=\"rt-cms-excerpt\">{post.excerpt}</p> : null}\n <span className=\"rt-cms-card-action\" aria-hidden=\"true\">\n 읽기\n </span>\n </a>\n </article>\n );\n}\n\nfunction defaultHref(post: CmsPostContent): string {\n return `/blog/${post.slug}`;\n}\n\nfunction formatPublishedDate(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n return new Intl.DateTimeFormat(\"ko-KR\", {\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n }).format(d);\n}\n","/**\n * `<RootTaleTableOfContents>` — server-only anchor list for blog posts.\n *\n * Pure SSR. Renders `<nav>` + ordered anchors that link to `#<id>` slots\n * emitted by `attachHeadingIds`. No scroll-spy here — that requires an\n * `IntersectionObserver` which only exists in the browser; a client-island\n * variant can layer on top later without changing the data contract.\n *\n * Internal ancestor: `roottale-internal/packages/cms-renderer/src/TableOfContents.tsx`\n * (had `'use client'` + IntersectionObserver). Stripped to server-safe form per\n * the platform renderer contract (no `'use client'` boundary — ADR-0023 §5.3).\n */\n\nimport type { ReactElement } from \"react\";\nimport type { TocEntry } from \"./toc.js\";\n\nexport interface RootTaleTableOfContentsProps {\n entries: TocEntry[];\n title?: string;\n}\n\nexport function RootTaleTableOfContents(\n props: RootTaleTableOfContentsProps,\n): ReactElement | null {\n const { entries, title = \"목차\" } = props;\n if (entries.length === 0) return null;\n return (\n <nav data-roottale-cms=\"toc\" className=\"rt-cms-toc\" aria-label={title}>\n <details className=\"rt-cms-toc-details\" open>\n <summary className=\"rt-cms-toc-summary\">\n {title ? <span className=\"rt-cms-toc-title\">{title}</span> : null}\n <span className=\"rt-cms-toc-toggle\" aria-hidden=\"true\" />\n </summary>\n <ol className=\"rt-cms-toc-list\">\n {entries.map((entry) => (\n <li\n key={entry.id}\n className={`rt-cms-toc-item rt-cms-toc-level-${entry.level}`}\n >\n <a href={`#${entry.id}`}>{entry.text}</a>\n </li>\n ))}\n </ol>\n </details>\n </nav>\n );\n}\n","/**\n * `RootTaleBlogList` 카테고리 필터 — 순수 헬퍼(서버 필터, client JS 불요).\n *\n * SSR-only 정책 유지: 필터는 `?category=<slug>` URL 파라미터로 동작하고(칩 =\n * 서버 렌더 `<a>` 링크), consumer 가 searchParams 의 category 를 `activeCategory`\n * 로 주입한다. hydration 없이 SEO 친화 + 공유 가능한 URL.\n */\n\nimport type { CmsPostContent, CmsTermRef } from \"@roottale/cms-client/server\";\n\nexport interface BlogCategory {\n name: string;\n slug: string;\n}\n\nfunction categoryTerms(post: CmsPostContent): CmsTermRef[] {\n return (post.terms ?? []).filter((t) => t.taxonomy === \"category\");\n}\n\n/**\n * 글 목록에 등장하는 카테고리를 slug 기준 unique 로 수집. 서버가 보내는 정렬\n * (category → name asc)을 보존하기 위해 처음 등장 순서를 유지한다.\n */\nexport function collectCategories(posts: CmsPostContent[]): BlogCategory[] {\n const seen = new Set<string>();\n const out: BlogCategory[] = [];\n for (const post of posts) {\n for (const t of categoryTerms(post)) {\n if (!seen.has(t.slug)) {\n seen.add(t.slug);\n out.push({ name: t.name, slug: t.slug });\n }\n }\n }\n return out;\n}\n\n/** activeSlug 가 비면 전체 반환. 아니면 그 카테고리를 가진 글만. */\nexport function filterPostsByCategory(\n posts: CmsPostContent[],\n activeSlug: string | null | undefined,\n): CmsPostContent[] {\n if (!activeSlug) return posts;\n return posts.filter((p) => categoryTerms(p).some((t) => t.slug === activeSlug));\n}\n\n/** 기본 칩 링크: 전체 → `?`, 특정 → `?category=<slug>`. */\nexport function defaultCategoryHref(slug: string | null): string {\n return slug ? `?category=${encodeURIComponent(slug)}` : \"?\";\n}\n","/**\n * `@roottale/cms-renderer/toc` — Tiptap doc → TOC entries + heading id injection.\n *\n * Server-only utility. Walks a Tiptap JSON doc, extracts H1-H4 headings, and\n * derives stable kebab-case ids (Korean-safe). The companion `attachHeadingIds`\n * mutates a shallow clone of the doc so renderers can emit heading anchors\n * that match what `<RootTaleTableOfContents>` links to.\n *\n * Internal ancestor: `roottale-internal/packages/cms-renderer/src/toc.ts`\n * (HTML-string based, vendored snapshot). Ported to Tiptap-JSON because the\n * platform renderer no longer round-trips through HTML before render.\n */\n\nexport interface TocEntry {\n level: 2 | 3 | 4;\n text: string;\n id: string;\n}\n\ntype TiptapNode = {\n type?: string;\n text?: string;\n attrs?: Record<string, unknown>;\n content?: TiptapNode[];\n marks?: { type?: string }[];\n};\n\ntype TiptapDoc = { type?: string; content?: TiptapNode[] };\n\nfunction tocLevel(rawLevel: number): TocEntry[\"level\"] | null {\n if (rawLevel < 1 || rawLevel > 4) return null;\n if (rawLevel < 3) return 2;\n return rawLevel === 3 ? 3 : 4;\n}\n\nexport function headingToId(text: string): string {\n return text\n .trim()\n .toLowerCase()\n .replace(/[^\\p{L}\\p{N}\\s-]/gu, \"\")\n .replace(/\\s+/g, \"-\")\n .replace(/-+/g, \"-\")\n .replace(/^-|-$/g, \"\");\n}\n\nfunction textOf(node: TiptapNode): string {\n if (typeof node.text === \"string\") return node.text;\n if (!Array.isArray(node.content)) return \"\";\n return node.content.map(textOf).join(\"\");\n}\n\nexport function extractToc(doc: Record<string, unknown>): TocEntry[] {\n const root = doc as TiptapDoc;\n const content = Array.isArray(root.content) ? root.content : [];\n const entries: TocEntry[] = [];\n const idCounts = new Map<string, number>();\n\n for (const node of content) {\n if (node?.type !== \"heading\") continue;\n const rawLevel = Number(node.attrs?.level ?? 2);\n const level = tocLevel(rawLevel);\n if (level === null) continue;\n const text = textOf(node).trim();\n if (!text) continue;\n\n const base = headingToId(text) || \"heading\";\n const count = idCounts.get(base) ?? 0;\n const id = count === 0 ? base : `${base}-${count}`;\n idCounts.set(base, count + 1);\n\n entries.push({ level, text, id });\n }\n\n return entries;\n}\n\n/**\n * Returns a shallow-cloned Tiptap doc where each H1-H4 carries the matching\n * `attrs.id` from `entries` (same order as `extractToc` returns). Existing\n * `id` attrs are overwritten so renderer + TOC always agree on anchors.\n */\nexport function attachHeadingIds(\n doc: Record<string, unknown>,\n entries: TocEntry[],\n): Record<string, unknown> {\n const root = doc as TiptapDoc;\n const content = Array.isArray(root.content) ? root.content : [];\n let cursor = 0;\n const nextContent = content.map((node) => {\n if (node?.type !== \"heading\") return node;\n const rawLevel = Number(node.attrs?.level ?? 2);\n if (rawLevel < 1 || rawLevel > 4) return node;\n if (!textOf(node).trim()) return node;\n const entry = entries[cursor++];\n if (!entry) return node;\n return {\n ...node,\n attrs: { ...(node.attrs ?? {}), id: entry.id },\n };\n });\n return { ...(root as Record<string, unknown>), content: nextContent };\n}\n","/**\n * `<RootTaleFloatingCta>` — fixed-position call-to-action stack.\n *\n * Server-only (anchors only, no state). External targets (`kakao` / `custom`)\n * open in a new tab with `rel=\"noopener noreferrer\"`; tel/contact stay in the\n * same tab so iOS/Android can trigger the dialer / mailto handler.\n *\n * Internal ancestor: `roottale-internal/packages/cms-renderer/src/FloatingCta.tsx`.\n */\n\nimport type { ReactElement } from \"react\";\n\nexport type CtaButtonType = \"phone\" | \"contact\" | \"kakao\" | \"custom\";\n\nexport interface CtaButton {\n type: CtaButtonType;\n label: string;\n href: string;\n /** Optional override icon (emoji or short text). */\n icon?: string;\n}\n\nexport type FloatingCtaPosition =\n | \"bottom-right\"\n | \"bottom-left\"\n | \"bottom-center\";\n\nexport interface RootTaleFloatingCtaProps {\n buttons: CtaButton[];\n position?: FloatingCtaPosition;\n}\n\nexport function RootTaleFloatingCta(\n props: RootTaleFloatingCtaProps,\n): ReactElement | null {\n const { buttons, position = \"bottom-right\" } = props;\n if (!buttons || buttons.length === 0) return null;\n return (\n <div\n data-roottale-cms=\"floating-cta\"\n className={`rt-cms-floating-cta rt-cms-floating-cta--${position}`}\n >\n {buttons.map((btn, idx) => {\n const isExternal = btn.type === \"kakao\" || btn.type === \"custom\";\n return (\n <a\n key={`${btn.type}-${idx}`}\n href={btn.href}\n className={`rt-cms-floating-cta__btn rt-cms-floating-cta__btn--${btn.type}`}\n target={isExternal ? \"_blank\" : undefined}\n rel={isExternal ? \"noopener noreferrer\" : undefined}\n >\n <span aria-hidden=\"true\" className=\"rt-cms-floating-cta__icon\">\n {btn.icon ?? defaultIcon(btn.type)}\n </span>\n <span className=\"rt-cms-floating-cta__label\">{btn.label}</span>\n </a>\n );\n })}\n </div>\n );\n}\n\nfunction defaultIcon(type: CtaButtonType): string {\n switch (type) {\n case \"phone\":\n return \"☎\";\n case \"contact\":\n return \"✉\";\n case \"kakao\":\n return \"💬\";\n case \"custom\":\n return \"→\";\n }\n}\n","/**\n * `@roottale/cms-renderer-next` — RootTaleLeadForm RSC.\n *\n * 외부 고객 사이트(roottale.com, 고객사 외주)의 진단 신청 폼.\n *\n * 권장 연동(API key 하나): `action` 을 *사이트 자체* 의 Route Handler / 폼\n * action 으로 두고, 그 서버 코드가 `@roottale/cms-client` 의 `submitInquiry()` 로\n * 블로그 조회와 동일한 `ROOTTALE_API_KEY` 를 실어 intake API 로 forward 한다. 키가\n * tenant 를 식별 → 리드가 그 tenant 의 admin /crm 으로 라우팅, `tenant_slug` 불필요.\n * (API key 는 브라우저로 노출 금지 — 직접 api.roottale.com 으로 POST 하지 말 것.)\n *\n * PII 는 server 측에서 암호화 후 `inquiries` insert, 302 `?ok=1` / `?err=*` redirect.\n *\n * vertical prop:\n * - 미지정 = \"분야 선택\" select 노출 (consulting/medical/tax/legal)\n * - 지정 = hidden input + 표시 라벨 + vertical 별 추가 필드\n * (medical = 국외이전 동의 — ADR-0018)\n *\n * redirectPath prop:\n * - 외부 사이트가 폼 제출 후 돌아갈 path (API 가 allowlist 검증, ?ok=1 / ?err=*)\n *\n * 0 JS RSC. customer-facing className 전체 `.rt-cms-*` prefix, scoped via\n * `[data-roottale-cms]` (cms-public.css). customer override 자유.\n */\n\nimport type { ReactElement } from \"react\";\n\nexport type LeadFormVertical = \"consulting\" | \"medical\" | \"tax\" | \"legal\";\n\nexport interface RootTaleLeadFormProps {\n /**\n * 폼 제출 대상 — *사이트 자체* 의 server 엔드포인트(Route Handler / form action).\n * 그 서버 코드가 `cms-client` `submitInquiry()` 로 API key 를 실어 forward 한다.\n * 예: `/api/inquiry`. (API key 노출 위험 때문에 intake API 직접 지정 금지.)\n */\n action: string;\n /**\n * vertical 고정. 미지정 시 select 노출.\n * `medical` 일 때 국외이전 동의 체크박스 추가 (ADR-0018).\n */\n vertical?: LeadFormVertical;\n /**\n * 폼 제출 후 API가 돌려보낼 절대 URL.\n * tenant-api `LEAD_INTAKE_ALLOWED_ORIGINS` allowlist 검증. fail = fallback.\n * 미지정 시 tenant-api env의 LEAD_INTAKE_REDIRECT_BASE 사용.\n */\n redirectUrl?: string;\n heading?: string;\n description?: string;\n submitLabel?: string;\n /** 추가 클래스 (customer 디자인 hook). */\n className?: string;\n}\n\nconst VERTICAL_LABEL: Record<LeadFormVertical, string> = {\n consulting: \"컨설팅\",\n medical: \"의료\",\n tax: \"세무\",\n legal: \"법률\",\n};\n\nconst VERTICAL_OPTIONS: { value: LeadFormVertical; label: string }[] = [\n { value: \"consulting\", label: \"컨설팅 / 일반\" },\n { value: \"medical\", label: \"의료 (병의원·치과·한의원)\" },\n { value: \"tax\", label: \"세무 (세무사·회계사)\" },\n { value: \"legal\", label: \"법률 (법무법인·변호사)\" },\n];\n\nexport function RootTaleLeadForm(\n props: RootTaleLeadFormProps,\n): ReactElement {\n const {\n action,\n vertical,\n redirectUrl,\n heading,\n description,\n submitLabel = \"진단 신청\",\n className,\n } = props;\n\n const formClass = [\"rt-cms-lead-form\", className].filter(Boolean).join(\" \");\n\n return (\n <form\n method=\"post\"\n action={action}\n data-roottale-cms=\"lead-form\"\n className={formClass}\n >\n {heading ? (\n <h2 className=\"rt-cms-lead-heading\">{heading}</h2>\n ) : null}\n {description ? (\n <p className=\"rt-cms-lead-description\">{description}</p>\n ) : null}\n\n {redirectUrl ? (\n <input type=\"hidden\" name=\"_redirect_url\" value={redirectUrl} />\n ) : null}\n\n {vertical ? (\n <>\n <input type=\"hidden\" name=\"vertical\" value={vertical} />\n <p className=\"rt-cms-lead-vertical-label\">\n 분야: <strong>{VERTICAL_LABEL[vertical]}</strong>\n </p>\n </>\n ) : (\n <label className=\"rt-cms-field\">\n <span className=\"rt-cms-field-label\">\n 분야 <span aria-hidden=\"true\">*</span>\n </span>\n <select\n name=\"vertical\"\n required\n className=\"rt-cms-field-input rt-cms-field-select\"\n defaultValue=\"\"\n >\n <option value=\"\" disabled>\n 선택하세요\n </option>\n {VERTICAL_OPTIONS.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n </label>\n )}\n\n <label className=\"rt-cms-field\">\n <span className=\"rt-cms-field-label\">\n 이름 <span aria-hidden=\"true\">*</span>\n </span>\n <input\n type=\"text\"\n name=\"contact_name\"\n required\n autoComplete=\"name\"\n className=\"rt-cms-field-input\"\n />\n </label>\n\n <label className=\"rt-cms-field\">\n <span className=\"rt-cms-field-label\">\n 사업체명 <span aria-hidden=\"true\">*</span>\n </span>\n <input\n type=\"text\"\n name=\"business_name\"\n required\n autoComplete=\"organization\"\n className=\"rt-cms-field-input\"\n />\n </label>\n\n <label className=\"rt-cms-field\">\n <span className=\"rt-cms-field-label\">\n 이메일 <span aria-hidden=\"true\">*</span>\n </span>\n <input\n type=\"email\"\n name=\"email\"\n required\n autoComplete=\"email\"\n className=\"rt-cms-field-input\"\n />\n </label>\n\n <label className=\"rt-cms-field\">\n <span className=\"rt-cms-field-label\">\n 전화번호 <span aria-hidden=\"true\">*</span>\n </span>\n <input\n type=\"tel\"\n name=\"phone\"\n required\n autoComplete=\"tel\"\n placeholder=\"010-0000-0000\"\n className=\"rt-cms-field-input\"\n />\n </label>\n\n <label className=\"rt-cms-field\">\n <span className=\"rt-cms-field-label\">현재 사이트 URL (선택)</span>\n <input\n type=\"url\"\n name=\"current_site_url\"\n placeholder=\"https://\"\n className=\"rt-cms-field-input\"\n />\n <span className=\"rt-cms-field-hint\">없으면 비워두세요.</span>\n </label>\n\n <label className=\"rt-cms-lead-consent\">\n <input type=\"checkbox\" name=\"privacy_consent\" required />\n <span>\n <strong>(필수)</strong> 개인정보 수집·이용에 동의합니다. (수집 항목:\n 이름·연락처·이메일·사업체명. 보관 기간: 문의 종료 후 3년)\n </span>\n </label>\n\n {vertical === \"medical\" ? (\n <label className=\"rt-cms-lead-consent\">\n <input\n type=\"checkbox\"\n name=\"overseas_transfer_consent\"\n required\n />\n <span>\n <strong>(필수, 의료)</strong> 의료 PII 의 국외이전(보관·처리)에\n 동의합니다. (의료법 §21·개인정보보호법 §28 — ADR-0018)\n </span>\n </label>\n ) : null}\n\n <div className=\"rt-cms-lead-actions\">\n <button type=\"submit\" className=\"rt-cms-lead-submit\">\n {submitLabel}\n </button>\n </div>\n </form>\n );\n}\n","// ADR-0034 §1.5 amended — Block JSON → React elements\n//\n// Next/RSC public renderer 가 본 함수로 block tree 를 server-side React element\n// 로 변환. cms-renderer-astro 의 `block-to-html.ts` 와 1:1 대응 (같은 7 + α\n// block 핸들러). `BlockDefinition.nextRender` 정합.\n//\n// 알려지지 않은 block 은 rawHtml 출력 (codex v3 verdict #4, opaque atom\n// round-trip 보존).\n\nimport type { ReactElement, ReactNode } from \"react\";\n\nimport { type Block, sanitizeHtml } from \"@roottale/cms-core\";\n\nfunction isString(value: unknown): value is string {\n return typeof value === \"string\";\n}\n\nfunction htmlContent(html: string): { __html: string } {\n // dangerouslySetInnerHTML 진입 전 단일 chokepoint — 모든 block 의\n // rawHtml/content 는 여기서 sanitize 된다 (cms-core sanitizeHtml). API 키로\n // 글을 작성하는 외부 writer 가 inline `<script>`/`on*`/`javascript:` 를\n // 주입하는 저장형 XSS 방어.\n return { __html: sanitizeHtml(html) };\n}\n\nfunction renderInner(blocks: readonly Block[]): ReactElement[] {\n return blocks.map((b, i) => renderBlock(b, i));\n}\n\n/**\n * 단일 block 을 ReactElement 로 변환. `block-to-html.ts` 와 동일 분기.\n */\nexport function renderBlock(block: Block, key: number = 0): ReactElement {\n const dataBlockId = block._id;\n\n switch (block.name) {\n case \"core/paragraph\": {\n const text = isString(block.attributes.content)\n ? block.attributes.content\n : block.rawHtml ?? \"\";\n return (\n <p\n key={key}\n data-block-id={dataBlockId}\n dangerouslySetInnerHTML={htmlContent(text)}\n />\n );\n }\n case \"core/heading\": {\n const levelRaw = block.attributes.level;\n const level =\n typeof levelRaw === \"number\" && levelRaw >= 2 && levelRaw <= 4\n ? levelRaw\n : 2;\n const text = isString(block.attributes.content)\n ? block.attributes.content\n : block.rawHtml ?? \"\";\n const props = {\n \"data-block-id\": dataBlockId,\n dangerouslySetInnerHTML: htmlContent(text),\n } as const;\n if (level === 2) return <h2 key={key} {...props} />;\n if (level === 3) return <h3 key={key} {...props} />;\n return <h4 key={key} {...props} />;\n }\n case \"core/image\": {\n const src = isString(block.attributes.url) ? block.attributes.url : \"\";\n const alt = isString(block.attributes.alt) ? block.attributes.alt : \"\";\n const caption = isString(block.attributes.caption)\n ? block.attributes.caption\n : null;\n return (\n <figure key={key} data-block-id={dataBlockId}>\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={src} alt={alt} loading=\"lazy\" />\n {caption && <figcaption>{caption}</figcaption>}\n </figure>\n );\n }\n case \"core/list\": {\n const ordered = block.attributes.ordered === true;\n const items = (block.innerBlocks ?? []).map(\n (item, i): ReactNode =>\n item.rawHtml ? (\n <li\n key={i}\n dangerouslySetInnerHTML={htmlContent(item.rawHtml)}\n />\n ) : (\n <li key={i}>{renderInner(item.innerBlocks ?? [])}</li>\n ),\n );\n return ordered ? (\n <ol key={key} data-block-id={dataBlockId}>\n {items}\n </ol>\n ) : (\n <ul key={key} data-block-id={dataBlockId}>\n {items}\n </ul>\n );\n }\n case \"core/quote\": {\n const inner = renderInner(block.innerBlocks ?? []);\n const citation = isString(block.attributes.citation)\n ? block.attributes.citation\n : null;\n return (\n <blockquote key={key} data-block-id={dataBlockId}>\n {inner}\n {citation && <cite>{citation}</cite>}\n </blockquote>\n );\n }\n case \"core/code\": {\n const code = isString(block.attributes.content)\n ? block.attributes.content\n : block.rawHtml ?? \"\";\n const lang = isString(block.attributes.language)\n ? block.attributes.language\n : undefined;\n return (\n <pre\n key={key}\n data-block-id={dataBlockId}\n data-language={lang}\n >\n <code>{code}</code>\n </pre>\n );\n }\n case \"core/columns\": {\n return (\n <div\n key={key}\n className=\"rt-columns\"\n data-block-id={dataBlockId}\n >\n {renderInner(block.innerBlocks ?? [])}\n </div>\n );\n }\n case \"core/separator\":\n return <hr key={key} data-block-id={dataBlockId} />;\n case \"core/spacer\": {\n const heightRaw = block.attributes.height;\n const height = isString(heightRaw) ? heightRaw : \"32px\";\n return (\n <div\n key={key}\n className=\"rt-spacer\"\n data-block-id={dataBlockId}\n style={{ height }}\n />\n );\n }\n case \"core/group\": {\n return (\n <div\n key={key}\n className=\"rt-group\"\n data-block-id={dataBlockId}\n >\n {renderInner(block.innerBlocks ?? [])}\n </div>\n );\n }\n default: {\n // codex review §15 — 알려지지 않은 block 의 rawHtml 을 그대로\n // `dangerouslySetInnerHTML` 로 내보내면 API key 로 글을 작성하는 외부\n // 클라이언트가 임의 HTML/script 를 외부 사이트로 삽입할 수 있다 (XSS).\n // rawHtml 분기 자체를 제거 — 후속 PR 에서 cms-core 의 sanitize 모듈을\n // 도입한 뒤에만 raw 출력을 허용한다.\n const rawHtml = block.rawHtml;\n return (\n <div\n key={key}\n className=\"rt-unknown-block\"\n data-block-id={dataBlockId}\n data-block-name={block.name}\n data-rawhtml-suppressed={rawHtml ? \"true\" : undefined}\n >\n {renderInner(block.innerBlocks ?? [])}\n </div>\n );\n }\n }\n}\n\n/** Block tree 전체를 React element 배열로 직렬화. */\nexport function renderBlocks(blocks: readonly Block[]): ReactElement[] {\n return blocks.map((b, i) => renderBlock(b, i));\n}\n"],"mappings":"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","names":["jsx","jsxs","jsx","jsxs","jsx","jsxs","jsx","jsxs","Fragment","jsx","jsxs","formatPublishedDate"]}
1
+ {"version":3,"sources":["../src/server.tsx","../src/PostCard.tsx","../src/TableOfContents.tsx","../src/toc.ts","../src/FloatingCta.tsx","../src/LeadForm.tsx","../src/block-to-react.tsx"],"sourcesContent":["/**\n * `@roottale/cms-renderer-next/server` — RootTale CMS public-render React Server Components.\n *\n * Drop into any RSC-capable framework (Next.js App Router, Astro server islands,\n * React Router server). Single-line import, SSR-only, no `'use client'` boundary.\n *\n * ```tsx\n * import { RootTaleBlogList } from \"@roottale/cms-renderer-next/server\";\n *\n * export default function BlogPage() {\n * return <RootTaleBlogList apiKey={process.env.ROOTTALE_API_KEY!} />;\n * }\n * ```\n *\n * Customer site MUST keep `apiKey` server-side. Browser bundle ships zero\n * RootTale credentials (ADR-0023 §5.1 #15).\n *\n * Design tokens auto-apply via the CSS import below. Customers can override\n * any class with their own CSS or Tailwind utilities — scoped selectors use\n * `:where()` to keep specificity at zero (ADR-0023 §5.1 #10).\n */\n\nimport type { CSSProperties, ReactElement, ReactNode } from \"react\";\nimport {\n type CmsPostContent,\n type CmsPostType,\n type RootTaleTheme,\n DEFAULT_BLOG_SETTINGS,\n fetchBlogSettings,\n fetchPost,\n fetchPosts,\n fetchTheme,\n resolvePostDisplay,\n} from \"@roottale/cms-client/server\";\n// CSS는 customer 가 root layout 에서 명시적으로 import:\n// `import \"@roottale/cms-renderer-next/styles\";`\n// 본 파일에서 side-effect import 하지 않음 — tsup dts build 가 .css\n// side-effect import 처리 못 함 + customer 측 framework 마다 CSS 처리\n// 방식 다르므로 명시적 import 가 명확. README 참조.\n\nimport {\n bindingSrcValue,\n bindingTextValue,\n boundFieldValue,\n isAllowedEmbedSrc,\n} from \"@roottale/cms-core\";\n\nimport { RootTalePostCard } from \"./PostCard.js\";\nimport { RootTaleTableOfContents } from \"./TableOfContents.js\";\nimport {\n collectCategories,\n collectCategoriesWithCounts,\n defaultCategoryHref,\n filterPostsByCategory,\n} from \"./blog-filter.js\";\nimport { attachHeadingIds, extractToc } from \"./toc.js\";\n\ntype CmsPostWithAuthor = CmsPostContent & {\n authorName?: string | null;\n authorImageUrl?: string | null;\n authorBio?: string | null;\n};\n\nfunction authorImageRadiusCss(radius: \"circle\" | \"rounded\" | \"square\"): string {\n if (radius === \"circle\") return \"999px\";\n if (radius === \"rounded\") return \"0.875rem\";\n return \"0\";\n}\n\nexport { RootTalePostCard, type RootTalePostCardProps } from \"./PostCard.js\";\nexport {\n RootTaleTableOfContents,\n type RootTaleTableOfContentsProps,\n} from \"./TableOfContents.js\";\nexport {\n RootTaleFloatingCta,\n type RootTaleFloatingCtaProps,\n type CtaButton,\n type CtaButtonType,\n type FloatingCtaPosition,\n} from \"./FloatingCta.js\";\nexport {\n RootTaleLeadForm,\n type RootTaleLeadFormProps,\n type LeadFormVertical,\n} from \"./LeadForm.js\";\nexport { renderBlock, renderBlocks, renderBoundBlocks } from \"./block-to-react.js\";\nexport {\n collectCategories,\n collectCategoriesWithCounts,\n filterPostsByCategory,\n defaultCategoryHref,\n type BlogCategory,\n type BlogCategoryWithCount,\n} from \"./blog-filter.js\";\nexport {\n attachHeadingIds,\n extractToc,\n headingToId,\n type TocEntry,\n} from \"./toc.js\";\nexport type { RootTaleTheme } from \"@roottale/cms-client/server\";\n\n/**\n * RootTaleBlogList/Post 가 받는 theme prop 의 평가 결과:\n * - `RootTaleTheme` 객체 → 그대로 CSS 변수로 주입\n * - `null` → override 없음 (cms-public.css fallback 그대로)\n * - `undefined` → API key 로 자동 fetch (mysite.roottale.com 의 admin /design)\n */\nexport type RootTaleThemeInput = RootTaleTheme | null | undefined;\n\n/**\n * RootTale 디자인 토큰을 CSS 변수로 자식 트리에 주입한다. 외부 사이트가\n * 여러 RootTale 컴포넌트를 한 레이아웃에 묶을 때, 컴포넌트마다 같은 토큰을\n * fetch 하지 않도록 부모에서 한 번만 fetch 해서 전달하는 용도.\n *\n * ```tsx\n * const theme = await fetchTheme({ apiKey });\n * <RootTaleThemeProvider theme={theme}>\n * <RootTaleBlogList apiKey={apiKey} theme={null} />\n * <RootTaleBlogPost apiKey={apiKey} slugOrId=\"...\" theme={null} />\n * </RootTaleThemeProvider>\n * ```\n *\n * 자식 RootTale 컴포넌트가 `theme={null}` 을 받으면 자체 주입을 건너뛰고\n * 부모의 변수를 그대로 상속. 자식이 `theme` 을 지정하면 자식 변수가 우선.\n */\nexport function RootTaleThemeProvider(props: {\n theme: RootTaleTheme | null | undefined;\n children: ReactNode;\n}): ReactElement {\n const style = themeToCssVars(props.theme ?? null);\n if (!style) return <>{props.children}</>;\n return (\n <div data-roottale-cms=\"theme-root\" style={style}>\n {props.children}\n </div>\n );\n}\n\nexport type RootTaleBlogListProps = {\n apiKey: string;\n baseUrl?: string;\n limit?: number;\n /** Filter by post type (`post` or `page`). Default = `post` only (blog list 의도). */\n type?: CmsPostType;\n /** Build a customer-side URL for a single post. Defaults to `/blog/${slug}`. */\n postHref?: (post: CmsPostContent) => string;\n /** Rendered when the tenant has no published posts yet. */\n emptyMessage?: string;\n /**\n * 카테고리 필터 칩을 목록 위에 렌더. SSR-only — 칩은 `<a>` 링크이고\n * `?category=<slug>` URL 파라미터로 동작한다(client JS 불요). consumer 는\n * searchParams 의 category 를 `activeCategory` 로 주입.\n */\n showCategoryFilter?: boolean;\n /** 현재 선택 카테고리 slug. 미지정/빈 값 = 전체. searchParams 에서 주입. */\n activeCategory?: string;\n /**\n * 카테고리 칩 링크 URL 빌더. `null` = \"전체\" 칩. 기본 `?category=<slug>`\n * (전체 = `?`). 별도 라우트(`/blog/category/<slug>`)를 쓰려면 override.\n */\n categoryHref?: (slug: string | null) => string;\n /** \"전체\" 칩 라벨. 기본 \"전체\". */\n allCategoriesLabel?: string;\n /**\n * RootTale 디자인 토큰. 생략 시 admin /design 에서 저장한 값을 자동 fetch.\n * `null` 을 명시하면 자동 fetch 도 건너뛰고 CSS fallback 사용 (부모\n * `<RootTaleThemeProvider>` 가 변수를 이미 설정한 경우 유용).\n */\n theme?: RootTaleThemeInput;\n};\n\nexport async function RootTaleBlogList(\n props: RootTaleBlogListProps,\n): Promise<ReactElement> {\n const {\n apiKey,\n baseUrl,\n limit,\n type = \"post\",\n postHref = defaultPostHref,\n emptyMessage = \"아직 발행된 글이 없습니다.\",\n showCategoryFilter = false,\n activeCategory,\n categoryHref = defaultCategoryHref,\n allCategoriesLabel = \"전체\",\n theme: themeProp,\n } = props;\n\n const [page, themeStyle, blogSettings] = await Promise.all([\n fetchPosts({ apiKey, baseUrl, limit, type }),\n resolveThemeStyle({ themeProp, apiKey, baseUrl }),\n fetchBlogSettings({ apiKey, baseUrl }).catch(() => DEFAULT_BLOG_SETTINGS),\n ]);\n\n if (page.items.length === 0) {\n return (\n <div data-roottale-cms=\"list\" style={themeStyle ?? undefined}>\n <p className=\"rt-cms-empty\">{emptyMessage}</p>\n </div>\n );\n }\n\n // 필터 칩은 *전체* 글의 카테고리에서 도출(선택 카테고리로 좁혀도 칩은 고정).\n const categories = showCategoryFilter ? collectCategories(page.items) : [];\n const visiblePosts = showCategoryFilter\n ? filterPostsByCategory(page.items, activeCategory)\n : page.items;\n\n return (\n <div data-roottale-cms=\"list\" style={themeStyle ?? undefined}>\n {showCategoryFilter && categories.length > 0 ? (\n <nav className=\"rt-cms-filter\" aria-label=\"카테고리 필터\">\n <a\n className=\"rt-cms-filter-chip\"\n href={categoryHref(null)}\n data-active={!activeCategory ? \"\" : undefined}\n aria-current={!activeCategory ? \"true\" : undefined}\n >\n {allCategoriesLabel}\n </a>\n {categories.map((c) => (\n <a\n key={c.slug}\n className=\"rt-cms-filter-chip\"\n href={categoryHref(c.slug)}\n data-active={activeCategory === c.slug ? \"\" : undefined}\n aria-current={activeCategory === c.slug ? \"true\" : undefined}\n >\n {c.name}\n </a>\n ))}\n </nav>\n ) : null}\n {visiblePosts.length === 0 ? (\n <p className=\"rt-cms-empty\">{emptyMessage}</p>\n ) : (\n <ul className=\"rt-cms-list\">\n {visiblePosts.map((post) => (\n <li key={post.id} className=\"rt-cms-list-item\">\n <RootTalePostCard\n post={post}\n href={postHref}\n display={resolvePostDisplay(post, blogSettings)}\n />\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nexport type RootTaleBlogCategoriesProps = {\n apiKey: string;\n baseUrl?: string;\n limit?: number;\n /** Build a customer-side URL for a category. Defaults to `/blog/categories/<slug>`. */\n categoryHref?: (slug: string) => string;\n /** Rendered when no categories are attached to published posts yet. */\n emptyMessage?: string;\n /** See `RootTaleBlogListProps['theme']`. */\n theme?: RootTaleThemeInput;\n};\n\nexport async function RootTaleBlogCategories(\n props: RootTaleBlogCategoriesProps,\n): Promise<ReactElement> {\n const {\n apiKey,\n baseUrl,\n limit,\n categoryHref = (slug) => defaultCategoryHref(slug),\n emptyMessage = \"아직 카테고리가 없습니다.\",\n theme: themeProp,\n } = props;\n\n const [page, themeStyle] = await Promise.all([\n fetchPosts({ apiKey, baseUrl, limit, type: \"post\" }),\n resolveThemeStyle({ themeProp, apiKey, baseUrl }),\n ]);\n const categories = collectCategoriesWithCounts(page.items);\n\n return (\n <nav\n data-roottale-cms=\"categories\"\n className=\"rt-cms-categories\"\n aria-label=\"블로그 카테고리\"\n style={themeStyle ?? undefined}\n >\n {categories.length === 0 ? (\n <p className=\"rt-cms-empty\">{emptyMessage}</p>\n ) : (\n <ul className=\"rt-cms-category-list\">\n {categories.map((category) => (\n <li key={category.slug} className=\"rt-cms-category-list-item\">\n <a\n className=\"rt-cms-category-link\"\n href={categoryHref(category.slug)}\n >\n <span className=\"rt-cms-category-name\">{category.name}</span>\n <span className=\"rt-cms-category-count\">{category.count}</span>\n </a>\n </li>\n ))}\n </ul>\n )}\n </nav>\n );\n}\n\nexport type RootTaleBlogPostProps = {\n apiKey: string;\n slugOrId: string;\n baseUrl?: string;\n /** Rendered when the slug is not found or not published. */\n notFoundElement?: ReactElement;\n /**\n * When true, prepends an auto-derived `<RootTaleTableOfContents>` before\n * the article body (H2-H4 headings only). Default: false — keep markup\n * stable for customers who already control their own layout.\n */\n showTableOfContents?: boolean;\n /** Title shown above the TOC (only when `showTableOfContents`). */\n tableOfContentsTitle?: string;\n /** See `RootTaleBlogListProps['theme']`. */\n theme?: RootTaleThemeInput;\n};\n\nexport async function RootTaleBlogPost(\n props: RootTaleBlogPostProps,\n): Promise<ReactElement> {\n const {\n apiKey,\n slugOrId,\n baseUrl,\n notFoundElement,\n showTableOfContents,\n tableOfContentsTitle,\n theme: themeProp,\n } = props;\n const [post, themeStyle, blogSettings] = await Promise.all([\n fetchPost({ apiKey, slugOrId, baseUrl }),\n resolveThemeStyle({ themeProp, apiKey, baseUrl }),\n // 사이트 기본 표시 설정 — 실패해도 렌더는 진행 (안전 기본값).\n fetchBlogSettings({ apiKey, baseUrl }).catch(() => DEFAULT_BLOG_SETTINGS),\n ]);\n\n if (!post) {\n return (\n notFoundElement ?? (\n <div data-roottale-cms=\"post-missing\" style={themeStyle ?? undefined}>\n <p className=\"rt-cms-empty\">글을 찾을 수 없습니다.</p>\n </div>\n )\n );\n }\n\n // 표시 결정: prop > 글별 override(metaJson.display) > 사이트 기본값.\n const display = resolvePostDisplay(post, blogSettings, {\n showTableOfContents,\n tableOfContentsTitle,\n });\n const toc = display.showTableOfContents ? extractToc(post.bodyJson) : [];\n const renderDoc =\n toc.length > 0 ? attachHeadingIds(post.bodyJson, toc) : post.bodyJson;\n const authorName = (post as CmsPostWithAuthor).authorName;\n const authorImageUrl = (post as CmsPostWithAuthor).authorImageUrl;\n const postAuthorBio = (post as CmsPostWithAuthor).authorBio;\n const publishedDate = display.showDate\n ? formatPublishedDate(post.publishedAt)\n : \"\";\n const showTopAuthor = display.showAuthor && Boolean(authorName);\n const displayAuthorName = authorName ?? display.authorProfileName;\n const authorBio =\n postAuthorBio ?? display.authorProfileBio ?? display.authorCardDescription;\n const authorImageUrlForCard =\n authorImageUrl ?? display.authorProfileImageUrl;\n const authorImageAlt = `${displayAuthorName ?? \"\"} 프로필 사진`.trim();\n const authorImageStyle: CSSProperties = {\n borderRadius: authorImageRadiusCss(display.authorProfileImageRadius),\n objectPosition: `${display.authorProfileImagePositionX}% ${display.authorProfileImagePositionY}%`,\n };\n const showAuthorCard = display.showAuthorCard && Boolean(displayAuthorName);\n\n return (\n <article\n data-roottale-cms=\"post\"\n className=\"rt-cms-article\"\n style={themeStyle ?? undefined}\n >\n <header className=\"rt-cms-post-header\">\n {publishedDate || showTopAuthor ? (\n <p className=\"rt-cms-meta\">\n {publishedDate ? (\n <span className=\"rt-cms-meta-item\" data-rt-cms-meta=\"date\">\n {publishedDate}\n </span>\n ) : null}\n {showTopAuthor ? (\n <span className=\"rt-cms-meta-item\" data-rt-cms-meta=\"author\">\n 작성자 {authorName}\n </span>\n ) : null}\n </p>\n ) : null}\n <h1 className=\"rt-cms-title\">{post.title}</h1>\n {post.excerpt ? <p className=\"rt-cms-excerpt\">{post.excerpt}</p> : null}\n </header>\n {toc.length > 0 ? (\n <RootTaleTableOfContents entries={toc} title={display.tocTitle} />\n ) : null}\n <div data-roottale-cms=\"body\" className=\"rt-cms-body\">\n {/* ADR-0041 D3 — 블록 바인딩을 공개 API 의 포맷 필드 값으로 치환. */}\n <RenderTiptap doc={renderDoc} fields={post.fields} />\n </div>\n {showAuthorCard && displayAuthorName ? (\n <section className=\"rt-cms-author-card\" aria-label=\"작성자\">\n {authorImageUrlForCard ? (\n <img\n className=\"rt-cms-author-card__image\"\n src={authorImageUrlForCard}\n alt={authorImageAlt}\n loading=\"lazy\"\n style={authorImageStyle}\n />\n ) : (\n <div className=\"rt-cms-author-card__avatar\" aria-hidden=\"true\">\n {authorInitials(displayAuthorName)}\n </div>\n )}\n <div className=\"rt-cms-author-card__body\">\n <p className=\"rt-cms-author-card__eyebrow\">\n {display.authorCardEyebrow}\n </p>\n <p className=\"rt-cms-author-card__name\">{displayAuthorName}</p>\n {authorBio ? (\n <p className=\"rt-cms-author-card__description\">\n {authorBio}\n </p>\n ) : null}\n </div>\n </section>\n ) : null}\n </article>\n );\n}\n\nfunction authorInitials(name: string): string {\n const trimmed = name.trim();\n if (!trimmed) return \"\";\n const parts = trimmed.split(/\\s+/).filter(Boolean);\n if (parts.length >= 2) {\n return `${parts[0]?.[0] ?? \"\"}${parts[1]?.[0] ?? \"\"}`.toUpperCase();\n }\n return [...trimmed].slice(0, 2).join(\"\").toUpperCase();\n}\n\n/**\n * theme prop 의 3가지 입력값 (object / null / undefined) 을 React inline style\n * 로 변환. `null` 또는 빈 객체는 style 자체를 생략 → 부모에서 이미 변수를\n * 설정했다면 자식이 덮어쓰지 않음.\n */\nasync function resolveThemeStyle(input: {\n themeProp: RootTaleThemeInput;\n apiKey: string;\n baseUrl?: string;\n}): Promise<CSSProperties | null> {\n const { themeProp, apiKey, baseUrl } = input;\n if (themeProp === null) return null;\n if (themeProp !== undefined) return themeToCssVars(themeProp);\n // undefined → auto-fetch. theme fetch 실패는 비치명적 — 빈 토큰으로 fallback.\n try {\n const fetched = await fetchTheme({ apiKey, baseUrl });\n return themeToCssVars(fetched);\n } catch {\n return null;\n }\n}\n\nfunction themeToCssVars(theme: RootTaleTheme | null): CSSProperties | null {\n if (!theme) return null;\n const vars: Record<string, string> = {};\n const c = theme.colors ?? {};\n if (c.primary) vars[\"--rt-color-primary\"] = c.primary;\n if (c.primaryForeground) vars[\"--rt-color-primary-foreground\"] = c.primaryForeground;\n if (c.foreground) vars[\"--rt-color-foreground\"] = c.foreground;\n if (c.background) vars[\"--rt-color-background\"] = c.background;\n if (c.muted) vars[\"--rt-color-muted\"] = c.muted;\n if (c.mutedForeground) vars[\"--rt-color-muted-foreground\"] = c.mutedForeground;\n if (c.border) vars[\"--rt-color-border\"] = c.border;\n const f = theme.fonts ?? {};\n if (f.body) vars[\"--rt-font-body\"] = f.body;\n if (f.display) vars[\"--rt-font-display\"] = f.display;\n const r = theme.radius ?? {};\n if (r.md) vars[\"--rt-radius-md\"] = r.md;\n if (Object.keys(vars).length === 0) return null;\n return vars as CSSProperties;\n}\n\nfunction defaultPostHref(post: CmsPostContent): string {\n return `/blog/${post.slug}`;\n}\n\nfunction formatPublishedDate(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n return new Intl.DateTimeFormat(\"ko-KR\", {\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n }).format(d);\n}\n\n/**\n * Minimal Tiptap doc renderer. Walks the Tiptap doc tree and emits semantic\n * HTML. This is intentionally tiny — richer extensions (image asset URL\n * resolution, TableOfContents, FloatingCta) belong in followup §5.7 #5.\n *\n * Browser-side sanitization is unnecessary here because output is rendered\n * server-side from data the customer-trusted API returned. If the customer\n * site federates with untrusted authors later, layer a sanitizer in front\n * of this component.\n */\nexport function RenderTiptap({\n doc,\n fields,\n}: {\n doc: Record<string, unknown>;\n /**\n * ADR-0041 D3 — 공개 API `post.fields` (포맷된 커스텀 필드 값). 전달 시\n * 블록 바인딩(`roottale/field`)을 필드 값으로 치환 — 값 없으면 본문이\n * fallback 으로 그대로 렌더. astro `renderTiptapDoc(doc, { fields })` 동등.\n */\n fields?: Record<string, unknown> | null;\n}): ReactElement {\n const rawContent = (doc as TiptapDoc).content;\n const content: TiptapNode[] = Array.isArray(rawContent) ? rawContent : [];\n return <>{content.map((node, i) => renderNode(node, i, fields ?? null))}</>;\n}\n\ntype FieldsRecord = Record<string, unknown> | null;\n\n/** content 바인딩 해석 — 항상 plain text (D4, raw HTML 금지). */\nfunction boundContentText(\n node: TiptapNode,\n fields: FieldsRecord,\n): string | undefined {\n return bindingTextValue(\n boundFieldValue(node.attrs?.bindings, \"content\", fields),\n );\n}\n\ntype TiptapDoc = { type?: string; content?: TiptapNode[] };\n\ntype TiptapMark = { type?: string; attrs?: Record<string, unknown> };\n\ntype TiptapNode = {\n type?: string;\n text?: string;\n attrs?: Record<string, unknown>;\n content?: TiptapNode[];\n marks?: TiptapMark[];\n};\n\n/**\n * Defense-in-depth: even though admin LinkButton.normalizeUrl filters input,\n * legacy imports / direct DB / external tenants can still emit unsafe href.\n * Allow only http(s)/mailto/tel/root-relative/fragment/query — falls back to \"#\".\n */\nfunction isSafeHref(value: unknown): value is string {\n if (typeof value !== \"string\") return false;\n const v = value.trim();\n if (v.length === 0) return false;\n if (v.startsWith(\"/\") || v.startsWith(\"#\") || v.startsWith(\"?\")) return true;\n return /^(https?:|mailto:|tel:)/i.test(v);\n}\n\nfunction alignStyle(node: TiptapNode): CSSProperties | undefined {\n const align = node.attrs?.textAlign;\n if (typeof align !== \"string\" || align === \"\" || align === \"left\") return undefined;\n if (align === \"center\" || align === \"right\" || align === \"justify\") {\n return { textAlign: align };\n }\n return undefined;\n}\n\nfunction renderNode(\n node: TiptapNode,\n key: number,\n fields: FieldsRecord = null,\n): ReactElement | string | null {\n if (!node || typeof node !== \"object\") return null;\n const children = Array.isArray(node.content)\n ? node.content.map((child, i) => renderNode(child, i, fields))\n : undefined;\n\n switch (node.type) {\n case \"paragraph\": {\n const bound = boundContentText(node, fields);\n return (\n <p key={key} style={alignStyle(node)}>\n {bound ?? children}\n </p>\n );\n }\n case \"heading\": {\n const rawLevel = Number(node.attrs?.level ?? 2);\n const level = Math.min(Math.max(Number.isFinite(rawLevel) ? rawLevel : 2, 2), 4);\n const idAttr = node.attrs?.id;\n const id = typeof idAttr === \"string\" && idAttr.length > 0 ? idAttr : undefined;\n const style = alignStyle(node);\n const inner = boundContentText(node, fields) ?? children;\n if (level === 2) return <h2 key={key} id={id} style={style}>{inner}</h2>;\n if (level === 3) return <h3 key={key} id={id} style={style}>{inner}</h3>;\n return <h4 key={key} id={id} style={style}>{inner}</h4>;\n }\n case \"bulletList\":\n return <ul key={key}>{children}</ul>;\n case \"orderedList\":\n return <ol key={key}>{children}</ol>;\n case \"listItem\":\n return <li key={key}>{children}</li>;\n case \"blockquote\":\n return <blockquote key={key}>{children}</blockquote>;\n case \"codeBlock\":\n return (\n <pre key={key}>\n <code>{children}</code>\n </pre>\n );\n case \"horizontalRule\":\n return <hr key={key} />;\n case \"hardBreak\":\n return <br key={key} />;\n case \"image\": {\n // ADR-0041 D3 — src 바인딩(포맷된 MediaRef.url / https URL)이 있으면 우선.\n const boundSrc = bindingSrcValue(\n boundFieldValue(node.attrs?.bindings, \"src\", fields),\n );\n const src = boundSrc ?? node.attrs?.src;\n if (typeof src !== \"string\" || src.length === 0) return null;\n const alt = typeof node.attrs?.alt === \"string\" ? node.attrs.alt : \"\";\n const title = typeof node.attrs?.title === \"string\" ? node.attrs.title : undefined;\n return <img key={key} src={src} alt={alt} title={title} loading=\"lazy\" />;\n }\n case \"columns\":\n return <div key={key} className=\"rt-columns\">{children}</div>;\n case \"column\":\n return <div key={key} className=\"rt-column\">{children}</div>;\n case \"embed\":\n return renderEmbed(node, key);\n case \"text\":\n return renderText(node, key);\n default:\n return children ? <span key={key}>{children}</span> : null;\n }\n}\n\n/**\n * 영상 임베드 노드 렌더. `isAllowedEmbedSrc` 로 allowlist host(https) 만 iframe\n * 출력 — legacy/import/외부 테넌트가 남긴 임의 src 는 원본 링크로 fallback.\n */\nfunction renderEmbed(node: TiptapNode, key: number): ReactElement | null {\n const src = node.attrs?.src;\n if (isAllowedEmbedSrc(src)) {\n const provider =\n typeof node.attrs?.provider === \"string\" ? node.attrs.provider : undefined;\n return (\n <div\n key={key}\n className=\"rt-embed\"\n data-provider={provider}\n style={{ aspectRatio: \"16 / 9\" }}\n >\n <iframe\n src={src}\n title=\"임베드된 영상\"\n loading=\"lazy\"\n allowFullScreen\n referrerPolicy=\"strict-origin-when-cross-origin\"\n allow=\"accelerometer; clipboard-write; encrypted-media; picture-in-picture; web-share\"\n style={{ width: \"100%\", height: \"100%\", border: 0 }}\n />\n </div>\n );\n }\n const orig = node.attrs?.originalUrl;\n if (isSafeHref(orig)) {\n return (\n <p key={key} className=\"rt-embed-fallback\">\n <a href={orig} rel=\"noopener noreferrer\" target=\"_blank\">\n {orig}\n </a>\n </p>\n );\n }\n return null;\n}\n\nfunction renderText(node: TiptapNode, key: number): ReactElement | string {\n const text = node.text ?? \"\";\n const marks = node.marks ?? [];\n if (marks.length === 0) return text;\n let element: ReactElement | string = text;\n for (const mark of marks) {\n element = applyMark(mark, element);\n }\n return <span key={key}>{element}</span>;\n}\n\nfunction applyMark(mark: TiptapMark, child: ReactElement | string): ReactElement {\n switch (mark.type) {\n case \"bold\":\n return <strong>{child}</strong>;\n case \"italic\":\n return <em>{child}</em>;\n case \"underline\":\n return <u>{child}</u>;\n case \"strike\":\n return <s>{child}</s>;\n case \"code\":\n return <code>{child}</code>;\n case \"highlight\": {\n const color = mark.attrs?.color;\n const style =\n typeof color === \"string\" && color.length > 0\n ? { background: color }\n : undefined;\n return <mark style={style}>{child}</mark>;\n }\n case \"textStyle\": {\n const color = mark.attrs?.color;\n const style =\n typeof color === \"string\" && color.length > 0 ? { color } : undefined;\n if (!style) return <>{child}</>;\n return <span style={style}>{child}</span>;\n }\n case \"link\": {\n const raw = mark.attrs?.href;\n const href = isSafeHref(raw) ? raw : \"#\";\n return (\n <a href={href} rel=\"noopener noreferrer\" target=\"_blank\">\n {child}\n </a>\n );\n }\n default:\n return <span>{child}</span>;\n }\n}\n","/**\n * `<RootTalePostCard>` — server-only summary card for a single CmsPostContent.\n *\n * Building block for `<RootTaleBlogList>` (and customer-side custom grids).\n * Pure SSR — featured image + category + title + meta + excerpt. Featured\n * image uses `CmsPostContent.featuredImageUrl` (CF Images delivery URL exposed\n * since cms-client 0.12); category is the first `terms` entry whose taxonomy is\n * `category`. Both render only when present, so text-only posts are unchanged.\n */\n\nimport type { ReactElement } from \"react\";\nimport type { CmsPostContent, CmsTermRef } from \"@roottale/cms-client/server\";\n\ntype CmsPostWithAuthor = CmsPostContent & { authorName?: string | null };\n\nexport interface RootTalePostCardProps {\n post: CmsPostContent;\n /** Customer-side URL builder. Defaults to `/blog/${slug}`. */\n href?: (post: CmsPostContent) => string;\n /**\n * 표시 옵션 (플랫폼 blog-settings + 글별 override resolve 결과). 미지정 시\n * 발행일만 표시(작성자 없음) — 직접 사용하는 커스텀 그리드 하위호환.\n * `showImage`/`showCategory` 는 데이터가 있을 때 노출 여부(기본 노출).\n */\n display?: {\n showDate: boolean;\n showAuthor: boolean;\n showImage?: boolean;\n showCategory?: boolean;\n };\n}\n\nfunction firstCategoryName(terms: CmsTermRef[] | undefined | null): string | null {\n if (!terms) return null;\n const cat = terms.find((t) => t.taxonomy === \"category\");\n return cat ? cat.name : null;\n}\n\nexport function RootTalePostCard(props: RootTalePostCardProps): ReactElement {\n const { post, href = defaultHref, display } = props;\n const showDate = display ? display.showDate : true;\n const showAuthor = display ? display.showAuthor : false;\n const showImage = display?.showImage !== false;\n const showCategory = display?.showCategory !== false;\n const authorName = (post as CmsPostWithAuthor).authorName;\n const publishedDate = showDate ? formatPublishedDate(post.publishedAt) : \"\";\n const imageUrl = showImage ? post.featuredImageUrl : null;\n const category = showCategory ? firstCategoryName(post.terms) : null;\n return (\n <article data-roottale-cms=\"card\" className=\"rt-cms-card\">\n <a className=\"rt-cms-card-link\" href={href(post)}>\n {imageUrl ? (\n // eslint-disable-next-line @next/next/no-img-element -- SSR string URL, framework-agnostic\n <img\n className=\"rt-cms-card-image\"\n src={imageUrl}\n alt=\"\"\n loading=\"lazy\"\n decoding=\"async\"\n />\n ) : null}\n {category ? (\n <span className=\"rt-cms-card-category\">{category}</span>\n ) : null}\n {publishedDate || (showAuthor && authorName) ? (\n <p className=\"rt-cms-meta\">\n {publishedDate ? (\n <span className=\"rt-cms-meta-item\" data-rt-cms-meta=\"date\">\n {publishedDate}\n </span>\n ) : null}\n {showAuthor && authorName ? (\n <span className=\"rt-cms-meta-item\" data-rt-cms-meta=\"author\">\n 작성자 {authorName}\n </span>\n ) : null}\n </p>\n ) : null}\n <h2 className=\"rt-cms-title\">{post.title}</h2>\n {post.excerpt ? <p className=\"rt-cms-excerpt\">{post.excerpt}</p> : null}\n <span className=\"rt-cms-card-action\" aria-hidden=\"true\">\n 읽기\n </span>\n </a>\n </article>\n );\n}\n\nfunction defaultHref(post: CmsPostContent): string {\n return `/blog/${post.slug}`;\n}\n\nfunction formatPublishedDate(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n return new Intl.DateTimeFormat(\"ko-KR\", {\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n }).format(d);\n}\n","/**\n * `<RootTaleTableOfContents>` — server-only anchor list for blog posts.\n *\n * Pure SSR. Renders `<nav>` + ordered anchors that link to `#<id>` slots\n * emitted by `attachHeadingIds`. No scroll-spy here — that requires an\n * `IntersectionObserver` which only exists in the browser; a client-island\n * variant can layer on top later without changing the data contract.\n *\n * Internal ancestor: `roottale-internal/packages/cms-renderer/src/TableOfContents.tsx`\n * (had `'use client'` + IntersectionObserver). Stripped to server-safe form per\n * the platform renderer contract (no `'use client'` boundary — ADR-0023 §5.3).\n */\n\nimport type { ReactElement } from \"react\";\nimport type { TocEntry } from \"./toc.js\";\n\nexport interface RootTaleTableOfContentsProps {\n entries: TocEntry[];\n title?: string;\n}\n\nexport function RootTaleTableOfContents(\n props: RootTaleTableOfContentsProps,\n): ReactElement | null {\n const { entries, title = \"목차\" } = props;\n if (entries.length === 0) return null;\n return (\n <nav data-roottale-cms=\"toc\" className=\"rt-cms-toc\" aria-label={title}>\n <details className=\"rt-cms-toc-details\" open>\n <summary className=\"rt-cms-toc-summary\">\n {title ? <span className=\"rt-cms-toc-title\">{title}</span> : null}\n <span className=\"rt-cms-toc-toggle\" aria-hidden=\"true\" />\n </summary>\n <ol className=\"rt-cms-toc-list\">\n {entries.map((entry) => (\n <li\n key={entry.id}\n className={`rt-cms-toc-item rt-cms-toc-level-${entry.level}`}\n >\n <a href={`#${entry.id}`}>{entry.text}</a>\n </li>\n ))}\n </ol>\n </details>\n </nav>\n );\n}\n","/**\n * `@roottale/cms-renderer/toc` — Tiptap doc → TOC entries + heading id injection.\n *\n * Server-only utility. Walks a Tiptap JSON doc, extracts H1-H4 headings, and\n * derives stable kebab-case ids (Korean-safe). The companion `attachHeadingIds`\n * mutates a shallow clone of the doc so renderers can emit heading anchors\n * that match what `<RootTaleTableOfContents>` links to.\n *\n * Internal ancestor: `roottale-internal/packages/cms-renderer/src/toc.ts`\n * (HTML-string based, vendored snapshot). Ported to Tiptap-JSON because the\n * platform renderer no longer round-trips through HTML before render.\n */\n\nexport interface TocEntry {\n level: 2 | 3 | 4;\n text: string;\n id: string;\n}\n\ntype TiptapNode = {\n type?: string;\n text?: string;\n attrs?: Record<string, unknown>;\n content?: TiptapNode[];\n marks?: { type?: string }[];\n};\n\ntype TiptapDoc = { type?: string; content?: TiptapNode[] };\n\nfunction tocLevel(rawLevel: number): TocEntry[\"level\"] | null {\n if (rawLevel < 1 || rawLevel > 4) return null;\n if (rawLevel < 3) return 2;\n return rawLevel === 3 ? 3 : 4;\n}\n\nexport function headingToId(text: string): string {\n return text\n .trim()\n .toLowerCase()\n .replace(/[^\\p{L}\\p{N}\\s-]/gu, \"\")\n .replace(/\\s+/g, \"-\")\n .replace(/-+/g, \"-\")\n .replace(/^-|-$/g, \"\");\n}\n\nfunction textOf(node: TiptapNode): string {\n if (typeof node.text === \"string\") return node.text;\n if (!Array.isArray(node.content)) return \"\";\n return node.content.map(textOf).join(\"\");\n}\n\nexport function extractToc(doc: Record<string, unknown>): TocEntry[] {\n const root = doc as TiptapDoc;\n const content = Array.isArray(root.content) ? root.content : [];\n const entries: TocEntry[] = [];\n const idCounts = new Map<string, number>();\n\n for (const node of content) {\n if (node?.type !== \"heading\") continue;\n const rawLevel = Number(node.attrs?.level ?? 2);\n const level = tocLevel(rawLevel);\n if (level === null) continue;\n const text = textOf(node).trim();\n if (!text) continue;\n\n const base = headingToId(text) || \"heading\";\n const count = idCounts.get(base) ?? 0;\n const id = count === 0 ? base : `${base}-${count}`;\n idCounts.set(base, count + 1);\n\n entries.push({ level, text, id });\n }\n\n return entries;\n}\n\n/**\n * Returns a shallow-cloned Tiptap doc where each H1-H4 carries the matching\n * `attrs.id` from `entries` (same order as `extractToc` returns). Existing\n * `id` attrs are overwritten so renderer + TOC always agree on anchors.\n */\nexport function attachHeadingIds(\n doc: Record<string, unknown>,\n entries: TocEntry[],\n): Record<string, unknown> {\n const root = doc as TiptapDoc;\n const content = Array.isArray(root.content) ? root.content : [];\n let cursor = 0;\n const nextContent = content.map((node) => {\n if (node?.type !== \"heading\") return node;\n const rawLevel = Number(node.attrs?.level ?? 2);\n if (rawLevel < 1 || rawLevel > 4) return node;\n if (!textOf(node).trim()) return node;\n const entry = entries[cursor++];\n if (!entry) return node;\n return {\n ...node,\n attrs: { ...(node.attrs ?? {}), id: entry.id },\n };\n });\n return { ...(root as Record<string, unknown>), content: nextContent };\n}\n","/**\n * `<RootTaleFloatingCta>` — fixed-position call-to-action stack.\n *\n * Server-only (anchors only, no state). External targets (`kakao` / `custom`)\n * open in a new tab with `rel=\"noopener noreferrer\"`; tel/contact stay in the\n * same tab so iOS/Android can trigger the dialer / mailto handler.\n *\n * Internal ancestor: `roottale-internal/packages/cms-renderer/src/FloatingCta.tsx`.\n */\n\nimport type { ReactElement } from \"react\";\n\nexport type CtaButtonType = \"phone\" | \"contact\" | \"kakao\" | \"custom\";\n\nexport interface CtaButton {\n type: CtaButtonType;\n label: string;\n href: string;\n /** Optional override icon (emoji or short text). */\n icon?: string;\n}\n\nexport type FloatingCtaPosition =\n | \"bottom-right\"\n | \"bottom-left\"\n | \"bottom-center\";\n\nexport interface RootTaleFloatingCtaProps {\n buttons: CtaButton[];\n position?: FloatingCtaPosition;\n}\n\nexport function RootTaleFloatingCta(\n props: RootTaleFloatingCtaProps,\n): ReactElement | null {\n const { buttons, position = \"bottom-right\" } = props;\n if (!buttons || buttons.length === 0) return null;\n return (\n <div\n data-roottale-cms=\"floating-cta\"\n className={`rt-cms-floating-cta rt-cms-floating-cta--${position}`}\n >\n {buttons.map((btn, idx) => {\n const isExternal = btn.type === \"kakao\" || btn.type === \"custom\";\n return (\n <a\n key={`${btn.type}-${idx}`}\n href={btn.href}\n className={`rt-cms-floating-cta__btn rt-cms-floating-cta__btn--${btn.type}`}\n target={isExternal ? \"_blank\" : undefined}\n rel={isExternal ? \"noopener noreferrer\" : undefined}\n >\n <span aria-hidden=\"true\" className=\"rt-cms-floating-cta__icon\">\n {btn.icon ?? defaultIcon(btn.type)}\n </span>\n <span className=\"rt-cms-floating-cta__label\">{btn.label}</span>\n </a>\n );\n })}\n </div>\n );\n}\n\nfunction defaultIcon(type: CtaButtonType): string {\n switch (type) {\n case \"phone\":\n return \"☎\";\n case \"contact\":\n return \"✉\";\n case \"kakao\":\n return \"💬\";\n case \"custom\":\n return \"→\";\n }\n}\n","/**\n * `@roottale/cms-renderer-next` — RootTaleLeadForm RSC.\n *\n * 외부 고객 사이트(roottale.com, 고객사 외주)의 진단 신청 폼.\n *\n * 권장 연동(API key 하나): `action` 을 *사이트 자체* 의 Route Handler / 폼\n * action 으로 두고, 그 서버 코드가 `@roottale/cms-client` 의 `submitInquiry()` 로\n * 블로그 조회와 동일한 `ROOTTALE_API_KEY` 를 실어 intake API 로 forward 한다. 키가\n * tenant 를 식별 → 리드가 그 tenant 의 admin /crm 으로 라우팅, `tenant_slug` 불필요.\n * (API key 는 브라우저로 노출 금지 — 직접 api.roottale.com 으로 POST 하지 말 것.)\n *\n * PII 는 server 측에서 암호화 후 `inquiries` insert, 302 `?ok=1` / `?err=*` redirect.\n *\n * vertical prop:\n * - 미지정 = \"분야 선택\" select 노출 (consulting/medical/tax/legal)\n * - 지정 = hidden input + 표시 라벨 + vertical 별 추가 필드\n * (medical = 국외이전 동의 — ADR-0018)\n *\n * redirectPath prop:\n * - 외부 사이트가 폼 제출 후 돌아갈 path (API 가 allowlist 검증, ?ok=1 / ?err=*)\n *\n * 0 JS RSC. customer-facing className 전체 `.rt-cms-*` prefix, scoped via\n * `[data-roottale-cms]` (cms-public.css). customer override 자유.\n */\n\nimport type { ReactElement } from \"react\";\n\nexport type LeadFormVertical = \"consulting\" | \"medical\" | \"tax\" | \"legal\";\n\nexport interface RootTaleLeadFormProps {\n /**\n * 폼 제출 대상 — *사이트 자체* 의 server 엔드포인트(Route Handler / form action).\n * 그 서버 코드가 `cms-client` `submitInquiry()` 로 API key 를 실어 forward 한다.\n * 예: `/api/inquiry`. (API key 노출 위험 때문에 intake API 직접 지정 금지.)\n */\n action: string;\n /**\n * vertical 고정. 미지정 시 select 노출.\n * `medical` 일 때 국외이전 동의 체크박스 추가 (ADR-0018).\n */\n vertical?: LeadFormVertical;\n /**\n * 폼 제출 후 API가 돌려보낼 절대 URL.\n * tenant-api `LEAD_INTAKE_ALLOWED_ORIGINS` allowlist 검증. fail = fallback.\n * 미지정 시 tenant-api env의 LEAD_INTAKE_REDIRECT_BASE 사용.\n */\n redirectUrl?: string;\n heading?: string;\n description?: string;\n submitLabel?: string;\n /** 추가 클래스 (customer 디자인 hook). */\n className?: string;\n}\n\nconst VERTICAL_LABEL: Record<LeadFormVertical, string> = {\n consulting: \"컨설팅\",\n medical: \"의료\",\n tax: \"세무\",\n legal: \"법률\",\n};\n\nconst VERTICAL_OPTIONS: { value: LeadFormVertical; label: string }[] = [\n { value: \"consulting\", label: \"컨설팅 / 일반\" },\n { value: \"medical\", label: \"의료 (병의원·치과·한의원)\" },\n { value: \"tax\", label: \"세무 (세무사·회계사)\" },\n { value: \"legal\", label: \"법률 (법무법인·변호사)\" },\n];\n\nexport function RootTaleLeadForm(\n props: RootTaleLeadFormProps,\n): ReactElement {\n const {\n action,\n vertical,\n redirectUrl,\n heading,\n description,\n submitLabel = \"진단 신청\",\n className,\n } = props;\n\n const formClass = [\"rt-cms-lead-form\", className].filter(Boolean).join(\" \");\n\n return (\n <form\n method=\"post\"\n action={action}\n data-roottale-cms=\"lead-form\"\n className={formClass}\n >\n {heading ? (\n <h2 className=\"rt-cms-lead-heading\">{heading}</h2>\n ) : null}\n {description ? (\n <p className=\"rt-cms-lead-description\">{description}</p>\n ) : null}\n\n {redirectUrl ? (\n <input type=\"hidden\" name=\"_redirect_url\" value={redirectUrl} />\n ) : null}\n\n {vertical ? (\n <>\n <input type=\"hidden\" name=\"vertical\" value={vertical} />\n <p className=\"rt-cms-lead-vertical-label\">\n 분야: <strong>{VERTICAL_LABEL[vertical]}</strong>\n </p>\n </>\n ) : (\n <label className=\"rt-cms-field\">\n <span className=\"rt-cms-field-label\">\n 분야 <span aria-hidden=\"true\">*</span>\n </span>\n <select\n name=\"vertical\"\n required\n className=\"rt-cms-field-input rt-cms-field-select\"\n defaultValue=\"\"\n >\n <option value=\"\" disabled>\n 선택하세요\n </option>\n {VERTICAL_OPTIONS.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n </label>\n )}\n\n <label className=\"rt-cms-field\">\n <span className=\"rt-cms-field-label\">\n 이름 <span aria-hidden=\"true\">*</span>\n </span>\n <input\n type=\"text\"\n name=\"contact_name\"\n required\n autoComplete=\"name\"\n className=\"rt-cms-field-input\"\n />\n </label>\n\n <label className=\"rt-cms-field\">\n <span className=\"rt-cms-field-label\">\n 사업체명 <span aria-hidden=\"true\">*</span>\n </span>\n <input\n type=\"text\"\n name=\"business_name\"\n required\n autoComplete=\"organization\"\n className=\"rt-cms-field-input\"\n />\n </label>\n\n <label className=\"rt-cms-field\">\n <span className=\"rt-cms-field-label\">\n 이메일 <span aria-hidden=\"true\">*</span>\n </span>\n <input\n type=\"email\"\n name=\"email\"\n required\n autoComplete=\"email\"\n className=\"rt-cms-field-input\"\n />\n </label>\n\n <label className=\"rt-cms-field\">\n <span className=\"rt-cms-field-label\">\n 전화번호 <span aria-hidden=\"true\">*</span>\n </span>\n <input\n type=\"tel\"\n name=\"phone\"\n required\n autoComplete=\"tel\"\n inputMode=\"tel\"\n pattern={\"0\\\\d{1,2}-?\\\\d{3,4}-?\\\\d{4}\"}\n placeholder=\"010-1234-5678\"\n className=\"rt-cms-field-input\"\n />\n </label>\n\n <label className=\"rt-cms-field\">\n <span className=\"rt-cms-field-label\">현재 사이트 URL (선택)</span>\n <input\n type=\"url\"\n name=\"current_site_url\"\n placeholder=\"https://\"\n className=\"rt-cms-field-input\"\n />\n <span className=\"rt-cms-field-hint\">없으면 비워두세요.</span>\n </label>\n\n <label className=\"rt-cms-lead-consent\">\n <input type=\"checkbox\" name=\"privacy_consent\" required />\n <span>\n <strong>(필수)</strong> 개인정보 수집·이용에 동의합니다. (수집 항목:\n 이름·연락처·이메일·사업체명. 보관 기간: 문의 종료 후 3년)\n </span>\n </label>\n\n {vertical === \"medical\" ? (\n <label className=\"rt-cms-lead-consent\">\n <input\n type=\"checkbox\"\n name=\"overseas_transfer_consent\"\n required\n />\n <span>\n <strong>(필수, 의료)</strong> 의료 PII 의 국외이전(보관·처리)에\n 동의합니다. (의료법 §21·개인정보보호법 §28 — ADR-0018)\n </span>\n </label>\n ) : null}\n\n <div className=\"rt-cms-lead-actions\">\n <button type=\"submit\" className=\"rt-cms-lead-submit\">\n {submitLabel}\n </button>\n </div>\n </form>\n );\n}\n","// ADR-0034 §1.5 amended — Block JSON → React elements\n//\n// Next/RSC public renderer 가 본 함수로 block tree 를 server-side React element\n// 로 변환. cms-renderer-astro 의 `block-to-html.ts` 와 1:1 대응 (같은 7 + α\n// block 핸들러). `BlockDefinition.nextRender` 정합.\n//\n// 알려지지 않은 block 은 rawHtml 출력 (codex v3 verdict #4, opaque atom\n// round-trip 보존).\n\nimport type { ReactElement, ReactNode } from \"react\";\n\nimport {\n type Block,\n type BindingResolver,\n applyBlockBindings,\n createFieldBindingResolver,\n sanitizeHtml,\n} from \"@roottale/cms-core\";\n\nfunction isString(value: unknown): value is string {\n return typeof value === \"string\";\n}\n\nfunction htmlContent(html: string): { __html: string } {\n // dangerouslySetInnerHTML 진입 전 단일 chokepoint — 모든 block 의\n // rawHtml/content 는 여기서 sanitize 된다 (cms-core sanitizeHtml). API 키로\n // 글을 작성하는 외부 writer 가 inline `<script>`/`on*`/`javascript:` 를\n // 주입하는 저장형 XSS 방어.\n return { __html: sanitizeHtml(html) };\n}\n\nfunction renderInner(blocks: readonly Block[]): ReactElement[] {\n return blocks.map((b, i) => renderBlock(b, i));\n}\n\n/**\n * 단일 block 을 ReactElement 로 변환. `block-to-html.ts` 와 동일 분기.\n */\nexport function renderBlock(block: Block, key: number = 0): ReactElement {\n const dataBlockId = block._id;\n\n switch (block.name) {\n case \"core/paragraph\": {\n const text = isString(block.attributes.content)\n ? block.attributes.content\n : block.rawHtml ?? \"\";\n return (\n <p\n key={key}\n data-block-id={dataBlockId}\n dangerouslySetInnerHTML={htmlContent(text)}\n />\n );\n }\n case \"core/heading\": {\n const levelRaw = block.attributes.level;\n const level =\n typeof levelRaw === \"number\" && levelRaw >= 2 && levelRaw <= 4\n ? levelRaw\n : 2;\n const text = isString(block.attributes.content)\n ? block.attributes.content\n : block.rawHtml ?? \"\";\n const props = {\n \"data-block-id\": dataBlockId,\n dangerouslySetInnerHTML: htmlContent(text),\n } as const;\n if (level === 2) return <h2 key={key} {...props} />;\n if (level === 3) return <h3 key={key} {...props} />;\n return <h4 key={key} {...props} />;\n }\n case \"core/image\": {\n const src = isString(block.attributes.url) ? block.attributes.url : \"\";\n const alt = isString(block.attributes.alt) ? block.attributes.alt : \"\";\n const caption = isString(block.attributes.caption)\n ? block.attributes.caption\n : null;\n return (\n <figure key={key} data-block-id={dataBlockId}>\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={src} alt={alt} loading=\"lazy\" />\n {caption && <figcaption>{caption}</figcaption>}\n </figure>\n );\n }\n case \"core/list\": {\n const ordered = block.attributes.ordered === true;\n const items = (block.innerBlocks ?? []).map(\n (item, i): ReactNode =>\n item.rawHtml ? (\n <li\n key={i}\n dangerouslySetInnerHTML={htmlContent(item.rawHtml)}\n />\n ) : (\n <li key={i}>{renderInner(item.innerBlocks ?? [])}</li>\n ),\n );\n return ordered ? (\n <ol key={key} data-block-id={dataBlockId}>\n {items}\n </ol>\n ) : (\n <ul key={key} data-block-id={dataBlockId}>\n {items}\n </ul>\n );\n }\n case \"core/quote\": {\n const inner = renderInner(block.innerBlocks ?? []);\n const citation = isString(block.attributes.citation)\n ? block.attributes.citation\n : null;\n return (\n <blockquote key={key} data-block-id={dataBlockId}>\n {inner}\n {citation && <cite>{citation}</cite>}\n </blockquote>\n );\n }\n case \"core/code\": {\n const code = isString(block.attributes.content)\n ? block.attributes.content\n : block.rawHtml ?? \"\";\n const lang = isString(block.attributes.language)\n ? block.attributes.language\n : undefined;\n return (\n <pre\n key={key}\n data-block-id={dataBlockId}\n data-language={lang}\n >\n <code>{code}</code>\n </pre>\n );\n }\n case \"core/columns\": {\n return (\n <div\n key={key}\n className=\"rt-columns\"\n data-block-id={dataBlockId}\n >\n {renderInner(block.innerBlocks ?? [])}\n </div>\n );\n }\n case \"core/separator\":\n return <hr key={key} data-block-id={dataBlockId} />;\n case \"core/spacer\": {\n const heightRaw = block.attributes.height;\n const height = isString(heightRaw) ? heightRaw : \"32px\";\n return (\n <div\n key={key}\n className=\"rt-spacer\"\n data-block-id={dataBlockId}\n style={{ height }}\n />\n );\n }\n case \"core/group\": {\n return (\n <div\n key={key}\n className=\"rt-group\"\n data-block-id={dataBlockId}\n >\n {renderInner(block.innerBlocks ?? [])}\n </div>\n );\n }\n default: {\n // codex review §15 — 알려지지 않은 block 의 rawHtml 을 그대로\n // `dangerouslySetInnerHTML` 로 내보내면 API key 로 글을 작성하는 외부\n // 클라이언트가 임의 HTML/script 를 외부 사이트로 삽입할 수 있다 (XSS).\n // rawHtml 분기 자체를 제거 — 후속 PR 에서 cms-core 의 sanitize 모듈을\n // 도입한 뒤에만 raw 출력을 허용한다.\n const rawHtml = block.rawHtml;\n return (\n <div\n key={key}\n className=\"rt-unknown-block\"\n data-block-id={dataBlockId}\n data-block-name={block.name}\n data-rawhtml-suppressed={rawHtml ? \"true\" : undefined}\n >\n {renderInner(block.innerBlocks ?? [])}\n </div>\n );\n }\n }\n}\n\n/** Block tree 전체를 React element 배열로 직렬화. */\nexport function renderBlocks(blocks: readonly Block[]): ReactElement[] {\n return blocks.map((b, i) => renderBlock(b, i));\n}\n\n/**\n * ADR-0041 §7 — Block Bindings 를 먼저 적용한 뒤 렌더.\n * `metadata.bindings` 의 `roottale/field`/`acf/field` 를 `fields`(acf 값)로\n * 치환. 바인딩 없으면 renderBlocks 와 동일.\n */\nexport function renderBoundBlocks(\n blocks: readonly Block[],\n opts: {\n fields?: Record<string, unknown> | null;\n resolver?: BindingResolver;\n } = {},\n): ReactElement[] {\n const resolver = opts.resolver ?? createFieldBindingResolver(opts.fields);\n return renderBlocks(applyBlockBindings(blocks, resolver));\n}\n"],"mappings":"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","names":["jsx","jsxs","jsx","jsxs","jsx","jsxs","jsx","jsxs","Fragment","jsx","jsxs","formatPublishedDate"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roottale/cms-renderer-next",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "type": "module",
5
5
  "description": "RootTale CMS public-render React/Next.js Server Components. SSR-only RSC components (RootTaleBlogList / RootTaleBlogPost / RootTaleLeadForm) for external customer sites. Companion of @roottale/cms-renderer-astro (ADR-0034 §1.5 amended).",
6
6
  "main": "./dist/index.js",
@@ -35,8 +35,8 @@
35
35
  "CHANGELOG.md"
36
36
  ],
37
37
  "dependencies": {
38
- "@roottale/cms-client": "^0.16.0",
39
- "@roottale/cms-core": "^0.7.0"
38
+ "@roottale/cms-client": "^0.18.0",
39
+ "@roottale/cms-core": "^0.18.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "react": "^19.0.0"