@roottale/cms-renderer-next 0.10.0 → 0.12.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 +21 -0
- package/README.md +24 -0
- package/dist/cms-public.css +418 -44
- package/dist/routes.d.ts +101 -0
- package/dist/routes.js +110 -0
- package/dist/routes.js.map +1 -0
- package/dist/server.d.ts +6 -6
- package/dist/server.js +72 -28
- package/dist/server.js.map +1 -1
- package/package.json +8 -3
package/dist/routes.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// src/routes.ts
|
|
2
|
+
import {
|
|
3
|
+
fetchPosts,
|
|
4
|
+
generateRssXml,
|
|
5
|
+
rssItemsFromPosts
|
|
6
|
+
} from "@roottale/cms-client/server";
|
|
7
|
+
import {
|
|
8
|
+
verifyRootTaleWebhook
|
|
9
|
+
} from "@roottale/cms-client/webhook";
|
|
10
|
+
function trimSlash(u) {
|
|
11
|
+
return u.replace(/\/+$/, "");
|
|
12
|
+
}
|
|
13
|
+
async function safeFetchPosts(config, limit) {
|
|
14
|
+
try {
|
|
15
|
+
const page = await fetchPosts({
|
|
16
|
+
apiKey: config.apiKey,
|
|
17
|
+
baseUrl: config.apiBase,
|
|
18
|
+
type: "post",
|
|
19
|
+
limit
|
|
20
|
+
});
|
|
21
|
+
return page.items;
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.error("[@roottale/cms-renderer-next/routes] fetchPosts \uC2E4\uD328:", err);
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function createFeedRoute(config) {
|
|
28
|
+
return async function GET() {
|
|
29
|
+
const siteUrl = trimSlash(config.siteUrl);
|
|
30
|
+
const blogBase = config.blogBasePath ?? "/blog";
|
|
31
|
+
const posts = await safeFetchPosts(config, 50);
|
|
32
|
+
const xml = generateRssXml({
|
|
33
|
+
title: config.title,
|
|
34
|
+
link: `${siteUrl}${blogBase}`,
|
|
35
|
+
feedUrl: `${siteUrl}/feed.xml`,
|
|
36
|
+
description: config.description,
|
|
37
|
+
language: config.language,
|
|
38
|
+
items: rssItemsFromPosts(posts, {
|
|
39
|
+
postUrl: (p) => `${siteUrl}${blogBase}/${p.slug}`
|
|
40
|
+
})
|
|
41
|
+
});
|
|
42
|
+
return new Response(xml, {
|
|
43
|
+
headers: {
|
|
44
|
+
"content-type": "application/rss+xml; charset=utf-8",
|
|
45
|
+
"cache-control": "public, max-age=1800, s-maxage=1800"
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function createSitemap(config, staticRoutes = []) {
|
|
51
|
+
return async function sitemap() {
|
|
52
|
+
const siteUrl = trimSlash(config.siteUrl);
|
|
53
|
+
const blogBase = config.blogBasePath ?? "/blog";
|
|
54
|
+
const posts = await safeFetchPosts(config, 200);
|
|
55
|
+
const postEntries = posts.map((p) => ({
|
|
56
|
+
url: `${siteUrl}${blogBase}/${p.slug}`,
|
|
57
|
+
lastModified: p.updatedAt ?? p.publishedAt ?? void 0,
|
|
58
|
+
changeFrequency: "weekly",
|
|
59
|
+
priority: 0.6
|
|
60
|
+
}));
|
|
61
|
+
return [...staticRoutes, ...postEntries];
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function pathsFromBody(rawBody) {
|
|
65
|
+
try {
|
|
66
|
+
const body = JSON.parse(rawBody);
|
|
67
|
+
if (Array.isArray(body.paths)) {
|
|
68
|
+
return body.paths.filter((p) => typeof p === "string");
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
function createRevalidateRoute(config) {
|
|
75
|
+
const blogBase = config.blogBasePath ?? "/blog";
|
|
76
|
+
const alsoRevalidate = config.alsoRevalidate ?? [
|
|
77
|
+
"/feed.xml",
|
|
78
|
+
"/sitemap.xml",
|
|
79
|
+
blogBase
|
|
80
|
+
];
|
|
81
|
+
return async function POST(req) {
|
|
82
|
+
const rawBody = await req.text();
|
|
83
|
+
const result = await verifyRootTaleWebhook({
|
|
84
|
+
rawBody,
|
|
85
|
+
headers: req.headers,
|
|
86
|
+
apiKey: config.apiKey,
|
|
87
|
+
apiBase: config.apiBase,
|
|
88
|
+
consumeJti: config.consumeJti
|
|
89
|
+
});
|
|
90
|
+
if (!result.ok) {
|
|
91
|
+
return Response.json({ ok: false, reason: result.reason }, { status: 401 });
|
|
92
|
+
}
|
|
93
|
+
const paths = [.../* @__PURE__ */ new Set([...pathsFromBody(rawBody), ...alsoRevalidate])];
|
|
94
|
+
for (const p of paths) {
|
|
95
|
+
await config.revalidate(p);
|
|
96
|
+
}
|
|
97
|
+
return Response.json({
|
|
98
|
+
ok: true,
|
|
99
|
+
event: result.event,
|
|
100
|
+
deliveryId: result.deliveryId,
|
|
101
|
+
revalidated: paths
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export {
|
|
106
|
+
createFeedRoute,
|
|
107
|
+
createRevalidateRoute,
|
|
108
|
+
createSitemap
|
|
109
|
+
};
|
|
110
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +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":[]}
|
package/dist/server.d.ts
CHANGED
|
@@ -31,24 +31,24 @@ declare function RootTalePostCard(props: RootTalePostCardProps): ReactElement;
|
|
|
31
31
|
/**
|
|
32
32
|
* `@roottale/cms-renderer/toc` — Tiptap doc → TOC entries + heading id injection.
|
|
33
33
|
*
|
|
34
|
-
* Server-only utility. Walks a Tiptap JSON doc, extracts
|
|
34
|
+
* Server-only utility. Walks a Tiptap JSON doc, extracts H1-H4 headings, and
|
|
35
35
|
* derives stable kebab-case ids (Korean-safe). The companion `attachHeadingIds`
|
|
36
|
-
* mutates a shallow clone of the doc so renderers can emit
|
|
37
|
-
*
|
|
36
|
+
* mutates a shallow clone of the doc so renderers can emit heading anchors
|
|
37
|
+
* that match what `<RootTaleTableOfContents>` links to.
|
|
38
38
|
*
|
|
39
39
|
* Internal ancestor: `roottale-internal/packages/cms-renderer/src/toc.ts`
|
|
40
40
|
* (HTML-string based, vendored snapshot). Ported to Tiptap-JSON because the
|
|
41
41
|
* platform renderer no longer round-trips through HTML before render.
|
|
42
42
|
*/
|
|
43
43
|
interface TocEntry {
|
|
44
|
-
level: 2 | 3;
|
|
44
|
+
level: 2 | 3 | 4;
|
|
45
45
|
text: string;
|
|
46
46
|
id: string;
|
|
47
47
|
}
|
|
48
48
|
declare function headingToId(text: string): string;
|
|
49
49
|
declare function extractToc(doc: Record<string, unknown>): TocEntry[];
|
|
50
50
|
/**
|
|
51
|
-
* Returns a shallow-cloned Tiptap doc where each
|
|
51
|
+
* Returns a shallow-cloned Tiptap doc where each H1-H4 carries the matching
|
|
52
52
|
* `attrs.id` from `entries` (same order as `extractToc` returns). Existing
|
|
53
53
|
* `id` attrs are overwritten so renderer + TOC always agree on anchors.
|
|
54
54
|
*/
|
|
@@ -232,7 +232,7 @@ type RootTaleBlogPostProps = {
|
|
|
232
232
|
notFoundElement?: ReactElement;
|
|
233
233
|
/**
|
|
234
234
|
* When true, prepends an auto-derived `<RootTaleTableOfContents>` before
|
|
235
|
-
* the article body (H2
|
|
235
|
+
* the article body (H2-H4 headings only). Default: false — keep markup
|
|
236
236
|
* stable for customers who already control their own layout.
|
|
237
237
|
*/
|
|
238
238
|
showTableOfContents?: boolean;
|
package/dist/server.js
CHANGED
|
@@ -16,14 +16,18 @@ function RootTalePostCard(props) {
|
|
|
16
16
|
const showDate = display ? display.showDate : true;
|
|
17
17
|
const showAuthor = display ? display.showAuthor : false;
|
|
18
18
|
const authorName = post.authorName;
|
|
19
|
-
const
|
|
20
|
-
showDate ? formatPublishedDate(post.publishedAt) : null,
|
|
21
|
-
showAuthor && authorName ? `\uC791\uC131\uC790 ${authorName}` : null
|
|
22
|
-
].filter(Boolean).join(" \xB7 ");
|
|
19
|
+
const publishedDate = showDate ? formatPublishedDate(post.publishedAt) : "";
|
|
23
20
|
return /* @__PURE__ */ jsx("article", { "data-roottale-cms": "card", className: "rt-cms-card", children: /* @__PURE__ */ jsxs("a", { className: "rt-cms-card-link", href: href(post), children: [
|
|
24
|
-
|
|
21
|
+
publishedDate || showAuthor && authorName ? /* @__PURE__ */ jsxs("p", { className: "rt-cms-meta", children: [
|
|
22
|
+
publishedDate ? /* @__PURE__ */ jsx("span", { className: "rt-cms-meta-item", "data-rt-cms-meta": "date", children: publishedDate }) : null,
|
|
23
|
+
showAuthor && authorName ? /* @__PURE__ */ jsxs("span", { className: "rt-cms-meta-item", "data-rt-cms-meta": "author", children: [
|
|
24
|
+
"\uC791\uC131\uC790 ",
|
|
25
|
+
authorName
|
|
26
|
+
] }) : null
|
|
27
|
+
] }) : null,
|
|
25
28
|
/* @__PURE__ */ jsx("h2", { className: "rt-cms-title", children: post.title }),
|
|
26
|
-
post.excerpt ? /* @__PURE__ */ jsx("p", { className: "rt-cms-excerpt", children: post.excerpt }) : null
|
|
29
|
+
post.excerpt ? /* @__PURE__ */ jsx("p", { className: "rt-cms-excerpt", children: post.excerpt }) : null,
|
|
30
|
+
/* @__PURE__ */ jsx("span", { className: "rt-cms-card-action", "aria-hidden": "true", children: "\uC77D\uAE30" })
|
|
27
31
|
] }) });
|
|
28
32
|
}
|
|
29
33
|
function defaultHref(post) {
|
|
@@ -44,8 +48,11 @@ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
|
44
48
|
function RootTaleTableOfContents(props) {
|
|
45
49
|
const { entries, title = "\uBAA9\uCC28" } = props;
|
|
46
50
|
if (entries.length === 0) return null;
|
|
47
|
-
return /* @__PURE__ */
|
|
48
|
-
|
|
51
|
+
return /* @__PURE__ */ jsx2("nav", { "data-roottale-cms": "toc", className: "rt-cms-toc", "aria-label": title, children: /* @__PURE__ */ jsxs2("details", { className: "rt-cms-toc-details", open: true, children: [
|
|
52
|
+
/* @__PURE__ */ jsxs2("summary", { className: "rt-cms-toc-summary", children: [
|
|
53
|
+
title ? /* @__PURE__ */ jsx2("span", { className: "rt-cms-toc-title", children: title }) : null,
|
|
54
|
+
/* @__PURE__ */ jsx2("span", { className: "rt-cms-toc-toggle", "aria-hidden": "true" })
|
|
55
|
+
] }),
|
|
49
56
|
/* @__PURE__ */ jsx2("ol", { className: "rt-cms-toc-list", children: entries.map((entry) => /* @__PURE__ */ jsx2(
|
|
50
57
|
"li",
|
|
51
58
|
{
|
|
@@ -54,10 +61,15 @@ function RootTaleTableOfContents(props) {
|
|
|
54
61
|
},
|
|
55
62
|
entry.id
|
|
56
63
|
)) })
|
|
57
|
-
] });
|
|
64
|
+
] }) });
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
// src/toc.ts
|
|
68
|
+
function tocLevel(rawLevel) {
|
|
69
|
+
if (rawLevel < 1 || rawLevel > 4) return null;
|
|
70
|
+
if (rawLevel < 3) return 2;
|
|
71
|
+
return rawLevel === 3 ? 3 : 4;
|
|
72
|
+
}
|
|
61
73
|
function headingToId(text) {
|
|
62
74
|
return text.trim().toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
63
75
|
}
|
|
@@ -74,14 +86,15 @@ function extractToc(doc) {
|
|
|
74
86
|
for (const node of content) {
|
|
75
87
|
if (node?.type !== "heading") continue;
|
|
76
88
|
const rawLevel = Number(node.attrs?.level ?? 2);
|
|
77
|
-
|
|
89
|
+
const level = tocLevel(rawLevel);
|
|
90
|
+
if (level === null) continue;
|
|
78
91
|
const text = textOf(node).trim();
|
|
79
92
|
if (!text) continue;
|
|
80
93
|
const base = headingToId(text) || "heading";
|
|
81
94
|
const count = idCounts.get(base) ?? 0;
|
|
82
95
|
const id = count === 0 ? base : `${base}-${count}`;
|
|
83
96
|
idCounts.set(base, count + 1);
|
|
84
|
-
entries.push({ level
|
|
97
|
+
entries.push({ level, text, id });
|
|
85
98
|
}
|
|
86
99
|
return entries;
|
|
87
100
|
}
|
|
@@ -92,7 +105,7 @@ function attachHeadingIds(doc, entries) {
|
|
|
92
105
|
const nextContent = content.map((node) => {
|
|
93
106
|
if (node?.type !== "heading") return node;
|
|
94
107
|
const rawLevel = Number(node.attrs?.level ?? 2);
|
|
95
|
-
if (rawLevel
|
|
108
|
+
if (rawLevel < 1 || rawLevel > 4) return node;
|
|
96
109
|
if (!textOf(node).trim()) return node;
|
|
97
110
|
const entry = entries[cursor++];
|
|
98
111
|
if (!entry) return node;
|
|
@@ -314,12 +327,13 @@ function RootTaleLeadForm(props) {
|
|
|
314
327
|
}
|
|
315
328
|
|
|
316
329
|
// src/block-to-react.tsx
|
|
330
|
+
import { sanitizeHtml } from "@roottale/cms-core";
|
|
317
331
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
318
332
|
function isString(value) {
|
|
319
333
|
return typeof value === "string";
|
|
320
334
|
}
|
|
321
335
|
function htmlContent(html) {
|
|
322
|
-
return { __html: html };
|
|
336
|
+
return { __html: sanitizeHtml(html) };
|
|
323
337
|
}
|
|
324
338
|
function renderInner(blocks) {
|
|
325
339
|
return blocks.map((b, i) => renderBlock(b, i));
|
|
@@ -340,18 +354,15 @@ function renderBlock(block, key = 0) {
|
|
|
340
354
|
}
|
|
341
355
|
case "core/heading": {
|
|
342
356
|
const levelRaw = block.attributes.level;
|
|
343
|
-
const level = typeof levelRaw === "number" && levelRaw >=
|
|
357
|
+
const level = typeof levelRaw === "number" && levelRaw >= 2 && levelRaw <= 4 ? levelRaw : 2;
|
|
344
358
|
const text = isString(block.attributes.content) ? block.attributes.content : block.rawHtml ?? "";
|
|
345
359
|
const props = {
|
|
346
360
|
"data-block-id": dataBlockId,
|
|
347
361
|
dangerouslySetInnerHTML: htmlContent(text)
|
|
348
362
|
};
|
|
349
|
-
if (level === 1) return /* @__PURE__ */ jsx5("h1", { ...props }, key);
|
|
350
363
|
if (level === 2) return /* @__PURE__ */ jsx5("h2", { ...props }, key);
|
|
351
364
|
if (level === 3) return /* @__PURE__ */ jsx5("h3", { ...props }, key);
|
|
352
|
-
|
|
353
|
-
if (level === 5) return /* @__PURE__ */ jsx5("h5", { ...props }, key);
|
|
354
|
-
return /* @__PURE__ */ jsx5("h6", { ...props }, key);
|
|
365
|
+
return /* @__PURE__ */ jsx5("h4", { ...props }, key);
|
|
355
366
|
}
|
|
356
367
|
case "core/image": {
|
|
357
368
|
const src = isString(block.attributes.url) ? block.attributes.url : "";
|
|
@@ -513,10 +524,11 @@ async function RootTaleBlogPost(props) {
|
|
|
513
524
|
const toc = display.showTableOfContents ? extractToc(post.bodyJson) : [];
|
|
514
525
|
const renderDoc = toc.length > 0 ? attachHeadingIds(post.bodyJson, toc) : post.bodyJson;
|
|
515
526
|
const authorName = post.authorName;
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
527
|
+
const publishedDate = display.showDate ? formatPublishedDate2(post.publishedAt) : "";
|
|
528
|
+
const showTopAuthor = display.showAuthor && Boolean(authorName);
|
|
529
|
+
const displayAuthorName = display.authorProfileName ?? authorName;
|
|
530
|
+
const authorBio = display.authorProfileBio ?? display.authorCardDescription;
|
|
531
|
+
const showAuthorCard = display.showAuthorCard && Boolean(displayAuthorName);
|
|
520
532
|
return /* @__PURE__ */ jsxs6(
|
|
521
533
|
"article",
|
|
522
534
|
{
|
|
@@ -524,17 +536,48 @@ async function RootTaleBlogPost(props) {
|
|
|
524
536
|
className: "rt-cms-article",
|
|
525
537
|
style: themeStyle ?? void 0,
|
|
526
538
|
children: [
|
|
527
|
-
/* @__PURE__ */ jsxs6("header", { children: [
|
|
528
|
-
|
|
539
|
+
/* @__PURE__ */ jsxs6("header", { className: "rt-cms-post-header", children: [
|
|
540
|
+
publishedDate || showTopAuthor ? /* @__PURE__ */ jsxs6("p", { className: "rt-cms-meta", children: [
|
|
541
|
+
publishedDate ? /* @__PURE__ */ jsx6("span", { className: "rt-cms-meta-item", "data-rt-cms-meta": "date", children: publishedDate }) : null,
|
|
542
|
+
showTopAuthor ? /* @__PURE__ */ jsxs6("span", { className: "rt-cms-meta-item", "data-rt-cms-meta": "author", children: [
|
|
543
|
+
"\uC791\uC131\uC790 ",
|
|
544
|
+
authorName
|
|
545
|
+
] }) : null
|
|
546
|
+
] }) : null,
|
|
529
547
|
/* @__PURE__ */ jsx6("h1", { className: "rt-cms-title", children: post.title }),
|
|
530
548
|
post.excerpt ? /* @__PURE__ */ jsx6("p", { className: "rt-cms-excerpt", children: post.excerpt }) : null
|
|
531
549
|
] }),
|
|
532
550
|
toc.length > 0 ? /* @__PURE__ */ jsx6(RootTaleTableOfContents, { entries: toc, title: display.tocTitle }) : null,
|
|
533
|
-
/* @__PURE__ */ jsx6(RenderTiptap, { doc: renderDoc })
|
|
551
|
+
/* @__PURE__ */ jsx6(RenderTiptap, { doc: renderDoc }),
|
|
552
|
+
showAuthorCard && displayAuthorName ? /* @__PURE__ */ jsxs6("section", { className: "rt-cms-author-card", "aria-label": "\uC791\uC131\uC790", children: [
|
|
553
|
+
display.authorProfileImageUrl ? /* @__PURE__ */ jsx6(
|
|
554
|
+
"img",
|
|
555
|
+
{
|
|
556
|
+
className: "rt-cms-author-card__image",
|
|
557
|
+
src: display.authorProfileImageUrl,
|
|
558
|
+
alt: "",
|
|
559
|
+
loading: "lazy"
|
|
560
|
+
}
|
|
561
|
+
) : /* @__PURE__ */ jsx6("div", { className: "rt-cms-author-card__avatar", "aria-hidden": "true", children: authorInitials(displayAuthorName) }),
|
|
562
|
+
/* @__PURE__ */ jsxs6("div", { className: "rt-cms-author-card__body", children: [
|
|
563
|
+
/* @__PURE__ */ jsx6("p", { className: "rt-cms-author-card__eyebrow", children: display.authorCardEyebrow }),
|
|
564
|
+
/* @__PURE__ */ jsx6("p", { className: "rt-cms-author-card__name", children: displayAuthorName }),
|
|
565
|
+
authorBio ? /* @__PURE__ */ jsx6("p", { className: "rt-cms-author-card__description", children: authorBio }) : null
|
|
566
|
+
] })
|
|
567
|
+
] }) : null
|
|
534
568
|
]
|
|
535
569
|
}
|
|
536
570
|
);
|
|
537
571
|
}
|
|
572
|
+
function authorInitials(name) {
|
|
573
|
+
const trimmed = name.trim();
|
|
574
|
+
if (!trimmed) return "";
|
|
575
|
+
const parts = trimmed.split(/\s+/).filter(Boolean);
|
|
576
|
+
if (parts.length >= 2) {
|
|
577
|
+
return `${parts[0]?.[0] ?? ""}${parts[1]?.[0] ?? ""}`.toUpperCase();
|
|
578
|
+
}
|
|
579
|
+
return [...trimmed].slice(0, 2).join("").toUpperCase();
|
|
580
|
+
}
|
|
538
581
|
async function resolveThemeStyle(input) {
|
|
539
582
|
const { themeProp, apiKey, baseUrl } = input;
|
|
540
583
|
if (themeProp === null) return null;
|
|
@@ -604,13 +647,14 @@ function renderNode(node, key) {
|
|
|
604
647
|
case "paragraph":
|
|
605
648
|
return /* @__PURE__ */ jsx6("p", { style: alignStyle(node), children }, key);
|
|
606
649
|
case "heading": {
|
|
607
|
-
const
|
|
650
|
+
const rawLevel = Number(node.attrs?.level ?? 2);
|
|
651
|
+
const level = Math.min(Math.max(Number.isFinite(rawLevel) ? rawLevel : 2, 2), 4);
|
|
608
652
|
const idAttr = node.attrs?.id;
|
|
609
653
|
const id = typeof idAttr === "string" && idAttr.length > 0 ? idAttr : void 0;
|
|
610
654
|
const style = alignStyle(node);
|
|
611
|
-
if (level === 1) return /* @__PURE__ */ jsx6("h1", { id, style, children }, key);
|
|
612
655
|
if (level === 2) return /* @__PURE__ */ jsx6("h2", { id, style, children }, key);
|
|
613
|
-
return /* @__PURE__ */ jsx6("h3", { id, style, children }, key);
|
|
656
|
+
if (level === 3) return /* @__PURE__ */ jsx6("h3", { id, style, children }, key);
|
|
657
|
+
return /* @__PURE__ */ jsx6("h4", { id, style, children }, key);
|
|
614
658
|
}
|
|
615
659
|
case "bulletList":
|
|
616
660
|
return /* @__PURE__ */ jsx6("ul", { children }, key);
|