@roottale/cms-renderer-astro 0.2.1 → 0.4.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,5 +1,62 @@
1
1
  # @roottale/cms-renderer-astro
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [31900de]
8
+ - @roottale/cms-client@0.4.0
9
+
10
+ ## 0.3.0
11
+
12
+ ### Minor Changes
13
+
14
+ - 1e738fd: Add design token auto-application for external sites.
15
+
16
+ admin `mysite.roottale.com → 디자인` 에서 저장한 색·글꼴·둥글기를 외부 사이트의
17
+ RootTale 블로그 컴포넌트가 자동으로 가져와 적용한다.
18
+
19
+ **`@roottale/cms-client`** — 신규 `fetchTheme()` + `RootTaleTheme` 타입 export.
20
+ `GET /v1/cms/public/theme` 엔드포인트 호출 (Bearer rtlk*cust*\*).
21
+
22
+ **`@roottale/cms-renderer-next`** — `<RootTaleBlogList>` / `<RootTaleBlogPost>` 가
23
+ 신규 `theme` prop 지원. 동시에 `<RootTaleThemeProvider>` 컴포넌트로 부모에서 1회
24
+ fetch 한 뒤 자식 컴포넌트가 변수를 상속하는 패턴도 지원.
25
+
26
+ ```tsx
27
+ // 자동 fetch (기본) — 별도 설정 없이 admin 토큰 적용
28
+ <RootTaleBlogList apiKey={process.env.ROOTTALE_API_KEY!} />
29
+
30
+ // 명시 override
31
+ <RootTaleBlogList
32
+ apiKey={process.env.ROOTTALE_API_KEY!}
33
+ theme={{ colors: { primary: "#0070f3" } }}
34
+ />
35
+
36
+ // 부모 1회 fetch → 자식들 상속 (다중 컴포넌트 페이지에서 호출 1회로 축소)
37
+ const theme = await fetchTheme({ apiKey });
38
+ <RootTaleThemeProvider theme={theme}>
39
+ <RootTaleBlogList apiKey={apiKey} theme={null} />
40
+ <RootTaleBlogPost apiKey={apiKey} slugOrId="..." theme={null} />
41
+ </RootTaleThemeProvider>
42
+ ```
43
+
44
+ **`@roottale/cms-renderer-astro`** — `renderBlogList` / `renderBlogPost` 에 동일
45
+ 의미의 `theme` 옵션. style="..." 속성으로 CSS 변수 주입 (서버 측 escape 보강).
46
+
47
+ `theme` prop semantics:
48
+ - `undefined` → 자동 fetch
49
+ - `RootTaleTheme` → 호출자 override
50
+ - `null` → 자동 fetch 건너뜀 (부모 `RootTaleThemeProvider` 활용 시)
51
+
52
+ fetch 실패는 비치명적 — CSS fallback 으로 graceful degrade.
53
+
54
+ ### Patch Changes
55
+
56
+ - Updated dependencies [a7444e2]
57
+ - Updated dependencies [1e738fd]
58
+ - @roottale/cms-client@0.2.0
59
+
3
60
  ## 0.2.1
4
61
 
5
62
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Block } from '@roottale/cms-core';
2
- import { CmsPostType, CmsPostContent } from '@roottale/cms-client/server';
2
+ import { CmsPostType, CmsPostContent, RootTaleTheme } from '@roottale/cms-client/server';
3
+ export { RootTaleTheme } from '@roottale/cms-client/server';
3
4
 
4
5
  /**
5
6
  * 단일 block 을 HTML 로 변환.
@@ -13,6 +14,13 @@ declare function renderBlocks(blocks: readonly Block[]): string;
13
14
  /** Tiptap doc (admin-tenant 저장 형식) 을 HTML 문자열로 변환. */
14
15
  declare function renderTiptapDoc(doc: unknown): string;
15
16
 
17
+ /**
18
+ * theme prop 평가 결과 — Next renderer 의 `RootTaleThemeInput` 과 동일 의미:
19
+ * - 객체 → 그대로 CSS 변수로 주입
20
+ * - `null` → override 없음 (cms-public.css fallback 사용)
21
+ * - 생략 → API key 로 자동 fetch (mysite.roottale.com 의 admin /design)
22
+ */
23
+ type RootTaleThemeInput = RootTaleTheme | null | undefined;
16
24
  interface RenderBlogListOptions {
17
25
  /** `rtlk_cust_*` API key (server-side only). */
18
26
  apiKey: string;
@@ -28,6 +36,11 @@ interface RenderBlogListOptions {
28
36
  postHref?: (post: CmsPostContent) => string;
29
37
  /** Rendered when the tenant has no published posts yet. */
30
38
  emptyMessage?: string;
39
+ /**
40
+ * RootTale 디자인 토큰. 생략 시 admin /design 의 저장값을 자동 fetch,
41
+ * `null` 명시 시 자동 fetch 도 건너뛰고 CSS fallback 사용.
42
+ */
43
+ theme?: RootTaleThemeInput;
31
44
  }
32
45
  declare function renderBlogList(options: RenderBlogListOptions): Promise<string>;
33
46
  interface RenderBlogPostOptions {
@@ -39,6 +52,8 @@ interface RenderBlogPostOptions {
39
52
  siteId?: string;
40
53
  /** Rendered when the slug is not found or not published. */
41
54
  notFoundHtml?: string;
55
+ /** See `RenderBlogListOptions['theme']`. */
56
+ theme?: RootTaleThemeInput;
42
57
  }
43
58
  declare function renderBlogPost(options: RenderBlogPostOptions): Promise<string>;
44
59
 
@@ -91,4 +106,4 @@ interface RobotsConfig {
91
106
  }
92
107
  declare function renderRobotsTxt(config?: RobotsConfig): string;
93
108
 
94
- export { type PostSeoInput, type RenderBlogListOptions, type RenderBlogPostOptions, type RobotsConfig, type SitemapEntry, renderBlock, renderBlocks, renderBlogList, renderBlogPost, renderRobotsTxt, renderSeoHead, renderSitemapXml, renderTiptapDoc };
109
+ export { type PostSeoInput, type RenderBlogListOptions, type RenderBlogPostOptions, type RobotsConfig, type RootTaleThemeInput, type SitemapEntry, renderBlock, renderBlocks, renderBlogList, renderBlogPost, renderRobotsTxt, renderSeoHead, renderSitemapXml, renderTiptapDoc };
package/dist/index.js CHANGED
@@ -196,7 +196,8 @@ function renderTiptapDoc(doc) {
196
196
  // src/blog.ts
197
197
  import {
198
198
  fetchPost,
199
- fetchPosts
199
+ fetchPosts,
200
+ fetchTheme
200
201
  } from "@roottale/cms-client/server";
201
202
  function escapeHtml3(text) {
202
203
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
@@ -231,28 +232,82 @@ async function renderBlogList(options) {
231
232
  siteId,
232
233
  type = "post",
233
234
  postHref = defaultPostHref,
234
- emptyMessage = "\uC544\uC9C1 \uBC1C\uD589\uB41C \uAE00\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."
235
+ emptyMessage = "\uC544\uC9C1 \uBC1C\uD589\uB41C \uAE00\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
236
+ theme: themeProp
235
237
  } = options;
236
- const page = await fetchPosts({ apiKey, baseUrl, limit, type, siteId });
238
+ const [page, themeStyleAttr] = await Promise.all([
239
+ fetchPosts({ apiKey, baseUrl, limit, type, siteId }),
240
+ resolveThemeStyleAttr({ themeProp, apiKey, baseUrl, siteId })
241
+ ]);
237
242
  if (page.items.length === 0) {
238
- return `<div data-roottale-cms="list"><p class="rt-cms-empty">${escapeHtml3(emptyMessage)}</p></div>`;
243
+ return `<div data-roottale-cms="list"${themeStyleAttr}><p class="rt-cms-empty">${escapeHtml3(emptyMessage)}</p></div>`;
239
244
  }
240
245
  const items = page.items.map(
241
246
  (post) => `<li class="rt-cms-list-item">${renderPostCard(post, postHref)}</li>`
242
247
  ).join("");
243
- return `<div data-roottale-cms="list"><ul class="rt-cms-list">${items}</ul></div>`;
248
+ return `<div data-roottale-cms="list"${themeStyleAttr}><ul class="rt-cms-list">${items}</ul></div>`;
244
249
  }
245
250
  async function renderBlogPost(options) {
246
- const { apiKey, baseUrl, slugOrId, siteId, notFoundHtml } = options;
247
- const post = await fetchPost({ apiKey, baseUrl, slugOrId, siteId });
251
+ const {
252
+ apiKey,
253
+ baseUrl,
254
+ slugOrId,
255
+ siteId,
256
+ notFoundHtml,
257
+ theme: themeProp
258
+ } = options;
259
+ const [post, themeStyleAttr] = await Promise.all([
260
+ fetchPost({ apiKey, baseUrl, slugOrId, siteId }),
261
+ resolveThemeStyleAttr({ themeProp, apiKey, baseUrl, siteId })
262
+ ]);
248
263
  if (!post) {
249
- return notFoundHtml ?? `<div data-roottale-cms="post-missing"><p class="rt-cms-empty">\uAE00\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.</p></div>`;
264
+ return notFoundHtml ?? `<div data-roottale-cms="post-missing"${themeStyleAttr}><p class="rt-cms-empty">\uAE00\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.</p></div>`;
250
265
  }
251
266
  const bodyHtml = renderTiptapDoc(post.bodyJson);
252
267
  const date = escapeHtml3(formatPublishedDate(post.publishedAt));
253
268
  const title = escapeHtml3(post.title);
254
269
  const excerpt = post.excerpt ? `<p class="rt-cms-excerpt">${escapeHtml3(post.excerpt)}</p>` : "";
255
- return `<article data-roottale-cms="post" class="rt-cms-article"><header><p class="rt-cms-meta">${date}</p><h1 class="rt-cms-title">${title}</h1>${excerpt}</header><div data-roottale-cms="body">${bodyHtml}</div></article>`;
270
+ return `<article data-roottale-cms="post" class="rt-cms-article"${themeStyleAttr}><header><p class="rt-cms-meta">${date}</p><h1 class="rt-cms-title">${title}</h1>${excerpt}</header><div data-roottale-cms="body">${bodyHtml}</div></article>`;
271
+ }
272
+ async function resolveThemeStyleAttr(input) {
273
+ const { themeProp, apiKey, baseUrl, siteId } = input;
274
+ let theme;
275
+ if (themeProp === null) {
276
+ theme = null;
277
+ } else if (themeProp !== void 0) {
278
+ theme = themeProp;
279
+ } else {
280
+ try {
281
+ theme = await fetchTheme({ apiKey, baseUrl, siteId });
282
+ } catch {
283
+ theme = null;
284
+ }
285
+ }
286
+ return renderThemeStyleAttr(theme);
287
+ }
288
+ function renderThemeStyleAttr(theme) {
289
+ if (!theme) return "";
290
+ const decls = [];
291
+ const push = (key, value) => {
292
+ if (!value) return;
293
+ if (/[;<>\r\n"]/.test(value)) return;
294
+ decls.push(`${key}: ${value}`);
295
+ };
296
+ const c = theme.colors ?? {};
297
+ push("--rt-color-primary", c.primary);
298
+ push("--rt-color-primary-foreground", c.primaryForeground);
299
+ push("--rt-color-foreground", c.foreground);
300
+ push("--rt-color-background", c.background);
301
+ push("--rt-color-muted", c.muted);
302
+ push("--rt-color-muted-foreground", c.mutedForeground);
303
+ push("--rt-color-border", c.border);
304
+ const f = theme.fonts ?? {};
305
+ push("--rt-font-body", f.body);
306
+ push("--rt-font-display", f.display);
307
+ const r = theme.radius ?? {};
308
+ push("--rt-radius-md", r.md);
309
+ if (decls.length === 0) return "";
310
+ return ` style="${decls.join("; ")}"`;
256
311
  }
257
312
 
258
313
  // src/seo.ts
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/block-to-html.ts","../src/tiptap-to-html.ts","../src/blog.ts","../src/seo.ts","../src/sitemap.ts","../src/robots.ts"],"sourcesContent":["// ADR-0034 §1.5 + §5 — block JSON → HTML\n//\n// Astro 가 public page 렌더링 시 본 함수로 block tree 를 server-side HTML 로 변환.\n// 7 block (paragraph/heading/image/list/quote/code/columns) Phase 1 핸들러.\n// 우리가 schema 정의 안 한 block 은 rawHtml 출력 (codex v3 verdict #4, opaque\n// atom round-trip 보존).\n\nimport type { Block } from \"@roottale/cms-core\";\n\n/** XSS 차단: text content escape */\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\n}\n\n/** attribute value escape (XSS) */\nfunction escapeAttr(value: string): string {\n return value.replace(/\"/g, \"&quot;\").replace(/</g, \"&lt;\");\n}\n\nfunction renderInner(blocks: readonly Block[]): string {\n return blocks.map((b) => renderBlock(b)).join(\"\");\n}\n\n/**\n * 단일 block 을 HTML 로 변환.\n * 알려진 `core/<x>` block name 은 dedicated handler. 그 외 = rawHtml 출력\n * (Tiptap opaque atom node 보존).\n */\nexport function renderBlock(block: Block): string {\n switch (block.name) {\n case \"core/paragraph\": {\n const text = typeof block.attributes.content === \"string\"\n ? block.attributes.content\n : (block.rawHtml ?? \"\");\n return `<p data-block-id=\"${escapeAttr(block._id)}\">${text}</p>`;\n }\n case \"core/heading\": {\n const levelRaw = block.attributes.level;\n const level = typeof levelRaw === \"number\" && levelRaw >= 1 && levelRaw <= 6 ? levelRaw : 2;\n const text = typeof block.attributes.content === \"string\"\n ? block.attributes.content\n : (block.rawHtml ?? \"\");\n return `<h${level} data-block-id=\"${escapeAttr(block._id)}\">${text}</h${level}>`;\n }\n case \"core/image\": {\n const src = typeof block.attributes.url === \"string\"\n ? escapeAttr(block.attributes.url)\n : \"\";\n const alt = typeof block.attributes.alt === \"string\"\n ? escapeAttr(block.attributes.alt)\n : \"\";\n const caption = typeof block.attributes.caption === \"string\"\n ? block.attributes.caption\n : null;\n const img = `<img src=\"${src}\" alt=\"${alt}\" loading=\"lazy\">`;\n const inner = caption ? `${img}<figcaption>${escapeHtml(caption)}</figcaption>` : img;\n return `<figure data-block-id=\"${escapeAttr(block._id)}\">${inner}</figure>`;\n }\n case \"core/list\": {\n const ordered = block.attributes.ordered === true;\n const tag = ordered ? \"ol\" : \"ul\";\n const items = (block.innerBlocks ?? [])\n .map((item) => `<li>${item.rawHtml ?? renderInner(item.innerBlocks ?? [])}</li>`)\n .join(\"\");\n return `<${tag} data-block-id=\"${escapeAttr(block._id)}\">${items}</${tag}>`;\n }\n case \"core/quote\": {\n const inner = renderInner(block.innerBlocks ?? []);\n const cite = typeof block.attributes.citation === \"string\"\n ? `<cite>${escapeHtml(block.attributes.citation)}</cite>`\n : \"\";\n return `<blockquote data-block-id=\"${escapeAttr(block._id)}\">${inner}${cite}</blockquote>`;\n }\n case \"core/code\": {\n const code = typeof block.attributes.content === \"string\"\n ? escapeHtml(block.attributes.content)\n : escapeHtml(block.rawHtml ?? \"\");\n const lang = typeof block.attributes.language === \"string\"\n ? ` data-language=\"${escapeAttr(block.attributes.language)}\"`\n : \"\";\n return `<pre data-block-id=\"${escapeAttr(block._id)}\"${lang}><code>${code}</code></pre>`;\n }\n case \"core/columns\": {\n const inner = renderInner(block.innerBlocks ?? []);\n return `<div class=\"rt-columns\" data-block-id=\"${escapeAttr(block._id)}\">${inner}</div>`;\n }\n case \"core/separator\":\n return `<hr data-block-id=\"${escapeAttr(block._id)}\">`;\n case \"core/spacer\": {\n const heightRaw = block.attributes.height;\n const height = typeof heightRaw === \"string\" ? heightRaw : \"32px\";\n return `<div class=\"rt-spacer\" data-block-id=\"${escapeAttr(block._id)}\" style=\"height:${escapeAttr(height)}\"></div>`;\n }\n case \"core/group\": {\n const inner = renderInner(block.innerBlocks ?? []);\n return `<div class=\"rt-group\" data-block-id=\"${escapeAttr(block._id)}\">${inner}</div>`;\n }\n default: {\n // 알려지지 않은 block = opaque atom (codex v3 #4). rawHtml 출력만, 편집 X.\n const inner = block.rawHtml ?? renderInner(block.innerBlocks ?? []);\n return `<div class=\"rt-unknown-block\" data-block-id=\"${escapeAttr(block._id)}\" data-block-name=\"${escapeAttr(block.name)}\">${inner}</div>`;\n }\n }\n}\n\n/** Block tree 전체를 HTML 문서 body 로 직렬화. */\nexport function renderBlocks(blocks: readonly Block[]): string {\n return blocks.map((b) => renderBlock(b)).join(\"\\n\");\n}\n","// Tiptap JSON doc → HTML 문자열.\n//\n// `block-to-html.ts` (Block JSON 처리) 와 별개 — 본 파일은 admin-tenant 가\n// 저장하는 Tiptap doc (type:\"doc\", content:[...nodes]) 을 그대로 변환.\n// cms-renderer-next 의 `RenderTiptap` (server.tsx) 과 1:1 동등 surface.\n\ntype TiptapMark = { type?: string; attrs?: Record<string, unknown> };\ntype TiptapNode = {\n type?: string;\n text?: string;\n attrs?: Record<string, unknown>;\n content?: TiptapNode[];\n marks?: TiptapMark[];\n};\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\n}\n\nfunction escapeAttr(value: string): string {\n return value.replace(/\"/g, \"&quot;\").replace(/</g, \"&lt;\");\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 alignStyleAttr(node: TiptapNode): string {\n const a = node.attrs?.textAlign;\n if (typeof a !== \"string\" || a === \"\" || a === \"left\") return \"\";\n if (a === \"center\" || a === \"right\" || a === \"justify\") {\n return ` style=\"text-align:${a}\"`;\n }\n return \"\";\n}\n\nfunction applyMarks(marks: TiptapMark[], inner: string): string {\n let out = inner;\n for (const m of marks) {\n switch (m.type) {\n case \"bold\":\n out = `<strong>${out}</strong>`;\n break;\n case \"italic\":\n out = `<em>${out}</em>`;\n break;\n case \"underline\":\n out = `<u>${out}</u>`;\n break;\n case \"strike\":\n out = `<s>${out}</s>`;\n break;\n case \"code\":\n out = `<code>${out}</code>`;\n break;\n case \"highlight\": {\n const color = m.attrs?.color;\n const style =\n typeof color === \"string\" && color.length > 0\n ? ` style=\"background:${escapeAttr(color)}\"`\n : \"\";\n out = `<mark${style}>${out}</mark>`;\n break;\n }\n case \"textStyle\": {\n const color = m.attrs?.color;\n if (typeof color === \"string\" && color.length > 0) {\n out = `<span style=\"color:${escapeAttr(color)}\">${out}</span>`;\n }\n break;\n }\n case \"link\": {\n const raw = m.attrs?.href;\n const safe = isSafeHref(raw) ? escapeAttr(raw) : \"#\";\n out = `<a href=\"${safe}\" rel=\"noopener noreferrer\" target=\"_blank\">${out}</a>`;\n break;\n }\n default:\n out = `<span>${out}</span>`;\n }\n }\n return out;\n}\n\nfunction renderNodes(nodes: TiptapNode[]): string {\n return nodes.map((n) => renderNode(n)).join(\"\");\n}\n\nfunction renderNode(node: TiptapNode): string {\n if (!node || typeof node !== \"object\") return \"\";\n const children = Array.isArray(node.content) ? renderNodes(node.content) : \"\";\n switch (node.type) {\n case \"paragraph\":\n return `<p${alignStyleAttr(node)}>${children}</p>`;\n case \"heading\": {\n const levelRaw = Number(node.attrs?.level ?? 2);\n const level = Math.min(Math.max(Number.isFinite(levelRaw) ? levelRaw : 2, 1), 3);\n const idRaw = node.attrs?.id;\n const idAttr =\n typeof idRaw === \"string\" && idRaw.length > 0\n ? ` id=\"${escapeAttr(idRaw)}\"`\n : \"\";\n return `<h${level}${idAttr}${alignStyleAttr(node)}>${children}</h${level}>`;\n }\n case \"bulletList\":\n return `<ul>${children}</ul>`;\n case \"orderedList\":\n return `<ol>${children}</ol>`;\n case \"listItem\":\n return `<li>${children}</li>`;\n case \"blockquote\":\n return `<blockquote>${children}</blockquote>`;\n case \"codeBlock\":\n return `<pre><code>${children}</code></pre>`;\n case \"horizontalRule\":\n return \"<hr>\";\n case \"hardBreak\":\n return \"<br>\";\n case \"image\": {\n const src = node.attrs?.src;\n if (typeof src !== \"string\" || src.length === 0) return \"\";\n const alt = typeof node.attrs?.alt === \"string\" ? node.attrs.alt : \"\";\n const title =\n typeof node.attrs?.title === \"string\"\n ? ` title=\"${escapeAttr(node.attrs.title)}\"`\n : \"\";\n return `<img src=\"${escapeAttr(src)}\" alt=\"${escapeAttr(alt)}\"${title} loading=\"lazy\">`;\n }\n case \"columns\":\n return `<div class=\"rt-columns\">${children}</div>`;\n case \"column\":\n return `<div class=\"rt-column\">${children}</div>`;\n case \"text\": {\n const text = typeof node.text === \"string\" ? escapeHtml(node.text) : \"\";\n const marks = Array.isArray(node.marks) ? node.marks : [];\n if (marks.length === 0) return text;\n return applyMarks(marks, text);\n }\n default:\n return children ? `<span>${children}</span>` : \"\";\n }\n}\n\n/** Tiptap doc (admin-tenant 저장 형식) 을 HTML 문자열로 변환. */\nexport function renderTiptapDoc(doc: unknown): string {\n if (!doc || typeof doc !== \"object\") return \"\";\n const content = (doc as { content?: unknown }).content;\n if (!Array.isArray(content)) return \"\";\n return renderNodes(content as TiptapNode[]);\n}\n","// ADR-0034 §1.5 amended — Astro public renderer = first-class with Next.\n//\n// External Astro 사이트가 cms-client `fetchPosts`/`fetchPost` 결과를 HTML 로\n// 변환하는 helper. cms-renderer-next 의 `<RootTaleBlogList>` / `<RootTaleBlogPost>`\n// 와 동등 surface.\n//\n// 사용 예 (.astro):\n// ```astro\n// ---\n// import { renderBlogList } from \"@roottale/cms-renderer-astro\";\n// const html = await renderBlogList({ apiKey: import.meta.env.ROOTTALE_API_KEY! });\n// ---\n// <Fragment set:html={html} />\n// ```\n\nimport {\n fetchPost,\n fetchPosts,\n type CmsPostContent,\n type CmsPostType,\n} from \"@roottale/cms-client/server\";\n\nimport { renderTiptapDoc } from \"./tiptap-to-html\";\n\n/** XSS 차단: text content escape (block-to-html.ts 와 동일 정책) */\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\n}\n\nfunction escapeAttr(value: string): string {\n return value.replace(/\"/g, \"&quot;\").replace(/</g, \"&lt;\");\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\nexport interface RenderBlogListOptions {\n /** `rtlk_cust_*` API key (server-side only). */\n apiKey: string;\n /** Override the API base URL. Defaults to `https://api.roottale.com`. */\n baseUrl?: string;\n /** Page size, 1-100, default 20. */\n limit?: number;\n /** Filter by post type. Default `post` (blog list 의도). */\n type?: CmsPostType;\n /** Override the auto-resolved site id. */\n siteId?: string;\n /** Customer-side URL builder. Defaults to `/blog/${slug}`. */\n postHref?: (post: CmsPostContent) => string;\n /** Rendered when the tenant has no published posts yet. */\n emptyMessage?: string;\n}\n\nfunction defaultPostHref(post: CmsPostContent): string {\n return `/blog/${post.slug}`;\n}\n\nfunction renderPostCard(\n post: CmsPostContent,\n href: (p: CmsPostContent) => string,\n): string {\n const date = escapeHtml(formatPublishedDate(post.publishedAt));\n const title = escapeHtml(post.title);\n const excerptHtml = post.excerpt\n ? `<p class=\"rt-cms-excerpt\">${escapeHtml(post.excerpt)}</p>`\n : \"\";\n const url = escapeAttr(href(post));\n return (\n `<article data-roottale-cms=\"card\" class=\"rt-cms-card\">` +\n `<a class=\"rt-cms-card-link\" href=\"${url}\">` +\n `<p class=\"rt-cms-meta\">${date}</p>` +\n `<h2 class=\"rt-cms-title\">${title}</h2>` +\n `${excerptHtml}` +\n `</a></article>`\n );\n}\n\nexport async function renderBlogList(\n options: RenderBlogListOptions,\n): Promise<string> {\n const {\n apiKey,\n baseUrl,\n limit,\n siteId,\n type = \"post\",\n postHref = defaultPostHref,\n emptyMessage = \"아직 발행된 글이 없습니다.\",\n } = options;\n\n const page = await fetchPosts({ apiKey, baseUrl, limit, type, siteId });\n if (page.items.length === 0) {\n return `<div data-roottale-cms=\"list\"><p class=\"rt-cms-empty\">${escapeHtml(emptyMessage)}</p></div>`;\n }\n const items = page.items\n .map(\n (post) =>\n `<li class=\"rt-cms-list-item\">${renderPostCard(post, postHref)}</li>`,\n )\n .join(\"\");\n return `<div data-roottale-cms=\"list\"><ul class=\"rt-cms-list\">${items}</ul></div>`;\n}\n\nexport interface RenderBlogPostOptions {\n apiKey: string;\n baseUrl?: string;\n /** Slug (kebab-case) or UUIDv7 id. */\n slugOrId: string;\n /** Override the auto-resolved site id. */\n siteId?: string;\n /** Rendered when the slug is not found or not published. */\n notFoundHtml?: string;\n}\n\nexport async function renderBlogPost(\n options: RenderBlogPostOptions,\n): Promise<string> {\n const { apiKey, baseUrl, slugOrId, siteId, notFoundHtml } = options;\n const post = await fetchPost({ apiKey, baseUrl, slugOrId, siteId });\n if (!post) {\n return (\n notFoundHtml ??\n `<div data-roottale-cms=\"post-missing\"><p class=\"rt-cms-empty\">글을 찾을 수 없습니다.</p></div>`\n );\n }\n\n const bodyHtml = renderTiptapDoc(post.bodyJson);\n const date = escapeHtml(formatPublishedDate(post.publishedAt));\n const title = escapeHtml(post.title);\n const excerpt = post.excerpt\n ? `<p class=\"rt-cms-excerpt\">${escapeHtml(post.excerpt)}</p>`\n : \"\";\n\n return (\n `<article data-roottale-cms=\"post\" class=\"rt-cms-article\">` +\n `<header>` +\n `<p class=\"rt-cms-meta\">${date}</p>` +\n `<h1 class=\"rt-cms-title\">${title}</h1>` +\n `${excerpt}` +\n `</header>` +\n `<div data-roottale-cms=\"body\">${bodyHtml}</div>` +\n `</article>`\n );\n}\n","// ADR-0034 §1 + §7.1.1 test #5 — Technical SEO 95+ 의 비타협 약속.\n//\n// Phase 1 Exit Contract test #1 + #5 + #8 (sitemap/robots/canonical 자동 주입).\n\nexport interface PostSeoInput {\n title: string;\n description?: string | null;\n canonicalUrl: string;\n siteName: string;\n ogImage?: string | null;\n publishedAt?: Date | null;\n modifiedAt?: Date | null;\n authorName?: string | null;\n locale?: string; // default \"ko_KR\"\n noIndex?: boolean;\n}\n\n/**\n * <head> 메타 태그를 HTML 문자열로 생성. Astro layout 안에서 `<head>` 내부에 inject.\n * - title (헌장 §1 = Technical SEO 95+ 의 비타협 약속)\n * - meta description (있는 경우)\n * - canonical link (slug change 시 301 redirect 와 정합)\n * - Open Graph (basic)\n * - robots noindex (옵션)\n * - Schema.org Article JSON-LD\n */\nexport function renderSeoHead(input: PostSeoInput): string {\n const locale = input.locale ?? \"ko_KR\";\n const parts: string[] = [];\n\n parts.push(`<title>${escapeText(input.title)}</title>`);\n\n if (input.description) {\n parts.push(`<meta name=\"description\" content=\"${escapeAttr(input.description)}\">`);\n }\n\n parts.push(`<link rel=\"canonical\" href=\"${escapeAttr(input.canonicalUrl)}\">`);\n\n if (input.noIndex) {\n parts.push(`<meta name=\"robots\" content=\"noindex,follow\">`);\n }\n\n // Open Graph\n parts.push(`<meta property=\"og:type\" content=\"article\">`);\n parts.push(`<meta property=\"og:title\" content=\"${escapeAttr(input.title)}\">`);\n if (input.description) {\n parts.push(`<meta property=\"og:description\" content=\"${escapeAttr(input.description)}\">`);\n }\n parts.push(`<meta property=\"og:url\" content=\"${escapeAttr(input.canonicalUrl)}\">`);\n parts.push(`<meta property=\"og:site_name\" content=\"${escapeAttr(input.siteName)}\">`);\n parts.push(`<meta property=\"og:locale\" content=\"${escapeAttr(locale)}\">`);\n if (input.ogImage) {\n parts.push(`<meta property=\"og:image\" content=\"${escapeAttr(input.ogImage)}\">`);\n }\n\n // Twitter card\n parts.push(`<meta name=\"twitter:card\" content=\"${input.ogImage ? \"summary_large_image\" : \"summary\"}\">`);\n parts.push(`<meta name=\"twitter:title\" content=\"${escapeAttr(input.title)}\">`);\n if (input.description) {\n parts.push(`<meta name=\"twitter:description\" content=\"${escapeAttr(input.description)}\">`);\n }\n if (input.ogImage) {\n parts.push(`<meta name=\"twitter:image\" content=\"${escapeAttr(input.ogImage)}\">`);\n }\n\n // Schema.org Article JSON-LD\n const schema: Record<string, unknown> = {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Article\",\n headline: input.title,\n mainEntityOfPage: input.canonicalUrl,\n };\n if (input.description) schema.description = input.description;\n if (input.publishedAt) schema.datePublished = input.publishedAt.toISOString();\n if (input.modifiedAt) schema.dateModified = input.modifiedAt.toISOString();\n if (input.authorName) {\n schema.author = { \"@type\": \"Person\", name: input.authorName };\n }\n if (input.ogImage) schema.image = input.ogImage;\n parts.push(\n `<script type=\"application/ld+json\">${JSON.stringify(schema)}</script>`,\n );\n\n return parts.join(\"\\n\");\n}\n\nfunction escapeText(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n}\n\nfunction escapeAttr(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/</g, \"&lt;\");\n}\n","// ADR-0034 §7.1.1 test #1 — sitemap include 의무\n//\n// Per-site sitemap.xml 생성. published post/page 만 포함.\n\nexport interface SitemapEntry {\n url: string;\n lastModified?: Date | null;\n changeFreq?: \"always\" | \"hourly\" | \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"never\";\n priority?: number; // 0.0~1.0\n}\n\n/**\n * sitemap.xml 문자열 생성 (XML 1.0).\n * Phase 1 = flat list. Phase 2+ = index sitemap (chunk by 50k entries).\n */\nexport function renderSitemapXml(entries: readonly SitemapEntry[]): string {\n const xmlEntries = entries\n .map((e) => {\n const parts = [` <url>`, ` <loc>${escapeXml(e.url)}</loc>`];\n if (e.lastModified) {\n parts.push(` <lastmod>${e.lastModified.toISOString()}</lastmod>`);\n }\n if (e.changeFreq) {\n parts.push(` <changefreq>${e.changeFreq}</changefreq>`);\n }\n if (typeof e.priority === \"number\") {\n parts.push(` <priority>${e.priority.toFixed(1)}</priority>`);\n }\n parts.push(` </url>`);\n return parts.join(\"\\n\");\n })\n .join(\"\\n\");\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n${xmlEntries}\n</urlset>`;\n}\n\nfunction escapeXml(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n}\n","// ADR-0034 §7.1.1 test #1 — robots.txt 자동.\n\nexport interface RobotsConfig {\n /** User-agent 별 rule. 기본 \"*\" */\n rules?: Array<{\n userAgent: string;\n allow?: readonly string[];\n disallow?: readonly string[];\n }>;\n /** sitemap.xml 의 절대 URL */\n sitemapUrl?: string;\n /** 기본 disallow (draft preview 등 차단) */\n defaultDisallow?: readonly string[];\n}\n\nexport function renderRobotsTxt(config: RobotsConfig = {}): string {\n const rules = config.rules ?? [\n {\n userAgent: \"*\",\n disallow: config.defaultDisallow ?? [\"/admin\", \"/api/\", \"/preview\"],\n },\n ];\n\n const parts: string[] = [];\n for (const rule of rules) {\n parts.push(`User-agent: ${rule.userAgent}`);\n for (const path of rule.allow ?? []) {\n parts.push(`Allow: ${path}`);\n }\n for (const path of rule.disallow ?? []) {\n parts.push(`Disallow: ${path}`);\n }\n parts.push(\"\");\n }\n if (config.sitemapUrl) {\n parts.push(`Sitemap: ${config.sitemapUrl}`);\n }\n return parts.join(\"\\n\");\n}\n"],"mappings":"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","names":["escapeHtml","escapeAttr","escapeHtml","escapeAttr","escapeAttr"]}
1
+ {"version":3,"sources":["../src/block-to-html.ts","../src/tiptap-to-html.ts","../src/blog.ts","../src/seo.ts","../src/sitemap.ts","../src/robots.ts"],"sourcesContent":["// ADR-0034 §1.5 + §5 — block JSON → HTML\n//\n// Astro 가 public page 렌더링 시 본 함수로 block tree 를 server-side HTML 로 변환.\n// 7 block (paragraph/heading/image/list/quote/code/columns) Phase 1 핸들러.\n// 우리가 schema 정의 안 한 block 은 rawHtml 출력 (codex v3 verdict #4, opaque\n// atom round-trip 보존).\n\nimport type { Block } from \"@roottale/cms-core\";\n\n/** XSS 차단: text content escape */\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\n}\n\n/** attribute value escape (XSS) */\nfunction escapeAttr(value: string): string {\n return value.replace(/\"/g, \"&quot;\").replace(/</g, \"&lt;\");\n}\n\nfunction renderInner(blocks: readonly Block[]): string {\n return blocks.map((b) => renderBlock(b)).join(\"\");\n}\n\n/**\n * 단일 block 을 HTML 로 변환.\n * 알려진 `core/<x>` block name 은 dedicated handler. 그 외 = rawHtml 출력\n * (Tiptap opaque atom node 보존).\n */\nexport function renderBlock(block: Block): string {\n switch (block.name) {\n case \"core/paragraph\": {\n const text = typeof block.attributes.content === \"string\"\n ? block.attributes.content\n : (block.rawHtml ?? \"\");\n return `<p data-block-id=\"${escapeAttr(block._id)}\">${text}</p>`;\n }\n case \"core/heading\": {\n const levelRaw = block.attributes.level;\n const level = typeof levelRaw === \"number\" && levelRaw >= 1 && levelRaw <= 6 ? levelRaw : 2;\n const text = typeof block.attributes.content === \"string\"\n ? block.attributes.content\n : (block.rawHtml ?? \"\");\n return `<h${level} data-block-id=\"${escapeAttr(block._id)}\">${text}</h${level}>`;\n }\n case \"core/image\": {\n const src = typeof block.attributes.url === \"string\"\n ? escapeAttr(block.attributes.url)\n : \"\";\n const alt = typeof block.attributes.alt === \"string\"\n ? escapeAttr(block.attributes.alt)\n : \"\";\n const caption = typeof block.attributes.caption === \"string\"\n ? block.attributes.caption\n : null;\n const img = `<img src=\"${src}\" alt=\"${alt}\" loading=\"lazy\">`;\n const inner = caption ? `${img}<figcaption>${escapeHtml(caption)}</figcaption>` : img;\n return `<figure data-block-id=\"${escapeAttr(block._id)}\">${inner}</figure>`;\n }\n case \"core/list\": {\n const ordered = block.attributes.ordered === true;\n const tag = ordered ? \"ol\" : \"ul\";\n const items = (block.innerBlocks ?? [])\n .map((item) => `<li>${item.rawHtml ?? renderInner(item.innerBlocks ?? [])}</li>`)\n .join(\"\");\n return `<${tag} data-block-id=\"${escapeAttr(block._id)}\">${items}</${tag}>`;\n }\n case \"core/quote\": {\n const inner = renderInner(block.innerBlocks ?? []);\n const cite = typeof block.attributes.citation === \"string\"\n ? `<cite>${escapeHtml(block.attributes.citation)}</cite>`\n : \"\";\n return `<blockquote data-block-id=\"${escapeAttr(block._id)}\">${inner}${cite}</blockquote>`;\n }\n case \"core/code\": {\n const code = typeof block.attributes.content === \"string\"\n ? escapeHtml(block.attributes.content)\n : escapeHtml(block.rawHtml ?? \"\");\n const lang = typeof block.attributes.language === \"string\"\n ? ` data-language=\"${escapeAttr(block.attributes.language)}\"`\n : \"\";\n return `<pre data-block-id=\"${escapeAttr(block._id)}\"${lang}><code>${code}</code></pre>`;\n }\n case \"core/columns\": {\n const inner = renderInner(block.innerBlocks ?? []);\n return `<div class=\"rt-columns\" data-block-id=\"${escapeAttr(block._id)}\">${inner}</div>`;\n }\n case \"core/separator\":\n return `<hr data-block-id=\"${escapeAttr(block._id)}\">`;\n case \"core/spacer\": {\n const heightRaw = block.attributes.height;\n const height = typeof heightRaw === \"string\" ? heightRaw : \"32px\";\n return `<div class=\"rt-spacer\" data-block-id=\"${escapeAttr(block._id)}\" style=\"height:${escapeAttr(height)}\"></div>`;\n }\n case \"core/group\": {\n const inner = renderInner(block.innerBlocks ?? []);\n return `<div class=\"rt-group\" data-block-id=\"${escapeAttr(block._id)}\">${inner}</div>`;\n }\n default: {\n // 알려지지 않은 block = opaque atom (codex v3 #4). rawHtml 출력만, 편집 X.\n const inner = block.rawHtml ?? renderInner(block.innerBlocks ?? []);\n return `<div class=\"rt-unknown-block\" data-block-id=\"${escapeAttr(block._id)}\" data-block-name=\"${escapeAttr(block.name)}\">${inner}</div>`;\n }\n }\n}\n\n/** Block tree 전체를 HTML 문서 body 로 직렬화. */\nexport function renderBlocks(blocks: readonly Block[]): string {\n return blocks.map((b) => renderBlock(b)).join(\"\\n\");\n}\n","// Tiptap JSON doc → HTML 문자열.\n//\n// `block-to-html.ts` (Block JSON 처리) 와 별개 — 본 파일은 admin-tenant 가\n// 저장하는 Tiptap doc (type:\"doc\", content:[...nodes]) 을 그대로 변환.\n// cms-renderer-next 의 `RenderTiptap` (server.tsx) 과 1:1 동등 surface.\n\ntype TiptapMark = { type?: string; attrs?: Record<string, unknown> };\ntype TiptapNode = {\n type?: string;\n text?: string;\n attrs?: Record<string, unknown>;\n content?: TiptapNode[];\n marks?: TiptapMark[];\n};\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\n}\n\nfunction escapeAttr(value: string): string {\n return value.replace(/\"/g, \"&quot;\").replace(/</g, \"&lt;\");\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 alignStyleAttr(node: TiptapNode): string {\n const a = node.attrs?.textAlign;\n if (typeof a !== \"string\" || a === \"\" || a === \"left\") return \"\";\n if (a === \"center\" || a === \"right\" || a === \"justify\") {\n return ` style=\"text-align:${a}\"`;\n }\n return \"\";\n}\n\nfunction applyMarks(marks: TiptapMark[], inner: string): string {\n let out = inner;\n for (const m of marks) {\n switch (m.type) {\n case \"bold\":\n out = `<strong>${out}</strong>`;\n break;\n case \"italic\":\n out = `<em>${out}</em>`;\n break;\n case \"underline\":\n out = `<u>${out}</u>`;\n break;\n case \"strike\":\n out = `<s>${out}</s>`;\n break;\n case \"code\":\n out = `<code>${out}</code>`;\n break;\n case \"highlight\": {\n const color = m.attrs?.color;\n const style =\n typeof color === \"string\" && color.length > 0\n ? ` style=\"background:${escapeAttr(color)}\"`\n : \"\";\n out = `<mark${style}>${out}</mark>`;\n break;\n }\n case \"textStyle\": {\n const color = m.attrs?.color;\n if (typeof color === \"string\" && color.length > 0) {\n out = `<span style=\"color:${escapeAttr(color)}\">${out}</span>`;\n }\n break;\n }\n case \"link\": {\n const raw = m.attrs?.href;\n const safe = isSafeHref(raw) ? escapeAttr(raw) : \"#\";\n out = `<a href=\"${safe}\" rel=\"noopener noreferrer\" target=\"_blank\">${out}</a>`;\n break;\n }\n default:\n out = `<span>${out}</span>`;\n }\n }\n return out;\n}\n\nfunction renderNodes(nodes: TiptapNode[]): string {\n return nodes.map((n) => renderNode(n)).join(\"\");\n}\n\nfunction renderNode(node: TiptapNode): string {\n if (!node || typeof node !== \"object\") return \"\";\n const children = Array.isArray(node.content) ? renderNodes(node.content) : \"\";\n switch (node.type) {\n case \"paragraph\":\n return `<p${alignStyleAttr(node)}>${children}</p>`;\n case \"heading\": {\n const levelRaw = Number(node.attrs?.level ?? 2);\n const level = Math.min(Math.max(Number.isFinite(levelRaw) ? levelRaw : 2, 1), 3);\n const idRaw = node.attrs?.id;\n const idAttr =\n typeof idRaw === \"string\" && idRaw.length > 0\n ? ` id=\"${escapeAttr(idRaw)}\"`\n : \"\";\n return `<h${level}${idAttr}${alignStyleAttr(node)}>${children}</h${level}>`;\n }\n case \"bulletList\":\n return `<ul>${children}</ul>`;\n case \"orderedList\":\n return `<ol>${children}</ol>`;\n case \"listItem\":\n return `<li>${children}</li>`;\n case \"blockquote\":\n return `<blockquote>${children}</blockquote>`;\n case \"codeBlock\":\n return `<pre><code>${children}</code></pre>`;\n case \"horizontalRule\":\n return \"<hr>\";\n case \"hardBreak\":\n return \"<br>\";\n case \"image\": {\n const src = node.attrs?.src;\n if (typeof src !== \"string\" || src.length === 0) return \"\";\n const alt = typeof node.attrs?.alt === \"string\" ? node.attrs.alt : \"\";\n const title =\n typeof node.attrs?.title === \"string\"\n ? ` title=\"${escapeAttr(node.attrs.title)}\"`\n : \"\";\n return `<img src=\"${escapeAttr(src)}\" alt=\"${escapeAttr(alt)}\"${title} loading=\"lazy\">`;\n }\n case \"columns\":\n return `<div class=\"rt-columns\">${children}</div>`;\n case \"column\":\n return `<div class=\"rt-column\">${children}</div>`;\n case \"text\": {\n const text = typeof node.text === \"string\" ? escapeHtml(node.text) : \"\";\n const marks = Array.isArray(node.marks) ? node.marks : [];\n if (marks.length === 0) return text;\n return applyMarks(marks, text);\n }\n default:\n return children ? `<span>${children}</span>` : \"\";\n }\n}\n\n/** Tiptap doc (admin-tenant 저장 형식) 을 HTML 문자열로 변환. */\nexport function renderTiptapDoc(doc: unknown): string {\n if (!doc || typeof doc !== \"object\") return \"\";\n const content = (doc as { content?: unknown }).content;\n if (!Array.isArray(content)) return \"\";\n return renderNodes(content as TiptapNode[]);\n}\n","// ADR-0034 §1.5 amended — Astro public renderer = first-class with Next.\n//\n// External Astro 사이트가 cms-client `fetchPosts`/`fetchPost` 결과를 HTML 로\n// 변환하는 helper. cms-renderer-next 의 `<RootTaleBlogList>` / `<RootTaleBlogPost>`\n// 와 동등 surface.\n//\n// 사용 예 (.astro):\n// ```astro\n// ---\n// import { renderBlogList } from \"@roottale/cms-renderer-astro\";\n// const html = await renderBlogList({ apiKey: import.meta.env.ROOTTALE_API_KEY! });\n// ---\n// <Fragment set:html={html} />\n// ```\n\nimport {\n fetchPost,\n fetchPosts,\n fetchTheme,\n type CmsPostContent,\n type CmsPostType,\n type RootTaleTheme,\n} from \"@roottale/cms-client/server\";\n\nimport { renderTiptapDoc } from \"./tiptap-to-html\";\n\nexport type { RootTaleTheme } from \"@roottale/cms-client/server\";\n\n/**\n * theme prop 평가 결과 — Next renderer 의 `RootTaleThemeInput` 과 동일 의미:\n * - 객체 → 그대로 CSS 변수로 주입\n * - `null` → override 없음 (cms-public.css fallback 사용)\n * - 생략 → API key 로 자동 fetch (mysite.roottale.com 의 admin /design)\n */\nexport type RootTaleThemeInput = RootTaleTheme | null | undefined;\n\n/** XSS 차단: text content escape (block-to-html.ts 와 동일 정책) */\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\n}\n\nfunction escapeAttr(value: string): string {\n return value.replace(/\"/g, \"&quot;\").replace(/</g, \"&lt;\");\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\nexport interface RenderBlogListOptions {\n /** `rtlk_cust_*` API key (server-side only). */\n apiKey: string;\n /** Override the API base URL. Defaults to `https://api.roottale.com`. */\n baseUrl?: string;\n /** Page size, 1-100, default 20. */\n limit?: number;\n /** Filter by post type. Default `post` (blog list 의도). */\n type?: CmsPostType;\n /** Override the auto-resolved site id. */\n siteId?: string;\n /** Customer-side URL builder. Defaults to `/blog/${slug}`. */\n postHref?: (post: CmsPostContent) => string;\n /** Rendered when the tenant has no published posts yet. */\n emptyMessage?: string;\n /**\n * RootTale 디자인 토큰. 생략 시 admin /design 의 저장값을 자동 fetch,\n * `null` 명시 시 자동 fetch 도 건너뛰고 CSS fallback 사용.\n */\n theme?: RootTaleThemeInput;\n}\n\nfunction defaultPostHref(post: CmsPostContent): string {\n return `/blog/${post.slug}`;\n}\n\nfunction renderPostCard(\n post: CmsPostContent,\n href: (p: CmsPostContent) => string,\n): string {\n const date = escapeHtml(formatPublishedDate(post.publishedAt));\n const title = escapeHtml(post.title);\n const excerptHtml = post.excerpt\n ? `<p class=\"rt-cms-excerpt\">${escapeHtml(post.excerpt)}</p>`\n : \"\";\n const url = escapeAttr(href(post));\n return (\n `<article data-roottale-cms=\"card\" class=\"rt-cms-card\">` +\n `<a class=\"rt-cms-card-link\" href=\"${url}\">` +\n `<p class=\"rt-cms-meta\">${date}</p>` +\n `<h2 class=\"rt-cms-title\">${title}</h2>` +\n `${excerptHtml}` +\n `</a></article>`\n );\n}\n\nexport async function renderBlogList(\n options: RenderBlogListOptions,\n): Promise<string> {\n const {\n apiKey,\n baseUrl,\n limit,\n siteId,\n type = \"post\",\n postHref = defaultPostHref,\n emptyMessage = \"아직 발행된 글이 없습니다.\",\n theme: themeProp,\n } = options;\n\n const [page, themeStyleAttr] = await Promise.all([\n fetchPosts({ apiKey, baseUrl, limit, type, siteId }),\n resolveThemeStyleAttr({ themeProp, apiKey, baseUrl, siteId }),\n ]);\n if (page.items.length === 0) {\n return `<div data-roottale-cms=\"list\"${themeStyleAttr}><p class=\"rt-cms-empty\">${escapeHtml(emptyMessage)}</p></div>`;\n }\n const items = page.items\n .map(\n (post) =>\n `<li class=\"rt-cms-list-item\">${renderPostCard(post, postHref)}</li>`,\n )\n .join(\"\");\n return `<div data-roottale-cms=\"list\"${themeStyleAttr}><ul class=\"rt-cms-list\">${items}</ul></div>`;\n}\n\nexport interface RenderBlogPostOptions {\n apiKey: string;\n baseUrl?: string;\n /** Slug (kebab-case) or UUIDv7 id. */\n slugOrId: string;\n /** Override the auto-resolved site id. */\n siteId?: string;\n /** Rendered when the slug is not found or not published. */\n notFoundHtml?: string;\n /** See `RenderBlogListOptions['theme']`. */\n theme?: RootTaleThemeInput;\n}\n\nexport async function renderBlogPost(\n options: RenderBlogPostOptions,\n): Promise<string> {\n const {\n apiKey,\n baseUrl,\n slugOrId,\n siteId,\n notFoundHtml,\n theme: themeProp,\n } = options;\n const [post, themeStyleAttr] = await Promise.all([\n fetchPost({ apiKey, baseUrl, slugOrId, siteId }),\n resolveThemeStyleAttr({ themeProp, apiKey, baseUrl, siteId }),\n ]);\n if (!post) {\n return (\n notFoundHtml ??\n `<div data-roottale-cms=\"post-missing\"${themeStyleAttr}><p class=\"rt-cms-empty\">글을 찾을 수 없습니다.</p></div>`\n );\n }\n\n const bodyHtml = renderTiptapDoc(post.bodyJson);\n const date = escapeHtml(formatPublishedDate(post.publishedAt));\n const title = escapeHtml(post.title);\n const excerpt = post.excerpt\n ? `<p class=\"rt-cms-excerpt\">${escapeHtml(post.excerpt)}</p>`\n : \"\";\n\n return (\n `<article data-roottale-cms=\"post\" class=\"rt-cms-article\"${themeStyleAttr}>` +\n `<header>` +\n `<p class=\"rt-cms-meta\">${date}</p>` +\n `<h1 class=\"rt-cms-title\">${title}</h1>` +\n `${excerpt}` +\n `</header>` +\n `<div data-roottale-cms=\"body\">${bodyHtml}</div>` +\n `</article>`\n );\n}\n\nasync function resolveThemeStyleAttr(input: {\n themeProp: RootTaleThemeInput;\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n}): Promise<string> {\n const { themeProp, apiKey, baseUrl, siteId } = input;\n let theme: RootTaleTheme | null;\n if (themeProp === null) {\n theme = null;\n } else if (themeProp !== undefined) {\n theme = themeProp;\n } else {\n try {\n theme = await fetchTheme({ apiKey, baseUrl, siteId });\n } catch {\n theme = null;\n }\n }\n return renderThemeStyleAttr(theme);\n}\n\n/**\n * RootTale 디자인 토큰을 외부 HTML 의 `style=\"...\"` attribute 로 변환.\n * CSS 인젝션은 admin /design 의 server action 이 1차 차단 (`; < > \\r \\n`),\n * 본 함수도 동일한 차단 + `\"` 추가 escape (속성값 종료 방지).\n */\nfunction renderThemeStyleAttr(theme: RootTaleTheme | null): string {\n if (!theme) return \"\";\n const decls: string[] = [];\n const push = (key: string, value: string | undefined) => {\n if (!value) return;\n if (/[;<>\\r\\n\"]/.test(value)) return;\n decls.push(`${key}: ${value}`);\n };\n const c = theme.colors ?? {};\n push(\"--rt-color-primary\", c.primary);\n push(\"--rt-color-primary-foreground\", c.primaryForeground);\n push(\"--rt-color-foreground\", c.foreground);\n push(\"--rt-color-background\", c.background);\n push(\"--rt-color-muted\", c.muted);\n push(\"--rt-color-muted-foreground\", c.mutedForeground);\n push(\"--rt-color-border\", c.border);\n const f = theme.fonts ?? {};\n push(\"--rt-font-body\", f.body);\n push(\"--rt-font-display\", f.display);\n const r = theme.radius ?? {};\n push(\"--rt-radius-md\", r.md);\n if (decls.length === 0) return \"\";\n return ` style=\"${decls.join(\"; \")}\"`;\n}\n","// ADR-0034 §1 + §7.1.1 test #5 — Technical SEO 95+ 의 비타협 약속.\n//\n// Phase 1 Exit Contract test #1 + #5 + #8 (sitemap/robots/canonical 자동 주입).\n\nexport interface PostSeoInput {\n title: string;\n description?: string | null;\n canonicalUrl: string;\n siteName: string;\n ogImage?: string | null;\n publishedAt?: Date | null;\n modifiedAt?: Date | null;\n authorName?: string | null;\n locale?: string; // default \"ko_KR\"\n noIndex?: boolean;\n}\n\n/**\n * <head> 메타 태그를 HTML 문자열로 생성. Astro layout 안에서 `<head>` 내부에 inject.\n * - title (헌장 §1 = Technical SEO 95+ 의 비타협 약속)\n * - meta description (있는 경우)\n * - canonical link (slug change 시 301 redirect 와 정합)\n * - Open Graph (basic)\n * - robots noindex (옵션)\n * - Schema.org Article JSON-LD\n */\nexport function renderSeoHead(input: PostSeoInput): string {\n const locale = input.locale ?? \"ko_KR\";\n const parts: string[] = [];\n\n parts.push(`<title>${escapeText(input.title)}</title>`);\n\n if (input.description) {\n parts.push(`<meta name=\"description\" content=\"${escapeAttr(input.description)}\">`);\n }\n\n parts.push(`<link rel=\"canonical\" href=\"${escapeAttr(input.canonicalUrl)}\">`);\n\n if (input.noIndex) {\n parts.push(`<meta name=\"robots\" content=\"noindex,follow\">`);\n }\n\n // Open Graph\n parts.push(`<meta property=\"og:type\" content=\"article\">`);\n parts.push(`<meta property=\"og:title\" content=\"${escapeAttr(input.title)}\">`);\n if (input.description) {\n parts.push(`<meta property=\"og:description\" content=\"${escapeAttr(input.description)}\">`);\n }\n parts.push(`<meta property=\"og:url\" content=\"${escapeAttr(input.canonicalUrl)}\">`);\n parts.push(`<meta property=\"og:site_name\" content=\"${escapeAttr(input.siteName)}\">`);\n parts.push(`<meta property=\"og:locale\" content=\"${escapeAttr(locale)}\">`);\n if (input.ogImage) {\n parts.push(`<meta property=\"og:image\" content=\"${escapeAttr(input.ogImage)}\">`);\n }\n\n // Twitter card\n parts.push(`<meta name=\"twitter:card\" content=\"${input.ogImage ? \"summary_large_image\" : \"summary\"}\">`);\n parts.push(`<meta name=\"twitter:title\" content=\"${escapeAttr(input.title)}\">`);\n if (input.description) {\n parts.push(`<meta name=\"twitter:description\" content=\"${escapeAttr(input.description)}\">`);\n }\n if (input.ogImage) {\n parts.push(`<meta name=\"twitter:image\" content=\"${escapeAttr(input.ogImage)}\">`);\n }\n\n // Schema.org Article JSON-LD\n const schema: Record<string, unknown> = {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Article\",\n headline: input.title,\n mainEntityOfPage: input.canonicalUrl,\n };\n if (input.description) schema.description = input.description;\n if (input.publishedAt) schema.datePublished = input.publishedAt.toISOString();\n if (input.modifiedAt) schema.dateModified = input.modifiedAt.toISOString();\n if (input.authorName) {\n schema.author = { \"@type\": \"Person\", name: input.authorName };\n }\n if (input.ogImage) schema.image = input.ogImage;\n parts.push(\n `<script type=\"application/ld+json\">${JSON.stringify(schema)}</script>`,\n );\n\n return parts.join(\"\\n\");\n}\n\nfunction escapeText(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n}\n\nfunction escapeAttr(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/</g, \"&lt;\");\n}\n","// ADR-0034 §7.1.1 test #1 — sitemap include 의무\n//\n// Per-site sitemap.xml 생성. published post/page 만 포함.\n\nexport interface SitemapEntry {\n url: string;\n lastModified?: Date | null;\n changeFreq?: \"always\" | \"hourly\" | \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"never\";\n priority?: number; // 0.0~1.0\n}\n\n/**\n * sitemap.xml 문자열 생성 (XML 1.0).\n * Phase 1 = flat list. Phase 2+ = index sitemap (chunk by 50k entries).\n */\nexport function renderSitemapXml(entries: readonly SitemapEntry[]): string {\n const xmlEntries = entries\n .map((e) => {\n const parts = [` <url>`, ` <loc>${escapeXml(e.url)}</loc>`];\n if (e.lastModified) {\n parts.push(` <lastmod>${e.lastModified.toISOString()}</lastmod>`);\n }\n if (e.changeFreq) {\n parts.push(` <changefreq>${e.changeFreq}</changefreq>`);\n }\n if (typeof e.priority === \"number\") {\n parts.push(` <priority>${e.priority.toFixed(1)}</priority>`);\n }\n parts.push(` </url>`);\n return parts.join(\"\\n\");\n })\n .join(\"\\n\");\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n${xmlEntries}\n</urlset>`;\n}\n\nfunction escapeXml(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n}\n","// ADR-0034 §7.1.1 test #1 — robots.txt 자동.\n\nexport interface RobotsConfig {\n /** User-agent 별 rule. 기본 \"*\" */\n rules?: Array<{\n userAgent: string;\n allow?: readonly string[];\n disallow?: readonly string[];\n }>;\n /** sitemap.xml 의 절대 URL */\n sitemapUrl?: string;\n /** 기본 disallow (draft preview 등 차단) */\n defaultDisallow?: readonly string[];\n}\n\nexport function renderRobotsTxt(config: RobotsConfig = {}): string {\n const rules = config.rules ?? [\n {\n userAgent: \"*\",\n disallow: config.defaultDisallow ?? [\"/admin\", \"/api/\", \"/preview\"],\n },\n ];\n\n const parts: string[] = [];\n for (const rule of rules) {\n parts.push(`User-agent: ${rule.userAgent}`);\n for (const path of rule.allow ?? []) {\n parts.push(`Allow: ${path}`);\n }\n for (const path of rule.disallow ?? []) {\n parts.push(`Disallow: ${path}`);\n }\n parts.push(\"\");\n }\n if (config.sitemapUrl) {\n parts.push(`Sitemap: ${config.sitemapUrl}`);\n }\n return parts.join(\"\\n\");\n}\n"],"mappings":"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","names":["escapeHtml","escapeAttr","escapeHtml","escapeAttr","escapeAttr"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roottale/cms-renderer-astro",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "RootTale CMS Astro 6 SSG/SSR public renderer. renderBlogList / renderBlogPost helpers + Tiptap → HTML. Companion of @roottale/cms-renderer-next (ADR-0034 §1.5 amended).",
6
6
  "main": "./dist/index.js",
@@ -19,8 +19,8 @@
19
19
  "CHANGELOG.md"
20
20
  ],
21
21
  "dependencies": {
22
- "@roottale/cms-core": "^0.2.1",
23
- "@roottale/cms-client": "^0.1.1"
22
+ "@roottale/cms-client": "^0.4.0",
23
+ "@roottale/cms-core": "^0.2.1"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^22.0.0",