@roottale/cms-renderer-next 0.36.0 → 0.37.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 +41 -0
- package/dist/{chunk-GAHOVQYW.js → chunk-VWGMLIY4.js} +3 -2
- package/dist/chunk-VWGMLIY4.js.map +1 -0
- package/dist/routes.d.ts +53 -7
- package/dist/routes.js +158 -52
- package/dist/routes.js.map +1 -1
- package/dist/server.d.ts +6 -0
- package/dist/server.js +1 -1
- package/dist/templates.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-GAHOVQYW.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# @roottale/cms-renderer-next
|
|
2
2
|
|
|
3
|
+
## 0.37.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- de62ad1: 작가 아카이브 페이지 + 작가 사이트맵 (blanche식 /blog/author/{slug})
|
|
8
|
+
- `fetchAuthors()` 추가 (`GET /v1/cms/public/authors`) — slug가 있고 발행 글이 1건
|
|
9
|
+
이상인 작가 목록(slug·이름·이미지·소개·글 수·최근 수정일). 어드민 **설정 > 팀**에서
|
|
10
|
+
작가 slug를 발급한다(site 내 unique).
|
|
11
|
+
- `RootTaleBlogList` 에 `author` prop, `fetchPosts` 에 `author` 옵션 추가 —
|
|
12
|
+
`GET /v1/cms/public/posts?author={slug}` 로 그 작가의 발행 글만 렌더(작가 아카이브용).
|
|
13
|
+
- `createSitemapIndex` 에 `authors` 하위 사이트맵(`/sitemap/authors.xml`) 추가 — 어드민
|
|
14
|
+
**설정 > 블로그 > 사이트맵**의 "작가 사이트맵"(`authors`) 토글이 켜졌을 때만 인덱스에
|
|
15
|
+
포함. `authorBasePath` 옵션(기본 `/blog/author`)으로 경로 커스터마이즈.
|
|
16
|
+
- starter 에 `app/blog/author/[slug]/page.tsx` 추가 + cms-mcp seo.md 문서 갱신.
|
|
17
|
+
|
|
18
|
+
- c77238d: 사이트맵 다국어 hreflang (스캐폴딩)
|
|
19
|
+
- `createSitemapIndex` 가 사이트맵 설정의 `locales`(예 `["ko","ja","en"]`)가 있으면
|
|
20
|
+
모든 섹션(static/blog/categories/authors) entry 에 `alternates.languages`(hreflang)
|
|
21
|
+
를 단다. 첫 로케일 = 기본(현 URL) + `x-default`, 나머지는 path-prefix(`/ja/...`).
|
|
22
|
+
어드민 **설정 > 블로그 > 사이트맵** 의 "다국어 로케일"에서 입력.
|
|
23
|
+
- 같은 slug + 언어 접두사 1:1 을 가정하는 스캐폴딩 — 실제 per-locale 페이지 서빙은
|
|
24
|
+
사이트 책임(번역 콘텐츠 모델 도입 시 재방문). 비우면 단일 언어(기본).
|
|
25
|
+
- 어드민 사이트맵 카드에 "작가 사이트맵"(authors) 토글도 노출 — 작가 사이트맵을
|
|
26
|
+
UI 에서 켤 수 있다.
|
|
27
|
+
|
|
28
|
+
- 36e2dae: 사이트맵 인덱스 분리 + 블로그 이미지 색인 (blanche식)
|
|
29
|
+
- `createSitemapIndex(config, staticRoutes, opts)` 추가 — Next 16 `generateSitemaps`
|
|
30
|
+
로 `/sitemap.xml`(인덱스) + `/sitemap/static.xml`·`/sitemap/blog.xml`·
|
|
31
|
+
`/sitemap/categories.xml` 하위 사이트맵으로 분리. 50k URL/50MB 한도 확장 + 검색엔진
|
|
32
|
+
섹션별 크롤. 소비자는 `export { generateSitemaps }; export default sitemap;`.
|
|
33
|
+
- 블로그 글 entry 에 대표 이미지(`featuredImageUrl`)를 `<image:image>` 로 색인 —
|
|
34
|
+
어드민 **설정 > 블로그 > 사이트맵**의 토글(`blogImages`)로 제어. `RootTaleBlogSettings.sitemap`
|
|
35
|
+
(`/v1/cms/public/blog-settings` 응답)에 사이트맵 정책(`blogImages`/`locales`/`authors`) 추가.
|
|
36
|
+
- `changeFrequency`/`priority` 는 더 이상 emit 하지 않음(Google 무시 — `loc`+`lastmod` +이미지만). 레거시 `createSitemap`(단일 평면)은 deprecate 되었으나 하위호환 유지.
|
|
37
|
+
|
|
38
|
+
### Patch Changes
|
|
39
|
+
|
|
40
|
+
- Updated dependencies [de62ad1]
|
|
41
|
+
- Updated dependencies [36e2dae]
|
|
42
|
+
- @roottale/cms-client@0.37.0
|
|
43
|
+
|
|
3
44
|
## 0.36.0
|
|
4
45
|
|
|
5
46
|
### Minor Changes
|
|
@@ -537,11 +537,12 @@ async function RootTaleBlogList(props) {
|
|
|
537
537
|
activeCategory,
|
|
538
538
|
categoryHref = defaultCategoryHref,
|
|
539
539
|
allCategoriesLabel = "\uC804\uCCB4",
|
|
540
|
+
author,
|
|
540
541
|
theme: themeProp
|
|
541
542
|
} = props;
|
|
542
543
|
const postHref = props.postHref ?? (collections ? makeCollectionPostHref(collections) : defaultPostHref);
|
|
543
544
|
const [page, themeStyle, blogSettings] = await Promise.all([
|
|
544
|
-
fetchPosts({ apiKey, baseUrl, limit, type }),
|
|
545
|
+
fetchPosts({ apiKey, baseUrl, limit, type, ...author ? { author } : {} }),
|
|
545
546
|
resolveThemeStyle({ themeProp, apiKey, baseUrl }),
|
|
546
547
|
fetchBlogSettings({ apiKey, baseUrl }).catch(() => DEFAULT_BLOG_SETTINGS)
|
|
547
548
|
]);
|
|
@@ -1043,4 +1044,4 @@ export {
|
|
|
1043
1044
|
RootTalePage,
|
|
1044
1045
|
RenderTiptap
|
|
1045
1046
|
};
|
|
1046
|
-
//# sourceMappingURL=chunk-
|
|
1047
|
+
//# sourceMappingURL=chunk-VWGMLIY4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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 type RootTaleBlogSettings,\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 postsForCollection,\n resolvePostPath,\n type RouteCollection,\n} from \"@roottale/cms-core\";\n\nimport { RootTalePostCard } from \"./PostCard.js\";\nimport { RootTaleTableOfContents } from \"./TableOfContents.js\";\nimport {\n collectCategories,\n collectCategoriesWithCounts,\n collectCollectionCategories,\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 /**\n * Build a customer-side URL for a single post. Defaults to `/blog/${slug}`,\n * 또는 `collections` 를 주면 글의 섹션 basePath 기준(`/notice/${slug}` 등).\n */\n postHref?: (post: CmsPostContent) => string;\n /** Rendered when the tenant has no published posts yet. */\n emptyMessage?: string;\n /**\n * 단일 섹션(stream)으로 스코프 (ADR-0060). `collections` 와 함께 줘야 동작한다.\n * 예: `collection=\"notice\"` → 공지 섹션 글만 렌더. **미지정 = 전체** 글(레거시\n * 단일 블로그). 이 prop 없이 다중 스트림 사이트에서 쓰면 공지·블로그가 한 목록에\n * 섞여 나오므로(`/blog` 에 공지가 새어 들어옴), 섹션을 나눈 사이트는 각 목록\n * 페이지에서 반드시 `collection` 으로 스코프해야 한다.\n */\n collection?: string;\n /**\n * 섹션 정의 (ADR-0060). `collection` 스코프와 collection-aware `postHref`(글을\n * 소속 섹션 basePath 로 라우팅) 해석에 쓰인다. admin 에서 동적으로 받으려면\n * `fetchCollections()` 결과를 넘긴다(`collections.md` 참고).\n */\n collections?: readonly RouteCollection[];\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 * 작가 slug 로 글을 필터 — 작가 아카이브(/blog/author/{slug}) 페이지용. 지정 시\n * 그 작가의 발행 글(post)만 fetch 한다. `fetchAuthors` 로 작가 메타(이름·소개)를\n * 따로 가져와 페이지 헤더에 쓴다.\n */\n author?: 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 collection,\n collections,\n emptyMessage = \"아직 발행된 글이 없습니다.\",\n showCategoryFilter = false,\n activeCategory,\n categoryHref = defaultCategoryHref,\n allCategoriesLabel = \"전체\",\n author,\n theme: themeProp,\n } = props;\n // collections 를 주면 글을 소속 섹션 basePath 로 라우팅(공지→/notice 등),\n // 아니면 레거시 `/blog/${slug}`. 명시 postHref 가 있으면 그게 우선.\n const postHref =\n props.postHref ??\n (collections ? makeCollectionPostHref(collections) : defaultPostHref);\n\n const [page, themeStyle, blogSettings] = await Promise.all([\n fetchPosts({ apiKey, baseUrl, limit, type, ...(author ? { author } : {}) }),\n resolveThemeStyle({ themeProp, apiKey, baseUrl }),\n fetchBlogSettings({ apiKey, baseUrl }).catch(() => DEFAULT_BLOG_SETTINGS),\n ]);\n\n // 섹션 스코프(ADR-0060) — `collection`+`collections` 가 있으면 그 섹션 글만.\n // 미지정이면 전체(레거시). 공지·블로그가 한 목록에 섞이는 누출을 막는다.\n const items =\n collection && collections\n ? postsForCollection(page.items, collections, collection)\n : page.items;\n\n if (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(items) : [];\n const visiblePosts = showCategoryFilter\n ? filterPostsByCategory(items, activeCategory)\n : 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 /**\n * 단일 섹션으로 스코프 (ADR-0060). `collections` 와 함께 줘야 동작. 그 섹션의\n * `categories`(주제 allowlist) 안에서, 그 섹션에 귀속된 글의 카테고리만 집계한다.\n * 미지정 = 전체 글의 카테고리.\n */\n collection?: string;\n /** 섹션 정의 (ADR-0060). `collection` 스코프에 필요. */\n collections?: readonly RouteCollection[];\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 collection,\n collections,\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 =\n collection && collections\n ? collectCollectionCategories(page.items, collections, collection)\n : 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 {renderPostCta(blogSettings.postCta)}\n </article>\n );\n}\n\n/**\n * 글 하단 고정 CTA — 사이트 블로그 설정에 활성화돼 있으면 모든 글 본문 끝에\n * 동일 CTA 블록을 그린다(리드 전환 유도). href 는 안전 스킴만 통과 — 외부\n * 링크(http/mailto/tel 이 아닌 절대 URL 제외)는 새 탭으로 열되 그 외엔 차단.\n */\nfunction renderPostCta(cta: RootTaleBlogSettings[\"postCta\"]): ReactElement | null {\n if (!cta) return null;\n const href = cta.buttonHref.trim();\n const safe =\n href.startsWith(\"/\") ||\n href.startsWith(\"#\") ||\n /^(https?:|mailto:|tel:)/i.test(href);\n if (!safe) return null;\n const external = /^https?:/i.test(href);\n return (\n <aside data-roottale-cms=\"post-cta\" className=\"rt-cms-post-cta\">\n {cta.title ? <p className=\"rt-cms-post-cta__title\">{cta.title}</p> : null}\n {cta.description ? (\n <p className=\"rt-cms-post-cta__desc\">{cta.description}</p>\n ) : null}\n <a\n className=\"rt-cms-post-cta__btn\"\n href={href}\n target={external ? \"_blank\" : undefined}\n rel={external ? \"noopener noreferrer\" : undefined}\n >\n {cta.buttonLabel}\n </a>\n </aside>\n );\n}\n\nexport type RootTalePageProps = {\n apiKey: string;\n /** Page slug (kebab-case) or UUIDv7 id. */\n slugOrId: string;\n baseUrl?: string;\n /** Rendered when the slug is not found or not published. */\n notFoundElement?: ReactElement;\n /**\n * Render the page title as `<h1>`. Default true — set false when the\n * customer layout already owns the heading.\n */\n showTitle?: boolean;\n /** See `RootTaleBlogListProps['theme']`. */\n theme?: RootTaleThemeInput;\n};\n\n/**\n * 고정 페이지(posts type='page') 렌더러 — RootTaleBlogPost 의 경량판.\n * 날짜·작성자·author card·blog settings 등 블로그 chrome 없이\n * 제목 + 본문만 그린다. 회사 소개·서비스·이용약관 류의 페이지 용도.\n *\n * ```tsx\n * // app/[slug]/page.tsx\n * export default async function CmsPage({ params }) {\n * const { slug } = await params;\n * return <RootTalePage apiKey={process.env.ROOTTALE_API_KEY!} slugOrId={slug} />;\n * }\n * ```\n */\nexport async function RootTalePage(\n props: RootTalePageProps,\n): Promise<ReactElement> {\n const {\n apiKey,\n slugOrId,\n baseUrl,\n notFoundElement,\n showTitle = true,\n theme: themeProp,\n } = props;\n const [post, themeStyle] = await Promise.all([\n fetchPost({ apiKey, slugOrId, baseUrl }),\n resolveThemeStyle({ themeProp, apiKey, baseUrl }),\n ]);\n\n if (!post) {\n return (\n notFoundElement ?? (\n <div data-roottale-cms=\"page-missing\" style={themeStyle ?? undefined}>\n <p className=\"rt-cms-empty\">페이지를 찾을 수 없습니다.</p>\n </div>\n )\n );\n }\n\n return (\n <article\n data-roottale-cms=\"page\"\n className=\"rt-cms-article\"\n style={themeStyle ?? undefined}\n >\n {showTitle ? (\n <header className=\"rt-cms-post-header\">\n <h1 className=\"rt-cms-title\">{post.title}</h1>\n </header>\n ) : null}\n <div data-roottale-cms=\"body\" className=\"rt-cms-body\">\n <RenderTiptap doc={post.bodyJson} fields={post.fields} />\n </div>\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.secondary) vars[\"--rt-color-secondary\"] = c.secondary;\n if (c.secondaryForeground) vars[\"--rt-color-secondary-foreground\"] = c.secondaryForeground;\n if (c.accent) vars[\"--rt-color-accent\"] = c.accent;\n if (c.accentForeground) vars[\"--rt-color-accent-foreground\"] = c.accentForeground;\n if (c.muted) vars[\"--rt-color-muted\"] = c.muted;\n if (c.mutedForeground) vars[\"--rt-color-muted-foreground\"] = c.mutedForeground;\n if (c.destructive) vars[\"--rt-color-destructive\"] = c.destructive;\n if (c.destructiveForeground) vars[\"--rt-color-destructive-foreground\"] = c.destructiveForeground;\n if (c.border) vars[\"--rt-color-border\"] = c.border;\n if (c.input) vars[\"--rt-color-input\"] = c.input;\n if (c.ring) vars[\"--rt-color-ring\"] = c.ring;\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.sm) vars[\"--rt-radius-sm\"] = r.sm;\n if (r.md) vars[\"--rt-radius-md\"] = r.md;\n if (r.lg) vars[\"--rt-radius-lg\"] = r.lg;\n if (r.xl) vars[\"--rt-radius-xl\"] = r.xl;\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\n/**\n * collections 기반 postHref — 글을 소속 섹션 basePath 로 라우팅(공지→/notice,\n * 블로그→/blog). 어느 섹션에도 안 속하면 레거시 `/blog/${slug}` 로 fallback.\n */\nfunction makeCollectionPostHref(\n collections: readonly RouteCollection[],\n): (post: CmsPostContent) => string {\n return (post) => resolvePostPath(post, collections) ?? defaultPostHref(post);\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 // 목차 블록이 있으면 헤딩 id 부여 + 항목 수집(앵커 대상). astro 와 동일.\n const hasToc = content.some((n) => n?.type === \"tableOfContents\");\n const docRecord = doc as Record<string, unknown>;\n const entries = hasToc ? extractToc(docRecord) : [];\n const renderDoc =\n hasToc && entries.length > 0 ? attachHeadingIds(docRecord, entries) : docRecord;\n const nodes: TiptapNode[] = Array.isArray((renderDoc as TiptapDoc).content)\n ? ((renderDoc as TiptapDoc).content as TiptapNode[])\n : content;\n return (\n <>\n {nodes.map((node, i) =>\n node?.type === \"tableOfContents\" ? (\n <RootTaleTableOfContents\n key={i}\n entries={entries}\n title={\n typeof node.attrs?.title === \"string\" ? node.attrs.title : \"목차\"\n }\n />\n ) : (\n renderNode(node, i, fields ?? null)\n ),\n )}\n </>\n );\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\n/** 표 셀의 colSpan/rowSpan (1 초과일 때만 적용, 아니면 undefined). */\nfunction tableCellSpans(node: TiptapNode): {\n colSpan: number | undefined;\n rowSpan: number | undefined;\n} {\n const colspan = Number(node.attrs?.colspan);\n const rowspan = Number(node.attrs?.rowspan);\n return {\n colSpan: Number.isFinite(colspan) && colspan > 1 ? colspan : undefined,\n rowSpan: Number.isFinite(rowspan) && rowspan > 1 ? rowspan : undefined,\n };\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 // width/height — CLS 대책. CSS(max-inline-size:100%; block-size:auto)와\n // 조합돼 반응형 유지. 에디터/WP import 가 보존한 치수가 있을 때만 출력.\n const width =\n typeof node.attrs?.width === \"number\" && node.attrs.width > 0\n ? node.attrs.width\n : undefined;\n const height =\n typeof node.attrs?.height === \"number\" && node.attrs.height > 0\n ? node.attrs.height\n : undefined;\n return (\n <img\n key={key}\n src={src}\n alt={alt}\n title={title}\n width={width}\n height={height}\n loading=\"lazy\"\n />\n );\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 \"table\":\n return (\n <div key={key} className=\"rt-table-wrap\">\n <table className=\"rt-table\">\n <tbody>{children}</tbody>\n </table>\n </div>\n );\n case \"tableRow\":\n return <tr key={key}>{children}</tr>;\n case \"tableHeader\": {\n const { colSpan, rowSpan } = tableCellSpans(node);\n return <th key={key} colSpan={colSpan} rowSpan={rowSpan}>{children}</th>;\n }\n case \"tableCell\": {\n const { colSpan, rowSpan } = tableCellSpans(node);\n return <td key={key} colSpan={colSpan} rowSpan={rowSpan}>{children}</td>;\n }\n case \"embed\":\n return renderEmbed(node, key);\n case \"tableOfContents\":\n // 목차는 문서 전체 컨텍스트가 필요해 RenderTiptap 최상위에서 처리.\n // 중첩 등장 시엔 무시(헤딩 수집 컨텍스트 없음).\n return null;\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/dist/routes.d.ts
CHANGED
|
@@ -70,21 +70,66 @@ declare function createFeedRoute(config: RootTaleSiteConfig): () => Promise<Resp
|
|
|
70
70
|
interface SitemapEntry {
|
|
71
71
|
url: string;
|
|
72
72
|
lastModified?: string | Date;
|
|
73
|
+
/** @deprecated Google 이 무시 — emit 하지 않는다(blanche식 loc+lastmod). */
|
|
73
74
|
changeFrequency?: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
|
|
75
|
+
/** @deprecated Google 이 무시 — emit 하지 않는다. */
|
|
74
76
|
priority?: number;
|
|
77
|
+
/** `<image:image>` — 글 대표 이미지 등. Next `MetadataRoute.Sitemap` 호환. */
|
|
78
|
+
images?: string[];
|
|
79
|
+
/** hreflang 대체 언어 — 다국어(`alternates.languages`). Next 호환. */
|
|
80
|
+
alternates?: {
|
|
81
|
+
languages?: Record<string, string>;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** `createSitemapIndex` 옵션 — admin 설정을 코드에서 덮어쓰거나 fetch 를 끌 때만. */
|
|
85
|
+
interface SitemapIndexOptions {
|
|
86
|
+
/** admin 의 `blogImages` 토글을 덮어쓴다. */
|
|
87
|
+
blogImages?: boolean;
|
|
88
|
+
/** admin 의 `locales` 를 덮어쓴다 (다국어, PR C). */
|
|
89
|
+
locales?: string[];
|
|
90
|
+
/** admin 의 `authors` 토글을 덮어쓴다 (작가 사이트맵). */
|
|
91
|
+
authors?: boolean;
|
|
92
|
+
/** 작가 아카이브 base path. 기본 "/blog/author" (→ /blog/author/{slug}). */
|
|
93
|
+
authorBasePath?: string;
|
|
94
|
+
/** true 면 admin 설정 fetch 생략(opts/기본값만). 빌드 시 네트워크 회피용. */
|
|
95
|
+
skipRemoteSettings?: boolean;
|
|
75
96
|
}
|
|
76
97
|
/**
|
|
77
|
-
* `
|
|
98
|
+
* @deprecated `createSitemapIndex` 를 쓰세요 — 단일 평면 사이트맵 대신 인덱스 분리
|
|
99
|
+
* (static/blog/categories) + 이미지 + admin 토글을 지원한다. 본 함수는 하위호환용으로
|
|
100
|
+
* 유지되며 `changeFrequency`/`priority` 는 더 이상 emit 하지 않는다(blanche식).
|
|
101
|
+
*
|
|
102
|
+
* `app/sitemap.ts` default export. 정적 경로 + 글 + 카테고리를 단일 파일로 합친다.
|
|
103
|
+
*/
|
|
104
|
+
declare function createSitemap(config: RootTaleSiteConfig, staticRoutes?: SitemapEntry[]): () => Promise<SitemapEntry[]>;
|
|
105
|
+
/** blanche식 인덱스 분리 사이트맵의 하위 섹션 id. */
|
|
106
|
+
type SitemapSectionId = "static" | "blog" | "categories" | "authors";
|
|
107
|
+
/**
|
|
108
|
+
* blanche식 사이트맵 인덱스 — `/sitemap.xml`(인덱스) + `/sitemap/<id>.xml` 하위
|
|
109
|
+
* 사이트맵(static/blog/categories)으로 분리한다(Next 16 `generateSitemaps`). 50k
|
|
110
|
+
* URL/50MB 한도 확장 + 검색엔진이 섹션별로 크롤. 블로그 글에는 대표 이미지를
|
|
111
|
+
* `<image:image>` 로 함께 색인(admin 토글). `changeFrequency`/`priority` 는 emit
|
|
112
|
+
* 하지 않는다.
|
|
78
113
|
*
|
|
79
114
|
* ```ts
|
|
80
|
-
*
|
|
81
|
-
*
|
|
115
|
+
* // app/sitemap.ts
|
|
116
|
+
* import { createSitemapIndex } from "@roottale/cms-renderer-next/routes";
|
|
117
|
+
* const { generateSitemaps, sitemap } = createSitemapIndex(
|
|
82
118
|
* { apiKey: process.env.ROOTTALE_API_KEY!, siteUrl: "https://example.com", title: "..." },
|
|
83
|
-
* [{ url: "https://example.com",
|
|
119
|
+
* [{ url: "https://example.com" }, { url: "https://example.com/contact" }],
|
|
84
120
|
* );
|
|
121
|
+
* export { generateSitemaps };
|
|
122
|
+
* export default sitemap;
|
|
85
123
|
* ```
|
|
86
124
|
*/
|
|
87
|
-
declare function
|
|
125
|
+
declare function createSitemapIndex(config: RootTaleSiteConfig, staticRoutes?: SitemapEntry[], opts?: SitemapIndexOptions): {
|
|
126
|
+
generateSitemaps: () => Promise<{
|
|
127
|
+
id: SitemapSectionId;
|
|
128
|
+
}[]>;
|
|
129
|
+
sitemap: (props: {
|
|
130
|
+
id: string | Promise<string>;
|
|
131
|
+
}) => Promise<SitemapEntry[]>;
|
|
132
|
+
};
|
|
88
133
|
|
|
89
134
|
/** `routes` — llms.txt (AEO/GEO, AI 검색·생성엔진용 마크다운 인덱스). */
|
|
90
135
|
|
|
@@ -146,7 +191,8 @@ interface RevalidateRouteConfig {
|
|
|
146
191
|
* 모든 글/분류/미디어 변경에 대해 *항상 함께* revalidate 할 파생 경로.
|
|
147
192
|
* 피드·사이트맵·블로그 인덱스처럼 "글 목록"에 의존하는 경로 — admin 변경이
|
|
148
193
|
* 자동 반영되려면 필수. 명시 시 collections 파생을 덮어쓴다.
|
|
149
|
-
* 기본(레거시): ["/feed.xml", "/
|
|
194
|
+
* 기본(레거시): ["/feed.xml", "/llms.txt", "/sitemap.xml", "/sitemap/static.xml",
|
|
195
|
+
* "/sitemap/blog.xml", "/sitemap/categories.xml", blogBase, `${blogBase}/categories`].
|
|
150
196
|
*/
|
|
151
197
|
alsoRevalidate?: string[];
|
|
152
198
|
/** (옵션) jti replay 방지 hook (cms-client verify 로 전달). */
|
|
@@ -483,4 +529,4 @@ interface FleetInfoRouteConfig {
|
|
|
483
529
|
*/
|
|
484
530
|
declare function createFleetInfoRoute(config?: FleetInfoRouteConfig): () => Promise<Response>;
|
|
485
531
|
|
|
486
|
-
export { type BuildPostMetadataOptions, type CollectionsInput, type FleetInfoRouteConfig, type ImageResponseLike, type LlmsTxtLink, type LlmsTxtRouteConfig, type LlmsTxtSection, OG_IMAGE_CONTENT_TYPE, OG_IMAGE_SIZE, type OgImageRouteConfig, type PostMetadata, type PostMetadataInput, type PostSeoOverrides, type RedirectMiddlewareConfig, type RevalidateRouteConfig, type RootTaleSiteConfig, type SitemapEntry, buildPostMetadata, createFeedRoute, createFleetInfoRoute, createLlmsTxtRoute, createPostOgImage, createRedirectMiddleware, createRevalidateRoute, createSitemap, matchRedirect, normalizeRedirectPath, postRedirectPath, resolveCollections, resolveCollectionsResult, safeFetchPosts, trimSlash };
|
|
532
|
+
export { type BuildPostMetadataOptions, type CollectionsInput, type FleetInfoRouteConfig, type ImageResponseLike, type LlmsTxtLink, type LlmsTxtRouteConfig, type LlmsTxtSection, OG_IMAGE_CONTENT_TYPE, OG_IMAGE_SIZE, type OgImageRouteConfig, type PostMetadata, type PostMetadataInput, type PostSeoOverrides, type RedirectMiddlewareConfig, type RevalidateRouteConfig, type RootTaleSiteConfig, type SitemapEntry, type SitemapIndexOptions, type SitemapSectionId, buildPostMetadata, createFeedRoute, createFleetInfoRoute, createLlmsTxtRoute, createPostOgImage, createRedirectMiddleware, createRevalidateRoute, createSitemap, createSitemapIndex, matchRedirect, normalizeRedirectPath, postRedirectPath, resolveCollections, resolveCollectionsResult, safeFetchPosts, trimSlash };
|
package/dist/routes.js
CHANGED
|
@@ -45,7 +45,12 @@ async function safeFetchPosts(config, limit) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// src/routes-feed-sitemap.ts
|
|
48
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
fetchAuthors,
|
|
50
|
+
fetchBlogSettings,
|
|
51
|
+
generateRssXml,
|
|
52
|
+
rssItemsFromPosts
|
|
53
|
+
} from "@roottale/cms-client/server";
|
|
49
54
|
import {
|
|
50
55
|
isInCollectionMode,
|
|
51
56
|
resolvePostCollection as resolvePostCollection2,
|
|
@@ -80,62 +85,152 @@ function createFeedRoute(config) {
|
|
|
80
85
|
});
|
|
81
86
|
};
|
|
82
87
|
}
|
|
88
|
+
var SITEMAP_DEFAULTS = {
|
|
89
|
+
blogImages: true,
|
|
90
|
+
locales: [],
|
|
91
|
+
authors: false
|
|
92
|
+
};
|
|
93
|
+
async function safeFetchAuthors(config) {
|
|
94
|
+
try {
|
|
95
|
+
return await fetchAuthors({ apiKey: config.apiKey, baseUrl: config.apiBase });
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error("[@roottale/cms-renderer-next/routes] fetchAuthors \uC2E4\uD328:", err);
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function resolveSitemapSettings(config, opts) {
|
|
102
|
+
let remote = {};
|
|
103
|
+
if (!opts.skipRemoteSettings) {
|
|
104
|
+
try {
|
|
105
|
+
remote = (await fetchBlogSettings({
|
|
106
|
+
apiKey: config.apiKey,
|
|
107
|
+
baseUrl: config.apiBase
|
|
108
|
+
})).sitemap ?? {};
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
blogImages: opts.blogImages ?? remote.blogImages ?? SITEMAP_DEFAULTS.blogImages,
|
|
114
|
+
locales: opts.locales ?? remote.locales ?? SITEMAP_DEFAULTS.locales,
|
|
115
|
+
authors: opts.authors ?? remote.authors ?? SITEMAP_DEFAULTS.authors
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function localizeEntries(entries, siteUrl, locales) {
|
|
119
|
+
const defaultLocale = locales[0];
|
|
120
|
+
if (!defaultLocale) return entries;
|
|
121
|
+
const rest = locales.slice(1);
|
|
122
|
+
return entries.map((entry) => {
|
|
123
|
+
if (entry.url !== siteUrl && !entry.url.startsWith(`${siteUrl}/`)) return entry;
|
|
124
|
+
const path = entry.url.slice(siteUrl.length);
|
|
125
|
+
const languages = {
|
|
126
|
+
[defaultLocale]: entry.url,
|
|
127
|
+
"x-default": entry.url
|
|
128
|
+
};
|
|
129
|
+
for (const loc of rest) languages[loc] = `${siteUrl}/${loc}${path}`;
|
|
130
|
+
return { ...entry, alternates: { languages } };
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function staticEntries(staticRoutes) {
|
|
134
|
+
return staticRoutes.map((r) => {
|
|
135
|
+
const entry = { url: r.url };
|
|
136
|
+
if (r.lastModified !== void 0) entry.lastModified = r.lastModified;
|
|
137
|
+
if (r.images) entry.images = r.images;
|
|
138
|
+
if (r.alternates) entry.alternates = r.alternates;
|
|
139
|
+
return entry;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function postEntries(config, siteUrl, posts, collections, settings) {
|
|
143
|
+
const inCollection = isInCollectionMode(collections);
|
|
144
|
+
const blogBase = config.blogBasePath ?? "/blog";
|
|
145
|
+
return posts.flatMap((p) => {
|
|
146
|
+
const url = inCollection ? resolvePostUrl2(siteUrl, p, collections) : `${siteUrl}${blogBase}/${p.slug}`;
|
|
147
|
+
if (!url) return [];
|
|
148
|
+
const entry = {
|
|
149
|
+
url,
|
|
150
|
+
lastModified: p.updatedAt ?? p.publishedAt ?? void 0
|
|
151
|
+
};
|
|
152
|
+
if (settings.blogImages && p.featuredImageUrl) {
|
|
153
|
+
entry.images = [p.featuredImageUrl];
|
|
154
|
+
}
|
|
155
|
+
return [entry];
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function categoryEntries(config, siteUrl, posts, collections) {
|
|
159
|
+
if (isInCollectionMode(collections)) {
|
|
160
|
+
return collections.filter((c) => c.archives).flatMap((c) => {
|
|
161
|
+
const cats = collectCollectionCategories(posts, collections, c.key);
|
|
162
|
+
if (!cats.length) return [];
|
|
163
|
+
const base = `${siteUrl}${trimSlash(c.basePath)}`;
|
|
164
|
+
return [
|
|
165
|
+
{ url: `${base}/categories` },
|
|
166
|
+
...cats.map((category) => ({
|
|
167
|
+
url: `${base}/categories/${category.slug}`
|
|
168
|
+
}))
|
|
169
|
+
];
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const categories = collectCategories(posts);
|
|
173
|
+
if (!categories.length) return [];
|
|
174
|
+
const blogBase = config.blogBasePath ?? "/blog";
|
|
175
|
+
return [
|
|
176
|
+
{ url: `${siteUrl}${blogBase}/categories` },
|
|
177
|
+
...categories.map((category) => ({
|
|
178
|
+
url: `${siteUrl}${blogBase}/categories/${category.slug}`
|
|
179
|
+
}))
|
|
180
|
+
];
|
|
181
|
+
}
|
|
83
182
|
function createSitemap(config, staticRoutes = []) {
|
|
84
183
|
return async function sitemap() {
|
|
85
184
|
const siteUrl = trimSlash(config.siteUrl);
|
|
86
|
-
const blogBase = config.blogBasePath ?? "/blog";
|
|
87
185
|
const collections = await resolveCollections(config.collections);
|
|
88
186
|
const posts = await safeFetchPosts(config, 200);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
priority: 0.6
|
|
99
|
-
}
|
|
100
|
-
];
|
|
101
|
-
});
|
|
102
|
-
const categoryEntries2 = collections.filter((c) => c.archives).flatMap((c) => {
|
|
103
|
-
const cats = collectCollectionCategories(posts, collections, c.key);
|
|
104
|
-
if (!cats.length) return [];
|
|
105
|
-
const base = `${siteUrl}${trimSlash(c.basePath)}`;
|
|
106
|
-
return [
|
|
107
|
-
{ url: `${base}/categories`, changeFrequency: "weekly", priority: 0.5 },
|
|
108
|
-
...cats.map((category) => ({
|
|
109
|
-
url: `${base}/categories/${category.slug}`,
|
|
110
|
-
changeFrequency: "weekly",
|
|
111
|
-
priority: 0.5
|
|
112
|
-
}))
|
|
113
|
-
];
|
|
114
|
-
});
|
|
115
|
-
return [...staticRoutes, ...postEntries2, ...categoryEntries2];
|
|
116
|
-
}
|
|
117
|
-
const postEntries = posts.map((p) => ({
|
|
118
|
-
url: `${siteUrl}${blogBase}/${p.slug}`,
|
|
119
|
-
lastModified: p.updatedAt ?? p.publishedAt ?? void 0,
|
|
120
|
-
changeFrequency: "weekly",
|
|
121
|
-
priority: 0.6
|
|
122
|
-
}));
|
|
123
|
-
const categories = collectCategories(posts);
|
|
124
|
-
const categoryEntries = categories.length ? [
|
|
125
|
-
{
|
|
126
|
-
url: `${siteUrl}${blogBase}/categories`,
|
|
127
|
-
changeFrequency: "weekly",
|
|
128
|
-
priority: 0.5
|
|
129
|
-
},
|
|
130
|
-
...categories.map((category) => ({
|
|
131
|
-
url: `${siteUrl}${blogBase}/categories/${category.slug}`,
|
|
132
|
-
changeFrequency: "weekly",
|
|
133
|
-
priority: 0.5
|
|
134
|
-
}))
|
|
135
|
-
] : [];
|
|
136
|
-
return [...staticRoutes, ...postEntries, ...categoryEntries];
|
|
187
|
+
const settings = await resolveSitemapSettings(config, {
|
|
188
|
+
skipRemoteSettings: true,
|
|
189
|
+
blogImages: false
|
|
190
|
+
});
|
|
191
|
+
return [
|
|
192
|
+
...staticEntries(staticRoutes),
|
|
193
|
+
...postEntries(config, siteUrl, posts, collections, settings),
|
|
194
|
+
...categoryEntries(config, siteUrl, posts, collections)
|
|
195
|
+
];
|
|
137
196
|
};
|
|
138
197
|
}
|
|
198
|
+
function createSitemapIndex(config, staticRoutes = [], opts = {}) {
|
|
199
|
+
async function generateSitemaps() {
|
|
200
|
+
const settings = await resolveSitemapSettings(config, opts);
|
|
201
|
+
const ids = ["static", "blog", "categories"];
|
|
202
|
+
if (settings.authors) ids.push("authors");
|
|
203
|
+
return ids.map((id) => ({ id }));
|
|
204
|
+
}
|
|
205
|
+
async function sitemap(props) {
|
|
206
|
+
const id = await props.id;
|
|
207
|
+
const siteUrl = trimSlash(config.siteUrl);
|
|
208
|
+
const settings = await resolveSitemapSettings(config, opts);
|
|
209
|
+
const localize = (entries) => localizeEntries(entries, siteUrl, settings.locales);
|
|
210
|
+
if (id === "static") return localize(staticEntries(staticRoutes));
|
|
211
|
+
if (id === "authors") {
|
|
212
|
+
if (!settings.authors) return [];
|
|
213
|
+
const authors = await safeFetchAuthors(config);
|
|
214
|
+
const base = trimSlash(opts.authorBasePath ?? "/blog/author");
|
|
215
|
+
return localize(
|
|
216
|
+
authors.map((a) => ({
|
|
217
|
+
url: `${siteUrl}${base}/${encodeURIComponent(a.slug)}`,
|
|
218
|
+
lastModified: a.lastUpdatedAt ?? void 0
|
|
219
|
+
}))
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const collections = await resolveCollections(config.collections);
|
|
223
|
+
const posts = await safeFetchPosts(config, 200);
|
|
224
|
+
if (id === "blog") {
|
|
225
|
+
return localize(postEntries(config, siteUrl, posts, collections, settings));
|
|
226
|
+
}
|
|
227
|
+
if (id === "categories") {
|
|
228
|
+
return localize(categoryEntries(config, siteUrl, posts, collections));
|
|
229
|
+
}
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
return { generateSitemaps, sitemap };
|
|
233
|
+
}
|
|
139
234
|
|
|
140
235
|
// src/routes-llms.ts
|
|
141
236
|
import { isInCollectionMode as isInCollectionMode2, resolvePostUrl as resolvePostUrl3 } from "@roottale/cms-core";
|
|
@@ -212,7 +307,17 @@ function createRevalidateRoute(config) {
|
|
|
212
307
|
if (!result.ok) {
|
|
213
308
|
return Response.json({ ok: false, reason: result.reason }, { status: 401 });
|
|
214
309
|
}
|
|
215
|
-
const INVARIANTS = [
|
|
310
|
+
const INVARIANTS = [
|
|
311
|
+
"/feed.xml",
|
|
312
|
+
"/llms.txt",
|
|
313
|
+
"/sitemap.xml",
|
|
314
|
+
"/sitemap/static.xml",
|
|
315
|
+
"/sitemap/blog.xml",
|
|
316
|
+
"/sitemap/categories.xml",
|
|
317
|
+
// 작가 사이트맵 — 글 발행/해제로 작가 등장·lastmod 가 바뀐다(authors 토글 off 면
|
|
318
|
+
// 빈 사이트맵이라 무효화해도 무해).
|
|
319
|
+
"/sitemap/authors.xml"
|
|
320
|
+
];
|
|
216
321
|
let alsoRevalidate;
|
|
217
322
|
let warning;
|
|
218
323
|
if (config.alsoRevalidate) {
|
|
@@ -506,7 +611,7 @@ function createFleetInfoRoute(config = {}) {
|
|
|
506
611
|
site: config.site ?? env.VERCEL_PROJECT_PRODUCTION_URL ?? null,
|
|
507
612
|
framework: "next",
|
|
508
613
|
cms_client: CMS_CLIENT_VERSION,
|
|
509
|
-
cms_renderer: true ? "0.
|
|
614
|
+
cms_renderer: true ? "0.37.0" : "dev",
|
|
510
615
|
build_sha: env.VERCEL_GIT_COMMIT_SHA?.slice(0, 7) ?? null
|
|
511
616
|
},
|
|
512
617
|
{ headers: { "cache-control": "public, max-age=300" } }
|
|
@@ -524,6 +629,7 @@ export {
|
|
|
524
629
|
createRedirectMiddleware,
|
|
525
630
|
createRevalidateRoute,
|
|
526
631
|
createSitemap,
|
|
632
|
+
createSitemapIndex,
|
|
527
633
|
matchRedirect,
|
|
528
634
|
normalizeRedirectPath,
|
|
529
635
|
postRedirectPath,
|
package/dist/routes.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/routes.ts","../src/routes-shared.ts","../src/routes-feed-sitemap.ts","../src/routes-llms.ts","../src/routes-revalidate.ts","../src/routes-og.ts","../src/routes-redirects.ts","../src/routes-metadata.ts"],"sourcesContent":["/**\n * `@roottale/cms-renderer-next/routes` — Next.js App Router 통합 라우트 팩토리.\n *\n * 외부 고객사 사이트가 매번 직접 작성하던 공통 라우트(RSS feed / sitemap /\n * revalidate webhook / llms.txt / OG 이미지)를 1줄 팩토리로 표준화. 블로그 조회와\n * *동일한* 단일 customer API key(`ROOTTALE_API_KEY`)를 사용한다.\n *\n * 설계 원칙:\n * - feed/sitemap/llms 는 fetch 실패를 swallow → 라우트가 *항상 200*.\n * - revalidate 는 서명 webhook 을 검증하고 본문 paths + 파생 경로를 revalidate.\n * - `next` 에 직접 의존하지 않음(revalidate 콜백·ImageResponse 주입).\n *\n * 구현은 `routes-*.ts` 모듈로 분리되어 있고 본 파일은 공개 배럴이다(파일당 ≤400줄).\n * 사용 예는 roottale-docs `services/external-site-integration.md` 참조.\n */\n\nimport { CMS_CLIENT_VERSION } from \"@roottale/cms-client/server\";\n\nimport { trimSlash } from \"./routes-shared.js\";\n\n// ── 공개 표면 배럴 (모듈별 구현 re-export) ───────────────────────────────────\nexport * from \"./routes-shared.js\";\nexport * from \"./routes-feed-sitemap.js\";\nexport * from \"./routes-llms.js\";\nexport * from \"./routes-revalidate.js\";\nexport * from \"./routes-og.js\";\n// 커스텀 리다이렉트 미들웨어 (WP RankMath Redirections 패리티).\nexport * from \"./routes-redirects.js\";\n// 글 메타데이터 빌더 (canonical/robots/og 1줄 — 어드민 SEO 패널 반영).\nexport * from \"./routes-metadata.js\";\n\n// ── slug 변경 301 (WP permalink 패리티) ─────────────────────────────────────\n\n/**\n * 옛 slug → 현재 slug 301 redirect 판단 (순수 함수, `next` 비의존).\n *\n * Public API 는 slug 변경 후에도 옛 slug 로 글을 찾아 *현재* slug 를 응답한다\n * (post_slug_history fallback). 요청 slug 와 응답 slug 가 다르면 새 경로를\n * 반환 — 페이지에서 `permanentRedirect()` 로 301 처리한다:\n *\n * ```ts\n * import { permanentRedirect, notFound } from \"next/navigation\";\n * import { postRedirectPath } from \"@roottale/cms-renderer-next/routes\";\n *\n * const post = await getPost(slug);\n * if (!post) notFound();\n * const redirect = postRedirectPath(post, slug);\n * if (redirect) permanentRedirect(redirect);\n * ```\n *\n * Next App Router 의 `params` 는 percent-encoding 된 채 들어올 수 있어\n * (한글 slug) 디코딩 후 비교한다. redirect 가 불필요하면 null.\n */\nexport function postRedirectPath(\n post: { slug: string },\n requestedSlug: string,\n blogBasePath = \"/blog\",\n): string | null {\n let requested = requestedSlug;\n try {\n requested = decodeURIComponent(requestedSlug);\n } catch {\n /* 잘못된 escape — 원본 그대로 비교 */\n }\n if (post.slug === requested) return null;\n // blogBasePath \"/\" 또는 \"\" = 루트 배치 → \"/{slug}\".\n return `${trimSlash(blogBasePath)}/${encodeURIComponent(post.slug)}`;\n}\n\n// ── fleet well-known (사이트 버전 노출 컨벤션) ───────────────────────────────\n\ndeclare const __CMS_RENDERER_VERSION__: string | undefined;\n\nexport interface FleetInfoRouteConfig {\n /** 사이트 식별 라벨 (선택 — 기본 Vercel project URL 또는 null). */\n site?: string;\n}\n\n/**\n * `GET /.well-known/roottale.json` — fleet 가시성 컨벤션 라우트.\n *\n * 플랫폼 internal fleet API 의 `?probe=1` 이 본 엔드포인트를 읽어 \"배포된\"\n * 패키지 버전을 트래픽 없이도 확인한다 (런타임 check-in 의 보조 신호).\n * 노출은 의도적으로 최소: 패키지 버전 + 짧은 build sha — 비밀 없음.\n *\n * 사용 (Next App Router):\n * ```ts\n * // app/.well-known/roottale.json/route.ts\n * import { createFleetInfoRoute } from \"@roottale/cms-renderer-next/routes\";\n * export const GET = createFleetInfoRoute();\n * ```\n */\nexport function createFleetInfoRoute(\n config: FleetInfoRouteConfig = {},\n): () => Promise<Response> {\n // dts 빌드는 node types 없이 돈다 — process 는 globalThis 경유로 안전 접근.\n const env =\n (globalThis as { process?: { env?: Record<string, string | undefined> } })\n .process?.env ?? {};\n return async function GET(): Promise<Response> {\n return Response.json(\n {\n site: config.site ?? env.VERCEL_PROJECT_PRODUCTION_URL ?? null,\n framework: \"next\",\n cms_client: CMS_CLIENT_VERSION,\n cms_renderer:\n typeof __CMS_RENDERER_VERSION__ === \"string\"\n ? __CMS_RENDERER_VERSION__\n : \"dev\",\n build_sha: env.VERCEL_GIT_COMMIT_SHA?.slice(0, 7) ?? null,\n },\n { headers: { \"cache-control\": \"public, max-age=300\" } },\n );\n };\n}\n","/**\n * `routes` 공통 — config 타입, collections resolve, fetch 헬퍼. feed/sitemap/llms/\n * revalidate/og 모듈이 공유한다(순환 import 회피). 공개 표면은 `routes.ts` 배럴.\n */\n\nimport { fetchPosts, type CmsPostContent } from \"@roottale/cms-client/server\";\nimport type { RouteCollection } from \"@roottale/cms-core\";\n\nexport type { RouteCollection };\n// 소비자 가드용 재export (`/blog/[slug]` 에서 스트림 누출 차단 등) — ADR-0060.\nexport { resolvePostCollection, resolvePostUrl } from \"@roottale/cms-core\";\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\". 레거시 단일 스트림 — `collections` 미사용 시. */\n blogBasePath?: string;\n /**\n * 다중 스트림 라우팅 (ADR-0060). **길이 > 0 이면 collection 모드** — feed/sitemap/\n * llms.txt 가 글을 소속 스트림 basePath 로 라우팅하고, 어느 스트림에도 안 속한 글은\n * 제외한다. 비우거나 미전달 = 레거시 단일 `blogBasePath`(/blog) 동작(하위호환).\n * 선언 순서 = 우선순위(first-wins).\n *\n * 배열 또는 **async resolver**(`() => Promise<RouteCollection[]>`)를 받는다 — 후자는\n * 사이트가 `@roottale/cms-client` `fetchCollections()` 로 admin 설정을 가져올 때 쓴다.\n * resolver 가 throw 하면 fail-soft(레거시 단일 /blog).\n */\n collections?: CollectionsInput;\n}\n\n/** collections 입력 — 정적 배열 또는 async resolver(admin 설정 fetch 용). */\nexport type CollectionsInput =\n | RouteCollection[]\n | (() => RouteCollection[] | Promise<RouteCollection[]>);\n\n/** resolve 결과 — `failed` 로 \"resolver throw\" 를 \"collections 미설정\" 과 구분한다. */\nexport async function resolveCollectionsResult(\n input: CollectionsInput | undefined,\n): Promise<{ collections: RouteCollection[] | undefined; failed: boolean }> {\n if (typeof input === \"function\") {\n try {\n return { collections: await input(), failed: false };\n } catch (err) {\n console.error(\n \"[@roottale/cms-renderer-next/routes] collections resolver 실패:\",\n err,\n );\n return { collections: undefined, failed: true };\n }\n }\n return { collections: input, failed: false };\n}\n\n/**\n * resolver 면 호출(실패 시 undefined → 레거시), 배열이면 그대로. GET 팩토리(feed/\n * sitemap/llms)는 always-200 철학상 실패를 레거시 /blog 로 흡수 — revalidate 만\n * `resolveCollectionsResult` 로 실패를 구분해 under-invalidation 을 막는다.\n */\nexport async function resolveCollections(\n input: CollectionsInput | undefined,\n): Promise<RouteCollection[] | undefined> {\n return (await resolveCollectionsResult(input)).collections;\n}\n\nexport function trimSlash(u: string): string {\n return u.replace(/\\/+$/, \"\");\n}\n\nexport async 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","/** `routes` — RSS feed + sitemap 팩토리. collection 모드/레거시 양쪽. */\n\nimport { generateRssXml, rssItemsFromPosts } from \"@roottale/cms-client/server\";\nimport {\n isInCollectionMode,\n resolvePostCollection,\n resolvePostUrl,\n} from \"@roottale/cms-core\";\n\nimport { collectCategories, collectCollectionCategories } from \"./blog-filter.js\";\nimport {\n type RootTaleSiteConfig,\n resolveCollections,\n safeFetchPosts,\n trimSlash,\n} from \"./routes-shared.js\";\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 collections = await resolveCollections(config.collections);\n const allPosts = await safeFetchPosts(config, 50);\n\n // collection 모드: /feed.xml = `feed:true` 스트림들의 합집합(aggregate). RSS\n // 리더는 안정적 사이트 피드를 기대하므로 스트림별 분리 대신 통합 피드(codex).\n // 각 글 링크는 소속 스트림 basePath 로. 레거시: 단일 blogBasePath.\n const inCollectionMode = isInCollectionMode(collections);\n const blogBase = config.blogBasePath ?? \"/blog\";\n const posts = inCollectionMode\n ? allPosts.filter((p) => {\n const c = resolvePostCollection(p, collections!);\n return c ? c.feed === true : false;\n })\n : allPosts;\n\n const xml = generateRssXml({\n title: config.title,\n link: inCollectionMode ? siteUrl : `${siteUrl}${blogBase}`,\n feedUrl: `${siteUrl}/feed.xml`,\n description: config.description,\n language: config.language,\n items: rssItemsFromPosts(posts, {\n postUrl: (p) =>\n inCollectionMode\n ? (resolvePostUrl(siteUrl, p, collections!) ?? `${siteUrl}${blogBase}/${p.slug}`)\n : `${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 collections = await resolveCollections(config.collections);\n const posts = await safeFetchPosts(config, 200);\n\n if (isInCollectionMode(collections)) {\n // 글마다 소속 스트림 basePath 로(공지→/notice, 블로그→/blog). 어느 스트림에도\n // 안 속한 글(상세 라우트 없음)은 제외. 아카이브는 `archives:true` 스트림만.\n const postEntries: SitemapEntry[] = posts.flatMap((p) => {\n const url = resolvePostUrl(siteUrl, p, collections!);\n if (!url) return [];\n return [\n {\n url,\n lastModified: p.updatedAt ?? p.publishedAt ?? undefined,\n changeFrequency: \"weekly\" as const,\n priority: 0.6,\n },\n ];\n });\n\n const categoryEntries: SitemapEntry[] = collections!\n .filter((c) => c.archives)\n .flatMap((c) => {\n const cats = collectCollectionCategories(posts, collections!, c.key);\n if (!cats.length) return [];\n const base = `${siteUrl}${trimSlash(c.basePath)}`;\n return [\n { url: `${base}/categories`, changeFrequency: \"weekly\" as const, priority: 0.5 },\n ...cats.map((category) => ({\n url: `${base}/categories/${category.slug}`,\n changeFrequency: \"weekly\" as const,\n priority: 0.5,\n })),\n ];\n });\n\n return [...staticRoutes, ...postEntries, ...categoryEntries];\n }\n\n // 레거시: 단일 blogBasePath.\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","/** `routes` — llms.txt (AEO/GEO, AI 검색·생성엔진용 마크다운 인덱스). */\n\nimport { isInCollectionMode, resolvePostUrl } from \"@roottale/cms-core\";\n\nimport {\n type RootTaleSiteConfig,\n resolveCollections,\n safeFetchPosts,\n trimSlash,\n} from \"./routes-shared.js\";\n\n/** llms.txt 정적 섹션의 링크 1개. */\nexport interface LlmsTxtLink {\n title: string;\n url: string;\n /** 링크 뒤 \": {note}\" 로 붙는 한 줄 설명 (선택). */\n note?: string;\n}\n\n/** llms.txt 의 정적 페이지 링크 그룹 — 블로그 목록 앞에 출력. */\nexport interface LlmsTxtSection {\n title: string;\n links: LlmsTxtLink[];\n}\n\nexport type LlmsTxtRouteConfig = RootTaleSiteConfig & {\n /** (선택) 서비스 소개 등 정적 페이지 링크 그룹. */\n sections?: LlmsTxtSection[];\n};\n\n/** 마크다운 링크 텍스트를 깨뜨리는 문자([, ], (, )) 제거. */\nfunction mdLinkSafe(text: string): string {\n return text.replace(/[[\\]()]/g, \"\").trim();\n}\n\n/** excerpt 를 1줄로 — 개행·연속 공백을 공백 1개로 정리 후 200자 절단. */\nfunction oneLine(text: string): string {\n return text.replace(/\\s+/g, \" \").trim().slice(0, 200);\n}\n\n/**\n * `app/llms.txt/route.ts` 용 GET 핸들러 — AEO/GEO(AI 검색·생성엔진 최적화).\n *\n * AI 크롤러(ChatGPT/Claude/Perplexity 등)가 사이트 구조·콘텐츠를 빠르게\n * 파악하도록 llms.txt 스펙(https://llmstxt.org)의 마크다운 인덱스를 제공한다.\n * 발행 글 최대 100개를 \"## 블로그\" 섹션으로 자동 포함 — feed/sitemap 처럼\n * fetch 실패도 swallow 하여 *항상 200* (헤더 부분만 출력).\n *\n * ```ts\n * import { createLlmsTxtRoute } from \"@roottale/cms-renderer-next/routes\";\n * export const dynamic = \"force-dynamic\";\n * export const GET = createLlmsTxtRoute({\n * apiKey: process.env.ROOTTALE_API_KEY!,\n * siteUrl: \"https://example.com\",\n * title: \"Example\",\n * description: \"...\",\n * sections: [{ title: \"주요 페이지\", links: [{ title: \"서비스\", url: \"https://example.com/services\" }] }],\n * });\n * ```\n */\nexport function createLlmsTxtRoute(\n config: LlmsTxtRouteConfig,\n): () => Promise<Response> {\n return async function GET(): Promise<Response> {\n const siteUrl = trimSlash(config.siteUrl);\n const blogBase = config.blogBasePath ?? \"/blog\";\n const collections = await resolveCollections(config.collections);\n const posts = await safeFetchPosts(config, 100);\n\n const lines: string[] = [`# ${config.title}`];\n if (config.description) {\n lines.push(\"\", `> ${oneLine(config.description)}`);\n }\n for (const section of config.sections ?? []) {\n lines.push(\"\", `## ${section.title}`);\n for (const link of section.links) {\n const note = link.note ? `: ${oneLine(link.note)}` : \"\";\n lines.push(`- [${mdLinkSafe(link.title)}](${link.url})${note}`);\n }\n }\n if (posts.length > 0) {\n const inCollectionMode = isInCollectionMode(collections);\n // 레거시 헤딩(\"## 블로그\")은 하위호환 유지, 다중 스트림은 \"## 콘텐츠\".\n lines.push(\"\", inCollectionMode ? \"## 콘텐츠\" : \"## 블로그\");\n for (const p of posts) {\n // collection 모드: 소속 스트림 URL, 미소속 글 제외. 레거시: 단일 blogBase.\n const url = inCollectionMode\n ? resolvePostUrl(siteUrl, p, collections!)\n : `${siteUrl}${blogBase}/${p.slug}`;\n if (!url) continue;\n const excerpt = p.excerpt ? `: ${oneLine(p.excerpt)}` : \"\";\n lines.push(`- [${mdLinkSafe(p.title)}](${url})${excerpt}`);\n }\n }\n lines.push(\"\");\n\n return new Response(lines.join(\"\\n\"), {\n headers: {\n \"content-type\": \"text/plain; charset=utf-8\",\n // feed 와 동일한 30분 캐시 — webhook alsoRevalidate 로 즉시 갱신됨.\n \"cache-control\": \"public, max-age=1800, s-maxage=1800\",\n },\n });\n };\n}\n","/** `routes` — revalidate webhook 핸들러 (서명 검증 + 파생 경로 재검증). */\n\nimport {\n verifyRootTaleWebhook,\n type VerifyRootTaleWebhookResult,\n} from \"@roottale/cms-client/webhook\";\nimport { isInCollectionMode } from \"@roottale/cms-core\";\n\nimport { type CollectionsInput, resolveCollectionsResult } from \"./routes-shared.js\";\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 * 다중 스트림 (ADR-0060). 지정 시 `alsoRevalidate` 기본값을 각 스트림 basePath\n * (+ `archives:true` 면 `${basePath}/categories`)로 파생. 미지정 = 레거시 단일 blog.\n * 배열 또는 async resolver(`fetchCollections()`) — resolver 실패 시 레거시로 fail-soft.\n */\n collections?: CollectionsInput;\n /**\n * 모든 글/분류/미디어 변경에 대해 *항상 함께* revalidate 할 파생 경로.\n * 피드·사이트맵·블로그 인덱스처럼 \"글 목록\"에 의존하는 경로 — admin 변경이\n * 자동 반영되려면 필수. 명시 시 collections 파생을 덮어쓴다.\n * 기본(레거시): [\"/feed.xml\", \"/sitemap.xml\", \"/llms.txt\", blogBase, `${blogBase}/categories`].\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\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 // 글 목록 의존 경로. 명시 alsoRevalidate 가 있으면 collections 를 resolve 하지 않는다\n // (불필요한 fetch/지연 방지 — codex#1). 없으면 요청 시 resolve 해 파생.\n // 항상 갱신하는 불변 경로 — feed/sitemap/llms 는 글 목록에 의존.\n const INVARIANTS = [\"/feed.xml\", \"/sitemap.xml\", \"/llms.txt\"];\n let alsoRevalidate: string[];\n let warning: string | undefined;\n if (config.alsoRevalidate) {\n alsoRevalidate = config.alsoRevalidate;\n } else {\n const { collections, failed } = await resolveCollectionsResult(config.collections);\n if (failed) {\n // resolver 실패: 스트림을 추측하지 않는다(잘못된 /blog 무효화 방지 — codex#2).\n // 불변 경로 + 본문 paths 만 갱신하고, 플랫폼이 재시도/알림하도록 warning 노출.\n alsoRevalidate = INVARIANTS;\n warning = \"collections_resolve_failed\";\n } else if (isInCollectionMode(collections)) {\n alsoRevalidate = [\n ...INVARIANTS,\n ...collections!.flatMap((c) =>\n c.archives ? [c.basePath, `${c.basePath}/categories`] : [c.basePath],\n ),\n ];\n } else {\n alsoRevalidate = [...INVARIANTS, blogBase, `${blogBase}/categories`];\n }\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 ...(warning ? { warning } : {}),\n });\n };\n}\n","/** `routes` — 동적 OG 이미지 (글별 1200×630, 한글 폰트 subset). */\n\nimport { fetchPost } from \"@roottale/cms-client/server\";\n\nimport { type RootTaleSiteConfig, trimSlash } from \"./routes-shared.js\";\n\n/** Next `opengraph-image` export 컨벤션과 호환되는 크기 상수. */\nexport const OG_IMAGE_SIZE = { width: 1200, height: 630 } as const;\nexport const OG_IMAGE_CONTENT_TYPE = \"image/png\";\n\nexport interface OgImageRouteConfig extends RootTaleSiteConfig {\n /** 배경색. 기본 진네이비 그라데이션 베이스. */\n backgroundColor?: string;\n /** 본문 텍스트 색. 기본 흰색. */\n foregroundColor?: string;\n /** 상단 액센트 바 + 브랜드 라벨 색. 기본 하늘색. */\n accentColor?: string;\n /** 하단 브랜드 라벨. 기본 = `title`. */\n brandLabel?: string;\n}\n\n/** `next/og` 의 ImageResponse 와 구조 호환(직접 의존 회피 — revalidate 콜백 주입과 동일 원칙). */\nexport interface ImageResponseLike {\n new (\n element: unknown,\n options?: {\n width?: number;\n height?: number;\n fonts?: { name: string; data: ArrayBuffer; weight?: number; style?: \"normal\" }[];\n },\n ): Response;\n}\n\n/**\n * 제목에 쓰인 글자만 부분셋(subset)으로 받아 한글을 렌더 — satori 기본 폰트는\n * 한글 글리프가 없다. 구형 UA 로 css2 를 호출하면 TTF(satori 호환) URL 을 준다.\n * 실패 시 null — 폰트 없이도 이미지는 생성(라틴 fallback).\n */\nasync function loadKoreanFontSubset(text: string): Promise<ArrayBuffer | null> {\n try {\n const unique = Array.from(new Set(Array.from(text))).join(\"\");\n const cssUrl = `https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@700&text=${encodeURIComponent(unique)}`;\n const cssRes = await fetch(cssUrl, {\n headers: { \"user-agent\": \"Mozilla/5.0 (Windows NT 6.1; rv:10.0)\" },\n });\n if (!cssRes.ok) return null;\n const css = await cssRes.text();\n const match = css.match(\n /src:\\s*url\\(([^)]+)\\)\\s*format\\(['\"](?:truetype|opentype)['\"]\\)/,\n );\n if (!match?.[1]) return null;\n const fontRes = await fetch(match[1]);\n if (!fontRes.ok) return null;\n return await fontRes.arrayBuffer();\n } catch (err) {\n console.error(\"[@roottale/cms-renderer-next/routes] OG 폰트 로딩 실패:\", err);\n return null;\n }\n}\n\n/**\n * `app/blog/[slug]/opengraph-image.tsx` 용 동적 OG 이미지 (1200×630).\n *\n * `next/og` 의 ImageResponse 를 주입받는다 — 본 패키지는 `next` 에 직접\n * 의존하지 않는다. 글이 없거나 fetch 실패면 사이트 제목만으로 fail-soft 렌더\n * (소셜 크롤러에 빈 카드 대신 브랜드 카드).\n *\n * ```tsx\n * // app/blog/[slug]/opengraph-image.tsx\n * import { ImageResponse } from \"next/og\";\n * import {\n * createPostOgImage,\n * OG_IMAGE_SIZE,\n * OG_IMAGE_CONTENT_TYPE,\n * } from \"@roottale/cms-renderer-next/routes\";\n * import { roottale } from \"@/lib/roottale\"; // RootTaleSiteConfig\n *\n * export const size = OG_IMAGE_SIZE;\n * export const contentType = OG_IMAGE_CONTENT_TYPE;\n * export default createPostOgImage(roottale, { ImageResponse });\n * ```\n */\nexport function createPostOgImage(\n config: OgImageRouteConfig,\n deps: { ImageResponse: ImageResponseLike },\n): (props: { params: Promise<{ slug: string }> | { slug: string } }) => Promise<Response> {\n const background = config.backgroundColor ?? \"#10172a\";\n const foreground = config.foregroundColor ?? \"#f8fafc\";\n const accent = config.accentColor ?? \"#38bdf8\";\n const brand = config.brandLabel ?? config.title;\n\n return async function OgImage(props): Promise<Response> {\n const { slug } = await props.params;\n\n let title = config.title;\n let dateLabel: string | null = null;\n try {\n const post = await fetchPost({\n apiKey: config.apiKey,\n baseUrl: config.apiBase,\n slugOrId: slug,\n });\n if (post) {\n title = post.title;\n dateLabel = new Intl.DateTimeFormat(\"ko-KR\", { dateStyle: \"long\" }).format(\n new Date(post.publishedAt),\n );\n }\n } catch (err) {\n console.error(\"[@roottale/cms-renderer-next/routes] OG fetchPost 실패:\", err);\n }\n\n // satori 줄바꿈에 맡기되 3줄을 넘는 초장문 제목만 자른다.\n const display = title.length > 64 ? `${title.slice(0, 63)}…` : title;\n const fontData = await loadKoreanFontSubset(`${display}${brand}${dateLabel ?? \"\"}`);\n\n const { createElement: h } = await import(\"react\");\n const element = h(\n \"div\",\n {\n style: {\n width: \"100%\",\n height: \"100%\",\n display: \"flex\",\n flexDirection: \"column\",\n justifyContent: \"space-between\",\n padding: \"72px 80px\",\n backgroundColor: background,\n backgroundImage: `linear-gradient(135deg, ${background} 60%, rgba(56,189,248,0.25))`,\n color: foreground,\n fontFamily: fontData ? \"NotoSansKR\" : \"sans-serif\",\n },\n },\n h(\"div\", {\n style: { width: 96, height: 10, backgroundColor: accent, borderRadius: 5 },\n }),\n h(\n \"div\",\n {\n style: {\n display: \"flex\",\n fontSize: display.length > 36 ? 56 : 68,\n fontWeight: 700,\n lineHeight: 1.25,\n letterSpacing: \"-0.02em\",\n wordBreak: \"keep-all\",\n },\n },\n display,\n ),\n h(\n \"div\",\n {\n style: {\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n fontSize: 28,\n },\n },\n h(\"div\", { style: { display: \"flex\", color: accent, fontWeight: 700 } }, brand),\n h(\n \"div\",\n { style: { display: \"flex\", color: \"rgba(248,250,252,0.72)\" } },\n dateLabel ?? trimSlash(config.siteUrl).replace(/^https?:\\/\\//, \"\"),\n ),\n ),\n );\n\n return new deps.ImageResponse(element, {\n width: OG_IMAGE_SIZE.width,\n height: OG_IMAGE_SIZE.height,\n ...(fontData\n ? { fonts: [{ name: \"NotoSansKR\", data: fontData, weight: 700 as const, style: \"normal\" as const }] }\n : {}),\n });\n };\n}\n","/**\n * 커스텀 리다이렉트 미들웨어 — 고객 Next 앱의 `middleware.ts` 에서 마운트.\n *\n * admin \"설정 > 주소 이동\" 에서 정의한 임의 경로 규칙(`/old → /new`)을 공개\n * API(`GET /v1/cms/public/redirects`)로 읽어 요청 경로에 매칭, 301/302 를\n * 발행한다. 슬러그 301(`postRedirectPath`)이 글 라우트에서 도는 것과 달리,\n * 임의 경로는 라우팅 *이전* 단계인 middleware 에서만 가로챌 수 있다.\n *\n * `next` 에 직접 의존하지 않는다(패키지 정책): 입력은 표준 `Request`(NextRequest\n * 가 상속), 출력은 표준 `Response`(NextResponse 가 상속)다.\n *\n * 사용 (Next App Router):\n * ```ts\n * // middleware.ts\n * import { NextResponse } from \"next/server\";\n * import { createRedirectMiddleware } from \"@roottale/cms-renderer-next/routes\";\n *\n * const redirects = createRedirectMiddleware({\n * apiKey: process.env.ROOTTALE_API_KEY!,\n * });\n *\n * export async function middleware(req: Request) {\n * return (await redirects(req)) ?? NextResponse.next();\n * }\n *\n * // Next 내부·정적 자산만 제외. `.*\\\\..*` 처럼 점 포함 경로를 전부 막으면\n * // `/old.html`·`/foo.php` 같은 레거시 마이그레이션 리다이렉트가 동작하지\n * // 않으므로, 알려진 자산 확장자만 제외한다.\n * export const config = {\n * matcher: [\n * \"/((?!_next/|api/|.*\\\\.(?:ico|png|jpg|jpeg|gif|svg|webp|css|js|txt|xml|json|woff2?|map)$).*)\",\n * ],\n * };\n * ```\n */\n\nimport { fetchRedirects } from \"@roottale/cms-client/server\";\nimport type { RootTaleRedirect } from \"@roottale/cms-client/server\";\n\nexport interface RedirectMiddlewareConfig {\n /** `rtlk_cust_*` — 블로그 조회와 같은 키. 서버에서만. */\n apiKey: string;\n /** API base 오버라이드. 기본 `https://api.roottale.com`. */\n apiBase?: string;\n /** site_id 명시(멀티사이트 키). 기본 = 키의 기본 사이트. */\n siteId?: string;\n /** 규칙 캐시 TTL(ms). 기본 60_000 (1분). */\n cacheTtlMs?: number;\n}\n\n/**\n * 경로 정규화 — 앞 슬래시 보장, 뒤 슬래시 제거(루트 제외), 쿼리/해시 제거,\n * percent-decode. 출발 경로 비교·한글 slug 매칭·루프 탐지에 요청·규칙 양쪽에\n * 동일 적용한다. 쿼리 제거는 `/a → /a?x` 류의 무한 루프 방지에 중요.\n */\nexport function normalizeRedirectPath(path: string): string {\n let p = path;\n try {\n p = decodeURIComponent(path);\n } catch {\n /* 잘못된 escape — 원본 유지 */\n }\n p = p.split(/[?#]/)[0] ?? p; // 쿼리·해시 제거\n if (!p.startsWith(\"/\")) p = `/${p}`;\n if (p.length > 1) p = p.replace(/\\/+$/, \"\");\n return p === \"\" ? \"/\" : p;\n}\n\nfunction isAbsoluteUrl(target: string): boolean {\n return /^https?:\\/\\//i.test(target);\n}\n\n/**\n * 요청 경로에 맞는 규칙을 찾는다(순수 함수, fetch 비의존 — 테스트 용이).\n * 정확 경로 일치만(와일드카드 없음).\n *\n * 내부 경로 체인을 따라가 최종 도착지를 한 번에 반환한다(`/a→/b→/c` ⇒ `/c`):\n * 리다이렉트 체인을 줄여 SEO·왕복을 개선하고, 한 호출 안에서의 무한 루프를\n * 막는다. 도착지가 출발지로 정규화되면(쿼리만 다른 경우 포함) 자기 루프로 보고\n * 매칭하지 않는다. 절대 URL 도착지는 체인 종단(다른 origin 위임). 상태 코드는\n * 실제 요청 경로에 매칭된 첫 규칙을 따른다. null = 해당 없음.\n */\nexport function matchRedirect(\n redirects: readonly RootTaleRedirect[],\n pathname: string,\n): { toTarget: string; statusCode: 301 | 302 } | null {\n const byFrom = new Map<string, RootTaleRedirect>();\n for (const r of redirects) {\n const key = normalizeRedirectPath(r.fromPath);\n if (!byFrom.has(key)) byFrom.set(key, r);\n }\n\n const start = normalizeRedirectPath(pathname);\n const visited = new Set<string>([start]);\n let current = start;\n let finalTarget: string | null = null;\n let firstStatus: 301 | 302 | null = null;\n\n for (let hop = 0; hop < 10; hop++) {\n const rule = byFrom.get(current);\n if (!rule) break;\n if (hop === 0) firstStatus = rule.statusCode;\n\n if (isAbsoluteUrl(rule.toTarget)) {\n finalTarget = rule.toTarget; // 절대 URL = 체인 종단\n break;\n }\n const nextPath = normalizeRedirectPath(rule.toTarget);\n if (nextPath === start) {\n // 시작점으로 복귀 = 루프. 첫 홉이면 자기 루프(매칭 안 함), 아니면 직전\n // finalTarget 까지만 적용해 한 호출 안에서의 무한 루프를 끊는다.\n if (hop === 0) return null;\n break;\n }\n finalTarget = rule.toTarget;\n if (visited.has(nextPath)) break; // 사이클 — 현재까지로 종료\n visited.add(nextPath);\n current = nextPath;\n }\n\n if (finalTarget === null || firstStatus === null) return null;\n return { toTarget: finalTarget, statusCode: firstStatus };\n}\n\ninterface CacheEntry {\n expires: number;\n value: RootTaleRedirect[];\n}\n\n/**\n * 미들웨어 팩토리. 규칙을 TTL 캐시하고(요청마다 API 호출 회피), fetch 실패는\n * swallow 해 트래픽을 절대 막지 않는다(stale 우선, 없으면 빈 목록 → 통과).\n *\n * 반환 함수: 매칭 시 redirect `Response`, 아니면 `null`(호출부가 통과 처리).\n */\nexport function createRedirectMiddleware(\n config: RedirectMiddlewareConfig,\n): (request: Request) => Promise<Response | null> {\n const ttl = config.cacheTtlMs ?? 60_000;\n let cache: CacheEntry | null = null;\n\n async function loadRedirects(now: number): Promise<RootTaleRedirect[]> {\n if (cache && cache.expires > now) return cache.value;\n try {\n const value = await fetchRedirects({\n apiKey: config.apiKey,\n baseUrl: config.apiBase,\n siteId: config.siteId,\n });\n cache = { expires: now + ttl, value };\n return value;\n } catch (err) {\n console.error(\n \"[@roottale/cms-renderer-next] fetchRedirects 실패 — 통과(fail-soft):\",\n err,\n );\n // stale 값이 있으면 그것으로, 없으면 빈 목록. 짧게 재시도 억제.\n if (cache) {\n cache = { expires: now + Math.min(ttl, 10_000), value: cache.value };\n return cache.value;\n }\n return [];\n }\n }\n\n return async function redirectMiddleware(\n request: Request,\n ): Promise<Response | null> {\n let pathname: string;\n let origin: string;\n try {\n const u = new URL(request.url);\n pathname = u.pathname;\n origin = u.origin;\n } catch {\n return null;\n }\n\n // 캐시 키가 stable 한 시각 — Date.now 는 미들웨어 런타임에서 허용.\n const now = Date.now();\n const redirects = await loadRedirects(now);\n const hit = matchRedirect(redirects, pathname);\n if (!hit) return null;\n\n // 내부 경로는 요청 origin 기준 절대화, 절대 URL 은 그대로.\n const dest = new URL(hit.toTarget, origin);\n // 같은 origin·같은 경로로의 redirect 는 무한 루프 — 통과시킨다. matchRedirect\n // 의 순수 루프 가드가 못 잡는 절대 URL(같은 origin) 자기참조까지 여기서 차단.\n if (\n dest.origin === origin &&\n normalizeRedirectPath(dest.pathname) === normalizeRedirectPath(pathname)\n ) {\n return null;\n }\n return Response.redirect(dest.toString(), hit.statusCode);\n };\n}\n","/**\n * `@roottale/cms-renderer-next/routes` — 블로그 글 메타데이터 빌더.\n *\n * 어드민 SEO 패널(`metaJson.seo`) 값을 Next.js `Metadata` 형태로 1줄 변환한다.\n * 고객사 `app/blog/[slug]/page.tsx` 의 `generateMetadata` 에서 매번 직접 쓰던\n * canonical/robots/openGraph 분기 보일러플레이트를 표준화 — 한 곳이라도\n * 빠뜨려 SEO 가 새는 사고(noindex 누락·canonical 미설정)를 방지한다.\n *\n * 설계 원칙(패키지 공통): `next` 에 직접 의존하지 않는다. 반환은 Next `Metadata`\n * 의 구조적 부분집합인 plain 객체이므로, 소비자는 `generateMetadata` 에서 그대로\n * `return` 하면 된다.\n */\n\nimport { resolvePostPath, type RouteCollection } from \"@roottale/cms-core\";\n\nimport { trimSlash } from \"./routes-shared.js\";\n\n/** 어드민 SEO 패널 override (`metaJson.seo`). 전부 선택. */\nexport interface PostSeoOverrides {\n /** 검색 제목 — 비면 글 제목 사용. */\n title?: string;\n /** 메타 설명 — 비면 글 요약 사용. */\n description?: string;\n /** 대표 URL. 비면 `siteUrl`+`path` 로 self-canonical 생성(옵션 제공 시). */\n canonical?: string;\n /** OG 이미지 — 비면 글 대표 이미지 사용. */\n ogImage?: string;\n /** 색인 제외. */\n noindex?: boolean;\n /** 링크 추적 제외. */\n nofollow?: boolean;\n}\n\n/** `buildPostMetadata` 입력 — `BlogPostMeta`·원본 post 모두 구조적으로 호환. */\nexport interface PostMetadataInput {\n /** 글 제목 (`seo.title` 없을 때 fallback). */\n title: string;\n /** 글 요약/설명 (`seo.description` 없을 때 fallback). */\n description?: string | null;\n /** 발행 ISO 일시 → `openGraph.publishedTime`. */\n date?: string | null;\n /** 대표 이미지 URL → `openGraph.images` (`seo.ogImage` 우선). */\n image?: string | null;\n /** 어드민 SEO 패널 override (`metaJson.seo`). */\n seo?: PostSeoOverrides | null;\n /** 글 slug — `collections` 기반 canonical 자동 해석에 사용. */\n slug?: string;\n /**\n * 소속 섹션 key (ADR-0060). `collections` 와 함께 주면 canonical basePath 를\n * 공지/블로그로 구분해 해석한다(`collectionKey:\"notice\"` → `/notice`).\n */\n collectionKey?: string | null;\n}\n\nexport interface BuildPostMetadataOptions {\n /**\n * self-canonical 기본값 origin (예: `https://example.com`).\n * `seo.canonical` override 가 있으면 무시된다. `path` 또는 `collections` 와\n * 함께 줄 때만 동작.\n */\n siteUrl?: string;\n /**\n * self-canonical 기본값 경로 (예: `/blog/my-post`). `siteUrl` 과 함께.\n * 명시하면 `collections` 파생보다 우선한다(단일 라우트 사이트용 명시 경로).\n */\n path?: string;\n /**\n * 섹션 정의 (ADR-0060). `path` 를 안 주면 글의 `collectionKey` 로 소속 섹션\n * basePath 를 찾아 canonical 경로를 **공지/블로그로 구분해 자동 해석**한다\n * (공지 글 → `/notice/{slug}`, 블로그 글 → `/blog/{slug}`). 어느 섹션에도\n * 안 속하면(미설정·미존재 key) canonical 을 생략한다. `post.slug` 필요.\n */\n collections?: readonly RouteCollection[];\n}\n\n/** Next `Metadata` 의 구조적 부분집합 — 소비자는 그대로 return 가능. */\nexport interface PostMetadata {\n title: string;\n description?: string;\n alternates?: { canonical: string };\n robots?: { index: boolean; follow: boolean };\n openGraph: {\n type: \"article\";\n title: string;\n description?: string;\n url?: string;\n publishedTime?: string;\n images?: { url: string; alt: string }[];\n };\n}\n\n/**\n * 글 메타데이터를 Next `Metadata` 형태로 빌드.\n *\n * - 제목/설명/OG 이미지는 어드민 SEO override 가 있으면 우선, 없으면 글 값 fallback.\n * - canonical: `seo.canonical` override → 없으면 `siteUrl`+`path`(명시) → 없으면\n * `siteUrl`+`collections`(글의 `collectionKey` 로 공지/블로그 basePath 자동 해석)\n * → 없으면 생략. 모든 글이 자기 자신을 가리키는 canonical 을 갖는 게 권장이므로\n * `siteUrl` 과 함께 `path` 또는 `collections` 전달을 권장한다.\n * - `noindex`/`nofollow` 중 하나라도 켜지면 `robots` 출력.\n *\n * ```ts\n * // 단일 블로그(레거시): 라우트가 곧 섹션이므로 path 를 직접 준다.\n * return buildPostMetadata(post, {\n * siteUrl: process.env.NEXT_PUBLIC_SITE_URL,\n * path: `/blog/${post.slug}`,\n * });\n *\n * // 다중 스트림(ADR-0060): collections 를 주면 공지/블로그를 글의\n * // collectionKey 로 구분해 canonical 을 자동 해석한다(공지→/notice, 블로그→/blog).\n * return buildPostMetadata(post, {\n * siteUrl: process.env.NEXT_PUBLIC_SITE_URL,\n * collections: COLLECTIONS,\n * });\n * ```\n */\nexport function buildPostMetadata(\n post: PostMetadataInput,\n options: BuildPostMetadataOptions = {},\n): PostMetadata {\n const seo = post.seo ?? undefined;\n const title = seo?.title || post.title;\n const description = seo?.description || post.description || undefined;\n const ogImage = seo?.ogImage || post.image || undefined;\n\n let canonical = seo?.canonical || undefined;\n if (!canonical && options.siteUrl) {\n // 경로 우선순위: 명시 path → collections 로 글의 섹션 basePath 자동 해석(공지/블로그).\n let path = options.path;\n if (!path && options.collections && post.slug) {\n path =\n resolvePostPath(\n { slug: post.slug, terms: [], collectionKey: post.collectionKey ?? null },\n options.collections,\n ) ?? undefined;\n }\n if (path) {\n const normalized = path.startsWith(\"/\") ? path : `/${path}`;\n canonical = `${trimSlash(options.siteUrl)}${normalized}`;\n }\n }\n\n const openGraph: PostMetadata[\"openGraph\"] = { type: \"article\", title };\n if (description) openGraph.description = description;\n if (canonical) openGraph.url = canonical;\n if (post.date) openGraph.publishedTime = post.date;\n if (ogImage) openGraph.images = [{ url: ogImage, alt: title }];\n\n const meta: PostMetadata = { title, openGraph };\n if (description) meta.description = description;\n if (canonical) meta.alternates = { canonical };\n if (seo?.noindex || seo?.nofollow) {\n meta.robots = { index: !seo.noindex, follow: !seo.nofollow };\n }\n return meta;\n}\n"],"mappings":"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","names":["resolvePostCollection","resolvePostUrl","resolvePostCollection","resolvePostUrl","postEntries","categoryEntries","isInCollectionMode","resolvePostUrl","isInCollectionMode","resolvePostUrl","isInCollectionMode","isInCollectionMode"]}
|
|
1
|
+
{"version":3,"sources":["../src/routes.ts","../src/routes-shared.ts","../src/routes-feed-sitemap.ts","../src/routes-llms.ts","../src/routes-revalidate.ts","../src/routes-og.ts","../src/routes-redirects.ts","../src/routes-metadata.ts"],"sourcesContent":["/**\n * `@roottale/cms-renderer-next/routes` — Next.js App Router 통합 라우트 팩토리.\n *\n * 외부 고객사 사이트가 매번 직접 작성하던 공통 라우트(RSS feed / sitemap /\n * revalidate webhook / llms.txt / OG 이미지)를 1줄 팩토리로 표준화. 블로그 조회와\n * *동일한* 단일 customer API key(`ROOTTALE_API_KEY`)를 사용한다.\n *\n * 설계 원칙:\n * - feed/sitemap/llms 는 fetch 실패를 swallow → 라우트가 *항상 200*.\n * - revalidate 는 서명 webhook 을 검증하고 본문 paths + 파생 경로를 revalidate.\n * - `next` 에 직접 의존하지 않음(revalidate 콜백·ImageResponse 주입).\n *\n * 구현은 `routes-*.ts` 모듈로 분리되어 있고 본 파일은 공개 배럴이다(파일당 ≤400줄).\n * 사용 예는 roottale-docs `services/external-site-integration.md` 참조.\n */\n\nimport { CMS_CLIENT_VERSION } from \"@roottale/cms-client/server\";\n\nimport { trimSlash } from \"./routes-shared.js\";\n\n// ── 공개 표면 배럴 (모듈별 구현 re-export) ───────────────────────────────────\nexport * from \"./routes-shared.js\";\nexport * from \"./routes-feed-sitemap.js\";\nexport * from \"./routes-llms.js\";\nexport * from \"./routes-revalidate.js\";\nexport * from \"./routes-og.js\";\n// 커스텀 리다이렉트 미들웨어 (WP RankMath Redirections 패리티).\nexport * from \"./routes-redirects.js\";\n// 글 메타데이터 빌더 (canonical/robots/og 1줄 — 어드민 SEO 패널 반영).\nexport * from \"./routes-metadata.js\";\n\n// ── slug 변경 301 (WP permalink 패리티) ─────────────────────────────────────\n\n/**\n * 옛 slug → 현재 slug 301 redirect 판단 (순수 함수, `next` 비의존).\n *\n * Public API 는 slug 변경 후에도 옛 slug 로 글을 찾아 *현재* slug 를 응답한다\n * (post_slug_history fallback). 요청 slug 와 응답 slug 가 다르면 새 경로를\n * 반환 — 페이지에서 `permanentRedirect()` 로 301 처리한다:\n *\n * ```ts\n * import { permanentRedirect, notFound } from \"next/navigation\";\n * import { postRedirectPath } from \"@roottale/cms-renderer-next/routes\";\n *\n * const post = await getPost(slug);\n * if (!post) notFound();\n * const redirect = postRedirectPath(post, slug);\n * if (redirect) permanentRedirect(redirect);\n * ```\n *\n * Next App Router 의 `params` 는 percent-encoding 된 채 들어올 수 있어\n * (한글 slug) 디코딩 후 비교한다. redirect 가 불필요하면 null.\n */\nexport function postRedirectPath(\n post: { slug: string },\n requestedSlug: string,\n blogBasePath = \"/blog\",\n): string | null {\n let requested = requestedSlug;\n try {\n requested = decodeURIComponent(requestedSlug);\n } catch {\n /* 잘못된 escape — 원본 그대로 비교 */\n }\n if (post.slug === requested) return null;\n // blogBasePath \"/\" 또는 \"\" = 루트 배치 → \"/{slug}\".\n return `${trimSlash(blogBasePath)}/${encodeURIComponent(post.slug)}`;\n}\n\n// ── fleet well-known (사이트 버전 노출 컨벤션) ───────────────────────────────\n\ndeclare const __CMS_RENDERER_VERSION__: string | undefined;\n\nexport interface FleetInfoRouteConfig {\n /** 사이트 식별 라벨 (선택 — 기본 Vercel project URL 또는 null). */\n site?: string;\n}\n\n/**\n * `GET /.well-known/roottale.json` — fleet 가시성 컨벤션 라우트.\n *\n * 플랫폼 internal fleet API 의 `?probe=1` 이 본 엔드포인트를 읽어 \"배포된\"\n * 패키지 버전을 트래픽 없이도 확인한다 (런타임 check-in 의 보조 신호).\n * 노출은 의도적으로 최소: 패키지 버전 + 짧은 build sha — 비밀 없음.\n *\n * 사용 (Next App Router):\n * ```ts\n * // app/.well-known/roottale.json/route.ts\n * import { createFleetInfoRoute } from \"@roottale/cms-renderer-next/routes\";\n * export const GET = createFleetInfoRoute();\n * ```\n */\nexport function createFleetInfoRoute(\n config: FleetInfoRouteConfig = {},\n): () => Promise<Response> {\n // dts 빌드는 node types 없이 돈다 — process 는 globalThis 경유로 안전 접근.\n const env =\n (globalThis as { process?: { env?: Record<string, string | undefined> } })\n .process?.env ?? {};\n return async function GET(): Promise<Response> {\n return Response.json(\n {\n site: config.site ?? env.VERCEL_PROJECT_PRODUCTION_URL ?? null,\n framework: \"next\",\n cms_client: CMS_CLIENT_VERSION,\n cms_renderer:\n typeof __CMS_RENDERER_VERSION__ === \"string\"\n ? __CMS_RENDERER_VERSION__\n : \"dev\",\n build_sha: env.VERCEL_GIT_COMMIT_SHA?.slice(0, 7) ?? null,\n },\n { headers: { \"cache-control\": \"public, max-age=300\" } },\n );\n };\n}\n","/**\n * `routes` 공통 — config 타입, collections resolve, fetch 헬퍼. feed/sitemap/llms/\n * revalidate/og 모듈이 공유한다(순환 import 회피). 공개 표면은 `routes.ts` 배럴.\n */\n\nimport { fetchPosts, type CmsPostContent } from \"@roottale/cms-client/server\";\nimport type { RouteCollection } from \"@roottale/cms-core\";\n\nexport type { RouteCollection };\n// 소비자 가드용 재export (`/blog/[slug]` 에서 스트림 누출 차단 등) — ADR-0060.\nexport { resolvePostCollection, resolvePostUrl } from \"@roottale/cms-core\";\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\". 레거시 단일 스트림 — `collections` 미사용 시. */\n blogBasePath?: string;\n /**\n * 다중 스트림 라우팅 (ADR-0060). **길이 > 0 이면 collection 모드** — feed/sitemap/\n * llms.txt 가 글을 소속 스트림 basePath 로 라우팅하고, 어느 스트림에도 안 속한 글은\n * 제외한다. 비우거나 미전달 = 레거시 단일 `blogBasePath`(/blog) 동작(하위호환).\n * 선언 순서 = 우선순위(first-wins).\n *\n * 배열 또는 **async resolver**(`() => Promise<RouteCollection[]>`)를 받는다 — 후자는\n * 사이트가 `@roottale/cms-client` `fetchCollections()` 로 admin 설정을 가져올 때 쓴다.\n * resolver 가 throw 하면 fail-soft(레거시 단일 /blog).\n */\n collections?: CollectionsInput;\n}\n\n/** collections 입력 — 정적 배열 또는 async resolver(admin 설정 fetch 용). */\nexport type CollectionsInput =\n | RouteCollection[]\n | (() => RouteCollection[] | Promise<RouteCollection[]>);\n\n/** resolve 결과 — `failed` 로 \"resolver throw\" 를 \"collections 미설정\" 과 구분한다. */\nexport async function resolveCollectionsResult(\n input: CollectionsInput | undefined,\n): Promise<{ collections: RouteCollection[] | undefined; failed: boolean }> {\n if (typeof input === \"function\") {\n try {\n return { collections: await input(), failed: false };\n } catch (err) {\n console.error(\n \"[@roottale/cms-renderer-next/routes] collections resolver 실패:\",\n err,\n );\n return { collections: undefined, failed: true };\n }\n }\n return { collections: input, failed: false };\n}\n\n/**\n * resolver 면 호출(실패 시 undefined → 레거시), 배열이면 그대로. GET 팩토리(feed/\n * sitemap/llms)는 always-200 철학상 실패를 레거시 /blog 로 흡수 — revalidate 만\n * `resolveCollectionsResult` 로 실패를 구분해 under-invalidation 을 막는다.\n */\nexport async function resolveCollections(\n input: CollectionsInput | undefined,\n): Promise<RouteCollection[] | undefined> {\n return (await resolveCollectionsResult(input)).collections;\n}\n\nexport function trimSlash(u: string): string {\n return u.replace(/\\/+$/, \"\");\n}\n\nexport async 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","/** `routes` — RSS feed + sitemap 팩토리. collection 모드/레거시 양쪽. */\n\nimport {\n type CmsPostContent,\n type RootTaleSitemapSettings,\n fetchAuthors,\n fetchBlogSettings,\n generateRssXml,\n rssItemsFromPosts,\n} from \"@roottale/cms-client/server\";\nimport {\n isInCollectionMode,\n resolvePostCollection,\n resolvePostUrl,\n} from \"@roottale/cms-core\";\n\nimport { collectCategories, collectCollectionCategories } from \"./blog-filter.js\";\nimport {\n type RootTaleSiteConfig,\n type RouteCollection,\n resolveCollections,\n safeFetchPosts,\n trimSlash,\n} from \"./routes-shared.js\";\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 collections = await resolveCollections(config.collections);\n const allPosts = await safeFetchPosts(config, 50);\n\n // collection 모드: /feed.xml = `feed:true` 스트림들의 합집합(aggregate). RSS\n // 리더는 안정적 사이트 피드를 기대하므로 스트림별 분리 대신 통합 피드(codex).\n // 각 글 링크는 소속 스트림 basePath 로. 레거시: 단일 blogBasePath.\n const inCollectionMode = isInCollectionMode(collections);\n const blogBase = config.blogBasePath ?? \"/blog\";\n const posts = inCollectionMode\n ? allPosts.filter((p) => {\n const c = resolvePostCollection(p, collections!);\n return c ? c.feed === true : false;\n })\n : allPosts;\n\n const xml = generateRssXml({\n title: config.title,\n link: inCollectionMode ? siteUrl : `${siteUrl}${blogBase}`,\n feedUrl: `${siteUrl}/feed.xml`,\n description: config.description,\n language: config.language,\n items: rssItemsFromPosts(posts, {\n postUrl: (p) =>\n inCollectionMode\n ? (resolvePostUrl(siteUrl, p, collections!) ?? `${siteUrl}${blogBase}/${p.slug}`)\n : `${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 /** @deprecated Google 이 무시 — emit 하지 않는다(blanche식 loc+lastmod). */\n changeFrequency?:\n | \"always\"\n | \"hourly\"\n | \"daily\"\n | \"weekly\"\n | \"monthly\"\n | \"yearly\"\n | \"never\";\n /** @deprecated Google 이 무시 — emit 하지 않는다. */\n priority?: number;\n /** `<image:image>` — 글 대표 이미지 등. Next `MetadataRoute.Sitemap` 호환. */\n images?: string[];\n /** hreflang 대체 언어 — 다국어(`alternates.languages`). Next 호환. */\n alternates?: { languages?: Record<string, string> };\n}\n\n// ── 사이트맵 빌더 (createSitemap / createSitemapIndex 공유) ───────────────────\n\nconst SITEMAP_DEFAULTS: RootTaleSitemapSettings = {\n blogImages: true,\n locales: [],\n authors: false,\n};\n\n/** `createSitemapIndex` 옵션 — admin 설정을 코드에서 덮어쓰거나 fetch 를 끌 때만. */\nexport interface SitemapIndexOptions {\n /** admin 의 `blogImages` 토글을 덮어쓴다. */\n blogImages?: boolean;\n /** admin 의 `locales` 를 덮어쓴다 (다국어, PR C). */\n locales?: string[];\n /** admin 의 `authors` 토글을 덮어쓴다 (작가 사이트맵). */\n authors?: boolean;\n /** 작가 아카이브 base path. 기본 \"/blog/author\" (→ /blog/author/{slug}). */\n authorBasePath?: string;\n /** true 면 admin 설정 fetch 생략(opts/기본값만). 빌드 시 네트워크 회피용. */\n skipRemoteSettings?: boolean;\n}\n\n/** 작가 fetch 실패 시 빈 목록 — 사이트맵은 항상 정상 응답(404 방지). */\nasync function safeFetchAuthors(\n config: RootTaleSiteConfig,\n): Promise<Awaited<ReturnType<typeof fetchAuthors>>> {\n try {\n return await fetchAuthors({ apiKey: config.apiKey, baseUrl: config.apiBase });\n } catch (err) {\n console.error(\"[@roottale/cms-renderer-next/routes] fetchAuthors 실패:\", err);\n return [];\n }\n}\n\n/** opts override > admin 설정(fetch) > 기본값. fetch 실패는 fail-soft. */\nasync function resolveSitemapSettings(\n config: RootTaleSiteConfig,\n opts: SitemapIndexOptions,\n): Promise<RootTaleSitemapSettings> {\n let remote: Partial<RootTaleSitemapSettings> = {};\n if (!opts.skipRemoteSettings) {\n try {\n // 구버전 cms-client(블로그 설정에 sitemap 없음)면 `?? {}` 로 fail-soft.\n remote =\n (\n await fetchBlogSettings({\n apiKey: config.apiKey,\n baseUrl: config.apiBase,\n })\n ).sitemap ?? {};\n } catch {\n /* fail-soft — 기본값으로 */\n }\n }\n return {\n blogImages: opts.blogImages ?? remote.blogImages ?? SITEMAP_DEFAULTS.blogImages,\n locales: opts.locales ?? remote.locales ?? SITEMAP_DEFAULTS.locales,\n authors: opts.authors ?? remote.authors ?? SITEMAP_DEFAULTS.authors,\n };\n}\n\n/**\n * 다국어 hreflang — `locales` 가 설정되면 각 entry 에 `alternates.languages` 를 단다.\n * 첫 로케일 = 기본(현 URL), 나머지 = path-prefix(`/ja/...`·`/en/...`). `x-default` =\n * 기본. **번역 콘텐츠 모델 전 단계의 스캐폴딩** — 소비자 사이트가 실제 per-locale\n * 라우트를 서빙해야 하고, slug 1:1(동일 slug + prefix)을 가정한다.\n */\nfunction localizeEntries(\n entries: SitemapEntry[],\n siteUrl: string,\n locales: string[],\n): SitemapEntry[] {\n const defaultLocale = locales[0];\n if (!defaultLocale) return entries;\n const rest = locales.slice(1);\n return entries.map((entry) => {\n // 정확히 siteUrl 이거나 그 하위 경로만 — `example.com.evil` 같은 형제 호스트 배제.\n if (entry.url !== siteUrl && !entry.url.startsWith(`${siteUrl}/`)) return entry;\n const path = entry.url.slice(siteUrl.length); // 선행 \"/\" 포함, 홈이면 \"\"\n const languages: Record<string, string> = {\n [defaultLocale]: entry.url,\n \"x-default\": entry.url,\n };\n for (const loc of rest) languages[loc] = `${siteUrl}/${loc}${path}`;\n return { ...entry, alternates: { languages } };\n });\n}\n\n/** 정적 경로 — changeFrequency/priority 는 떨군다(blanche식). 나머지는 보존. */\nfunction staticEntries(staticRoutes: SitemapEntry[]): SitemapEntry[] {\n return staticRoutes.map((r) => {\n const entry: SitemapEntry = { url: r.url };\n if (r.lastModified !== undefined) entry.lastModified = r.lastModified;\n if (r.images) entry.images = r.images;\n if (r.alternates) entry.alternates = r.alternates;\n return entry;\n });\n}\n\n/**\n * 발행 글 entry. collection 모드면 소속 스트림 basePath(`resolvePostUrl`), 레거시면\n * 단일 `/blog`. 어느 스트림에도 안 속한 글(상세 라우트 없음)은 제외. `blogImages`\n * 가 켜져 있으면 대표 이미지를 `<image:image>` 로 함께 색인.\n */\nfunction postEntries(\n config: RootTaleSiteConfig,\n siteUrl: string,\n posts: CmsPostContent[],\n collections: RouteCollection[] | undefined,\n settings: RootTaleSitemapSettings,\n): SitemapEntry[] {\n const inCollection = isInCollectionMode(collections);\n const blogBase = config.blogBasePath ?? \"/blog\";\n return posts.flatMap((p) => {\n const url = inCollection\n ? resolvePostUrl(siteUrl, p, collections!)\n : `${siteUrl}${blogBase}/${p.slug}`;\n if (!url) return [];\n const entry: SitemapEntry = {\n url,\n lastModified: p.updatedAt ?? p.publishedAt ?? undefined,\n };\n if (settings.blogImages && p.featuredImageUrl) {\n entry.images = [p.featuredImageUrl];\n }\n return [entry];\n });\n}\n\n/** 카테고리 아카이브 entry. collection 모드는 `archives:true` 스트림만. */\nfunction categoryEntries(\n config: RootTaleSiteConfig,\n siteUrl: string,\n posts: CmsPostContent[],\n collections: RouteCollection[] | undefined,\n): SitemapEntry[] {\n if (isInCollectionMode(collections)) {\n return collections!\n .filter((c) => c.archives)\n .flatMap((c) => {\n const cats = collectCollectionCategories(posts, collections!, c.key);\n if (!cats.length) return [];\n const base = `${siteUrl}${trimSlash(c.basePath)}`;\n return [\n { url: `${base}/categories` },\n ...cats.map((category) => ({\n url: `${base}/categories/${category.slug}`,\n })),\n ];\n });\n }\n const categories = collectCategories(posts);\n if (!categories.length) return [];\n const blogBase = config.blogBasePath ?? \"/blog\";\n return [\n { url: `${siteUrl}${blogBase}/categories` },\n ...categories.map((category) => ({\n url: `${siteUrl}${blogBase}/categories/${category.slug}`,\n })),\n ];\n}\n\n/**\n * @deprecated `createSitemapIndex` 를 쓰세요 — 단일 평면 사이트맵 대신 인덱스 분리\n * (static/blog/categories) + 이미지 + admin 토글을 지원한다. 본 함수는 하위호환용으로\n * 유지되며 `changeFrequency`/`priority` 는 더 이상 emit 하지 않는다(blanche식).\n *\n * `app/sitemap.ts` default export. 정적 경로 + 글 + 카테고리를 단일 파일로 합친다.\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 collections = await resolveCollections(config.collections);\n const posts = await safeFetchPosts(config, 200);\n // 레거시 동등성 유지 — admin 설정 fetch 생략 + 이미지 off(기존 동작 = freq/priority\n // 만 제거). 이미지·admin 토글이 필요하면 createSitemapIndex 를 쓴다(codex P2).\n const settings = await resolveSitemapSettings(config, {\n skipRemoteSettings: true,\n blogImages: false,\n });\n return [\n ...staticEntries(staticRoutes),\n ...postEntries(config, siteUrl, posts, collections, settings),\n ...categoryEntries(config, siteUrl, posts, collections),\n ];\n };\n}\n\n/** blanche식 인덱스 분리 사이트맵의 하위 섹션 id. */\nexport type SitemapSectionId = \"static\" | \"blog\" | \"categories\" | \"authors\";\n\n/**\n * blanche식 사이트맵 인덱스 — `/sitemap.xml`(인덱스) + `/sitemap/<id>.xml` 하위\n * 사이트맵(static/blog/categories)으로 분리한다(Next 16 `generateSitemaps`). 50k\n * URL/50MB 한도 확장 + 검색엔진이 섹션별로 크롤. 블로그 글에는 대표 이미지를\n * `<image:image>` 로 함께 색인(admin 토글). `changeFrequency`/`priority` 는 emit\n * 하지 않는다.\n *\n * ```ts\n * // app/sitemap.ts\n * import { createSitemapIndex } from \"@roottale/cms-renderer-next/routes\";\n * const { generateSitemaps, sitemap } = createSitemapIndex(\n * { apiKey: process.env.ROOTTALE_API_KEY!, siteUrl: \"https://example.com\", title: \"...\" },\n * [{ url: \"https://example.com\" }, { url: \"https://example.com/contact\" }],\n * );\n * export { generateSitemaps };\n * export default sitemap;\n * ```\n */\nexport function createSitemapIndex(\n config: RootTaleSiteConfig,\n staticRoutes: SitemapEntry[] = [],\n opts: SitemapIndexOptions = {},\n): {\n generateSitemaps: () => Promise<{ id: SitemapSectionId }[]>;\n sitemap: (props: { id: string | Promise<string> }) => Promise<SitemapEntry[]>;\n} {\n async function generateSitemaps(): Promise<{ id: SitemapSectionId }[]> {\n // static/blog/categories 항상. authors 는 admin 토글(settings.authors) 시만 —\n // 작가 아카이브 라우트가 있어야 404 가 안 난다. per-locale(PR C)은 후속.\n const settings = await resolveSitemapSettings(config, opts);\n const ids: SitemapSectionId[] = [\"static\", \"blog\", \"categories\"];\n if (settings.authors) ids.push(\"authors\");\n return ids.map((id) => ({ id }));\n }\n\n async function sitemap(props: {\n id: string | Promise<string>;\n }): Promise<SitemapEntry[]> {\n const id = await props.id;\n const siteUrl = trimSlash(config.siteUrl);\n const settings = await resolveSitemapSettings(config, opts);\n // 다국어 설정 시 모든 섹션 entry 에 hreflang alternates 를 단다(스캐폴딩).\n const localize = (entries: SitemapEntry[]): SitemapEntry[] =>\n localizeEntries(entries, siteUrl, settings.locales);\n\n if (id === \"static\") return localize(staticEntries(staticRoutes));\n\n if (id === \"authors\") {\n // generateSitemaps 와 동일하게 토글 gate — 직접 호출 시에도 off 면 빈 배열.\n if (!settings.authors) return [];\n const authors = await safeFetchAuthors(config);\n const base = trimSlash(opts.authorBasePath ?? \"/blog/author\");\n return localize(\n authors.map((a) => ({\n url: `${siteUrl}${base}/${encodeURIComponent(a.slug)}`,\n lastModified: a.lastUpdatedAt ?? undefined,\n })),\n );\n }\n\n const collections = await resolveCollections(config.collections);\n const posts = await safeFetchPosts(config, 200);\n if (id === \"blog\") {\n return localize(postEntries(config, siteUrl, posts, collections, settings));\n }\n if (id === \"categories\") {\n return localize(categoryEntries(config, siteUrl, posts, collections));\n }\n return [];\n }\n\n return { generateSitemaps, sitemap };\n}\n","/** `routes` — llms.txt (AEO/GEO, AI 검색·생성엔진용 마크다운 인덱스). */\n\nimport { isInCollectionMode, resolvePostUrl } from \"@roottale/cms-core\";\n\nimport {\n type RootTaleSiteConfig,\n resolveCollections,\n safeFetchPosts,\n trimSlash,\n} from \"./routes-shared.js\";\n\n/** llms.txt 정적 섹션의 링크 1개. */\nexport interface LlmsTxtLink {\n title: string;\n url: string;\n /** 링크 뒤 \": {note}\" 로 붙는 한 줄 설명 (선택). */\n note?: string;\n}\n\n/** llms.txt 의 정적 페이지 링크 그룹 — 블로그 목록 앞에 출력. */\nexport interface LlmsTxtSection {\n title: string;\n links: LlmsTxtLink[];\n}\n\nexport type LlmsTxtRouteConfig = RootTaleSiteConfig & {\n /** (선택) 서비스 소개 등 정적 페이지 링크 그룹. */\n sections?: LlmsTxtSection[];\n};\n\n/** 마크다운 링크 텍스트를 깨뜨리는 문자([, ], (, )) 제거. */\nfunction mdLinkSafe(text: string): string {\n return text.replace(/[[\\]()]/g, \"\").trim();\n}\n\n/** excerpt 를 1줄로 — 개행·연속 공백을 공백 1개로 정리 후 200자 절단. */\nfunction oneLine(text: string): string {\n return text.replace(/\\s+/g, \" \").trim().slice(0, 200);\n}\n\n/**\n * `app/llms.txt/route.ts` 용 GET 핸들러 — AEO/GEO(AI 검색·생성엔진 최적화).\n *\n * AI 크롤러(ChatGPT/Claude/Perplexity 등)가 사이트 구조·콘텐츠를 빠르게\n * 파악하도록 llms.txt 스펙(https://llmstxt.org)의 마크다운 인덱스를 제공한다.\n * 발행 글 최대 100개를 \"## 블로그\" 섹션으로 자동 포함 — feed/sitemap 처럼\n * fetch 실패도 swallow 하여 *항상 200* (헤더 부분만 출력).\n *\n * ```ts\n * import { createLlmsTxtRoute } from \"@roottale/cms-renderer-next/routes\";\n * export const dynamic = \"force-dynamic\";\n * export const GET = createLlmsTxtRoute({\n * apiKey: process.env.ROOTTALE_API_KEY!,\n * siteUrl: \"https://example.com\",\n * title: \"Example\",\n * description: \"...\",\n * sections: [{ title: \"주요 페이지\", links: [{ title: \"서비스\", url: \"https://example.com/services\" }] }],\n * });\n * ```\n */\nexport function createLlmsTxtRoute(\n config: LlmsTxtRouteConfig,\n): () => Promise<Response> {\n return async function GET(): Promise<Response> {\n const siteUrl = trimSlash(config.siteUrl);\n const blogBase = config.blogBasePath ?? \"/blog\";\n const collections = await resolveCollections(config.collections);\n const posts = await safeFetchPosts(config, 100);\n\n const lines: string[] = [`# ${config.title}`];\n if (config.description) {\n lines.push(\"\", `> ${oneLine(config.description)}`);\n }\n for (const section of config.sections ?? []) {\n lines.push(\"\", `## ${section.title}`);\n for (const link of section.links) {\n const note = link.note ? `: ${oneLine(link.note)}` : \"\";\n lines.push(`- [${mdLinkSafe(link.title)}](${link.url})${note}`);\n }\n }\n if (posts.length > 0) {\n const inCollectionMode = isInCollectionMode(collections);\n // 레거시 헤딩(\"## 블로그\")은 하위호환 유지, 다중 스트림은 \"## 콘텐츠\".\n lines.push(\"\", inCollectionMode ? \"## 콘텐츠\" : \"## 블로그\");\n for (const p of posts) {\n // collection 모드: 소속 스트림 URL, 미소속 글 제외. 레거시: 단일 blogBase.\n const url = inCollectionMode\n ? resolvePostUrl(siteUrl, p, collections!)\n : `${siteUrl}${blogBase}/${p.slug}`;\n if (!url) continue;\n const excerpt = p.excerpt ? `: ${oneLine(p.excerpt)}` : \"\";\n lines.push(`- [${mdLinkSafe(p.title)}](${url})${excerpt}`);\n }\n }\n lines.push(\"\");\n\n return new Response(lines.join(\"\\n\"), {\n headers: {\n \"content-type\": \"text/plain; charset=utf-8\",\n // feed 와 동일한 30분 캐시 — webhook alsoRevalidate 로 즉시 갱신됨.\n \"cache-control\": \"public, max-age=1800, s-maxage=1800\",\n },\n });\n };\n}\n","/** `routes` — revalidate webhook 핸들러 (서명 검증 + 파생 경로 재검증). */\n\nimport {\n verifyRootTaleWebhook,\n type VerifyRootTaleWebhookResult,\n} from \"@roottale/cms-client/webhook\";\nimport { isInCollectionMode } from \"@roottale/cms-core\";\n\nimport { type CollectionsInput, resolveCollectionsResult } from \"./routes-shared.js\";\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 * 다중 스트림 (ADR-0060). 지정 시 `alsoRevalidate` 기본값을 각 스트림 basePath\n * (+ `archives:true` 면 `${basePath}/categories`)로 파생. 미지정 = 레거시 단일 blog.\n * 배열 또는 async resolver(`fetchCollections()`) — resolver 실패 시 레거시로 fail-soft.\n */\n collections?: CollectionsInput;\n /**\n * 모든 글/분류/미디어 변경에 대해 *항상 함께* revalidate 할 파생 경로.\n * 피드·사이트맵·블로그 인덱스처럼 \"글 목록\"에 의존하는 경로 — admin 변경이\n * 자동 반영되려면 필수. 명시 시 collections 파생을 덮어쓴다.\n * 기본(레거시): [\"/feed.xml\", \"/llms.txt\", \"/sitemap.xml\", \"/sitemap/static.xml\",\n * \"/sitemap/blog.xml\", \"/sitemap/categories.xml\", blogBase, `${blogBase}/categories`].\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\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 // 글 목록 의존 경로. 명시 alsoRevalidate 가 있으면 collections 를 resolve 하지 않는다\n // (불필요한 fetch/지연 방지 — codex#1). 없으면 요청 시 resolve 해 파생.\n // 항상 갱신하는 불변 경로 — feed/sitemap/llms 는 글 목록에 의존. 사이트맵은\n // 인덱스(/sitemap.xml) + 분리된 하위(static/blog/categories) 모두 무효화해야\n // 글·이미지 토글 변경이 반영된다(createSitemapIndex — codex P1).\n const INVARIANTS = [\n \"/feed.xml\",\n \"/llms.txt\",\n \"/sitemap.xml\",\n \"/sitemap/static.xml\",\n \"/sitemap/blog.xml\",\n \"/sitemap/categories.xml\",\n // 작가 사이트맵 — 글 발행/해제로 작가 등장·lastmod 가 바뀐다(authors 토글 off 면\n // 빈 사이트맵이라 무효화해도 무해).\n \"/sitemap/authors.xml\",\n ];\n let alsoRevalidate: string[];\n let warning: string | undefined;\n if (config.alsoRevalidate) {\n alsoRevalidate = config.alsoRevalidate;\n } else {\n const { collections, failed } = await resolveCollectionsResult(config.collections);\n if (failed) {\n // resolver 실패: 스트림을 추측하지 않는다(잘못된 /blog 무효화 방지 — codex#2).\n // 불변 경로 + 본문 paths 만 갱신하고, 플랫폼이 재시도/알림하도록 warning 노출.\n alsoRevalidate = INVARIANTS;\n warning = \"collections_resolve_failed\";\n } else if (isInCollectionMode(collections)) {\n alsoRevalidate = [\n ...INVARIANTS,\n ...collections!.flatMap((c) =>\n c.archives ? [c.basePath, `${c.basePath}/categories`] : [c.basePath],\n ),\n ];\n } else {\n alsoRevalidate = [...INVARIANTS, blogBase, `${blogBase}/categories`];\n }\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 ...(warning ? { warning } : {}),\n });\n };\n}\n","/** `routes` — 동적 OG 이미지 (글별 1200×630, 한글 폰트 subset). */\n\nimport { fetchPost } from \"@roottale/cms-client/server\";\n\nimport { type RootTaleSiteConfig, trimSlash } from \"./routes-shared.js\";\n\n/** Next `opengraph-image` export 컨벤션과 호환되는 크기 상수. */\nexport const OG_IMAGE_SIZE = { width: 1200, height: 630 } as const;\nexport const OG_IMAGE_CONTENT_TYPE = \"image/png\";\n\nexport interface OgImageRouteConfig extends RootTaleSiteConfig {\n /** 배경색. 기본 진네이비 그라데이션 베이스. */\n backgroundColor?: string;\n /** 본문 텍스트 색. 기본 흰색. */\n foregroundColor?: string;\n /** 상단 액센트 바 + 브랜드 라벨 색. 기본 하늘색. */\n accentColor?: string;\n /** 하단 브랜드 라벨. 기본 = `title`. */\n brandLabel?: string;\n}\n\n/** `next/og` 의 ImageResponse 와 구조 호환(직접 의존 회피 — revalidate 콜백 주입과 동일 원칙). */\nexport interface ImageResponseLike {\n new (\n element: unknown,\n options?: {\n width?: number;\n height?: number;\n fonts?: { name: string; data: ArrayBuffer; weight?: number; style?: \"normal\" }[];\n },\n ): Response;\n}\n\n/**\n * 제목에 쓰인 글자만 부분셋(subset)으로 받아 한글을 렌더 — satori 기본 폰트는\n * 한글 글리프가 없다. 구형 UA 로 css2 를 호출하면 TTF(satori 호환) URL 을 준다.\n * 실패 시 null — 폰트 없이도 이미지는 생성(라틴 fallback).\n */\nasync function loadKoreanFontSubset(text: string): Promise<ArrayBuffer | null> {\n try {\n const unique = Array.from(new Set(Array.from(text))).join(\"\");\n const cssUrl = `https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@700&text=${encodeURIComponent(unique)}`;\n const cssRes = await fetch(cssUrl, {\n headers: { \"user-agent\": \"Mozilla/5.0 (Windows NT 6.1; rv:10.0)\" },\n });\n if (!cssRes.ok) return null;\n const css = await cssRes.text();\n const match = css.match(\n /src:\\s*url\\(([^)]+)\\)\\s*format\\(['\"](?:truetype|opentype)['\"]\\)/,\n );\n if (!match?.[1]) return null;\n const fontRes = await fetch(match[1]);\n if (!fontRes.ok) return null;\n return await fontRes.arrayBuffer();\n } catch (err) {\n console.error(\"[@roottale/cms-renderer-next/routes] OG 폰트 로딩 실패:\", err);\n return null;\n }\n}\n\n/**\n * `app/blog/[slug]/opengraph-image.tsx` 용 동적 OG 이미지 (1200×630).\n *\n * `next/og` 의 ImageResponse 를 주입받는다 — 본 패키지는 `next` 에 직접\n * 의존하지 않는다. 글이 없거나 fetch 실패면 사이트 제목만으로 fail-soft 렌더\n * (소셜 크롤러에 빈 카드 대신 브랜드 카드).\n *\n * ```tsx\n * // app/blog/[slug]/opengraph-image.tsx\n * import { ImageResponse } from \"next/og\";\n * import {\n * createPostOgImage,\n * OG_IMAGE_SIZE,\n * OG_IMAGE_CONTENT_TYPE,\n * } from \"@roottale/cms-renderer-next/routes\";\n * import { roottale } from \"@/lib/roottale\"; // RootTaleSiteConfig\n *\n * export const size = OG_IMAGE_SIZE;\n * export const contentType = OG_IMAGE_CONTENT_TYPE;\n * export default createPostOgImage(roottale, { ImageResponse });\n * ```\n */\nexport function createPostOgImage(\n config: OgImageRouteConfig,\n deps: { ImageResponse: ImageResponseLike },\n): (props: { params: Promise<{ slug: string }> | { slug: string } }) => Promise<Response> {\n const background = config.backgroundColor ?? \"#10172a\";\n const foreground = config.foregroundColor ?? \"#f8fafc\";\n const accent = config.accentColor ?? \"#38bdf8\";\n const brand = config.brandLabel ?? config.title;\n\n return async function OgImage(props): Promise<Response> {\n const { slug } = await props.params;\n\n let title = config.title;\n let dateLabel: string | null = null;\n try {\n const post = await fetchPost({\n apiKey: config.apiKey,\n baseUrl: config.apiBase,\n slugOrId: slug,\n });\n if (post) {\n title = post.title;\n dateLabel = new Intl.DateTimeFormat(\"ko-KR\", { dateStyle: \"long\" }).format(\n new Date(post.publishedAt),\n );\n }\n } catch (err) {\n console.error(\"[@roottale/cms-renderer-next/routes] OG fetchPost 실패:\", err);\n }\n\n // satori 줄바꿈에 맡기되 3줄을 넘는 초장문 제목만 자른다.\n const display = title.length > 64 ? `${title.slice(0, 63)}…` : title;\n const fontData = await loadKoreanFontSubset(`${display}${brand}${dateLabel ?? \"\"}`);\n\n const { createElement: h } = await import(\"react\");\n const element = h(\n \"div\",\n {\n style: {\n width: \"100%\",\n height: \"100%\",\n display: \"flex\",\n flexDirection: \"column\",\n justifyContent: \"space-between\",\n padding: \"72px 80px\",\n backgroundColor: background,\n backgroundImage: `linear-gradient(135deg, ${background} 60%, rgba(56,189,248,0.25))`,\n color: foreground,\n fontFamily: fontData ? \"NotoSansKR\" : \"sans-serif\",\n },\n },\n h(\"div\", {\n style: { width: 96, height: 10, backgroundColor: accent, borderRadius: 5 },\n }),\n h(\n \"div\",\n {\n style: {\n display: \"flex\",\n fontSize: display.length > 36 ? 56 : 68,\n fontWeight: 700,\n lineHeight: 1.25,\n letterSpacing: \"-0.02em\",\n wordBreak: \"keep-all\",\n },\n },\n display,\n ),\n h(\n \"div\",\n {\n style: {\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n fontSize: 28,\n },\n },\n h(\"div\", { style: { display: \"flex\", color: accent, fontWeight: 700 } }, brand),\n h(\n \"div\",\n { style: { display: \"flex\", color: \"rgba(248,250,252,0.72)\" } },\n dateLabel ?? trimSlash(config.siteUrl).replace(/^https?:\\/\\//, \"\"),\n ),\n ),\n );\n\n return new deps.ImageResponse(element, {\n width: OG_IMAGE_SIZE.width,\n height: OG_IMAGE_SIZE.height,\n ...(fontData\n ? { fonts: [{ name: \"NotoSansKR\", data: fontData, weight: 700 as const, style: \"normal\" as const }] }\n : {}),\n });\n };\n}\n","/**\n * 커스텀 리다이렉트 미들웨어 — 고객 Next 앱의 `middleware.ts` 에서 마운트.\n *\n * admin \"설정 > 주소 이동\" 에서 정의한 임의 경로 규칙(`/old → /new`)을 공개\n * API(`GET /v1/cms/public/redirects`)로 읽어 요청 경로에 매칭, 301/302 를\n * 발행한다. 슬러그 301(`postRedirectPath`)이 글 라우트에서 도는 것과 달리,\n * 임의 경로는 라우팅 *이전* 단계인 middleware 에서만 가로챌 수 있다.\n *\n * `next` 에 직접 의존하지 않는다(패키지 정책): 입력은 표준 `Request`(NextRequest\n * 가 상속), 출력은 표준 `Response`(NextResponse 가 상속)다.\n *\n * 사용 (Next App Router):\n * ```ts\n * // middleware.ts\n * import { NextResponse } from \"next/server\";\n * import { createRedirectMiddleware } from \"@roottale/cms-renderer-next/routes\";\n *\n * const redirects = createRedirectMiddleware({\n * apiKey: process.env.ROOTTALE_API_KEY!,\n * });\n *\n * export async function middleware(req: Request) {\n * return (await redirects(req)) ?? NextResponse.next();\n * }\n *\n * // Next 내부·정적 자산만 제외. `.*\\\\..*` 처럼 점 포함 경로를 전부 막으면\n * // `/old.html`·`/foo.php` 같은 레거시 마이그레이션 리다이렉트가 동작하지\n * // 않으므로, 알려진 자산 확장자만 제외한다.\n * export const config = {\n * matcher: [\n * \"/((?!_next/|api/|.*\\\\.(?:ico|png|jpg|jpeg|gif|svg|webp|css|js|txt|xml|json|woff2?|map)$).*)\",\n * ],\n * };\n * ```\n */\n\nimport { fetchRedirects } from \"@roottale/cms-client/server\";\nimport type { RootTaleRedirect } from \"@roottale/cms-client/server\";\n\nexport interface RedirectMiddlewareConfig {\n /** `rtlk_cust_*` — 블로그 조회와 같은 키. 서버에서만. */\n apiKey: string;\n /** API base 오버라이드. 기본 `https://api.roottale.com`. */\n apiBase?: string;\n /** site_id 명시(멀티사이트 키). 기본 = 키의 기본 사이트. */\n siteId?: string;\n /** 규칙 캐시 TTL(ms). 기본 60_000 (1분). */\n cacheTtlMs?: number;\n}\n\n/**\n * 경로 정규화 — 앞 슬래시 보장, 뒤 슬래시 제거(루트 제외), 쿼리/해시 제거,\n * percent-decode. 출발 경로 비교·한글 slug 매칭·루프 탐지에 요청·규칙 양쪽에\n * 동일 적용한다. 쿼리 제거는 `/a → /a?x` 류의 무한 루프 방지에 중요.\n */\nexport function normalizeRedirectPath(path: string): string {\n let p = path;\n try {\n p = decodeURIComponent(path);\n } catch {\n /* 잘못된 escape — 원본 유지 */\n }\n p = p.split(/[?#]/)[0] ?? p; // 쿼리·해시 제거\n if (!p.startsWith(\"/\")) p = `/${p}`;\n if (p.length > 1) p = p.replace(/\\/+$/, \"\");\n return p === \"\" ? \"/\" : p;\n}\n\nfunction isAbsoluteUrl(target: string): boolean {\n return /^https?:\\/\\//i.test(target);\n}\n\n/**\n * 요청 경로에 맞는 규칙을 찾는다(순수 함수, fetch 비의존 — 테스트 용이).\n * 정확 경로 일치만(와일드카드 없음).\n *\n * 내부 경로 체인을 따라가 최종 도착지를 한 번에 반환한다(`/a→/b→/c` ⇒ `/c`):\n * 리다이렉트 체인을 줄여 SEO·왕복을 개선하고, 한 호출 안에서의 무한 루프를\n * 막는다. 도착지가 출발지로 정규화되면(쿼리만 다른 경우 포함) 자기 루프로 보고\n * 매칭하지 않는다. 절대 URL 도착지는 체인 종단(다른 origin 위임). 상태 코드는\n * 실제 요청 경로에 매칭된 첫 규칙을 따른다. null = 해당 없음.\n */\nexport function matchRedirect(\n redirects: readonly RootTaleRedirect[],\n pathname: string,\n): { toTarget: string; statusCode: 301 | 302 } | null {\n const byFrom = new Map<string, RootTaleRedirect>();\n for (const r of redirects) {\n const key = normalizeRedirectPath(r.fromPath);\n if (!byFrom.has(key)) byFrom.set(key, r);\n }\n\n const start = normalizeRedirectPath(pathname);\n const visited = new Set<string>([start]);\n let current = start;\n let finalTarget: string | null = null;\n let firstStatus: 301 | 302 | null = null;\n\n for (let hop = 0; hop < 10; hop++) {\n const rule = byFrom.get(current);\n if (!rule) break;\n if (hop === 0) firstStatus = rule.statusCode;\n\n if (isAbsoluteUrl(rule.toTarget)) {\n finalTarget = rule.toTarget; // 절대 URL = 체인 종단\n break;\n }\n const nextPath = normalizeRedirectPath(rule.toTarget);\n if (nextPath === start) {\n // 시작점으로 복귀 = 루프. 첫 홉이면 자기 루프(매칭 안 함), 아니면 직전\n // finalTarget 까지만 적용해 한 호출 안에서의 무한 루프를 끊는다.\n if (hop === 0) return null;\n break;\n }\n finalTarget = rule.toTarget;\n if (visited.has(nextPath)) break; // 사이클 — 현재까지로 종료\n visited.add(nextPath);\n current = nextPath;\n }\n\n if (finalTarget === null || firstStatus === null) return null;\n return { toTarget: finalTarget, statusCode: firstStatus };\n}\n\ninterface CacheEntry {\n expires: number;\n value: RootTaleRedirect[];\n}\n\n/**\n * 미들웨어 팩토리. 규칙을 TTL 캐시하고(요청마다 API 호출 회피), fetch 실패는\n * swallow 해 트래픽을 절대 막지 않는다(stale 우선, 없으면 빈 목록 → 통과).\n *\n * 반환 함수: 매칭 시 redirect `Response`, 아니면 `null`(호출부가 통과 처리).\n */\nexport function createRedirectMiddleware(\n config: RedirectMiddlewareConfig,\n): (request: Request) => Promise<Response | null> {\n const ttl = config.cacheTtlMs ?? 60_000;\n let cache: CacheEntry | null = null;\n\n async function loadRedirects(now: number): Promise<RootTaleRedirect[]> {\n if (cache && cache.expires > now) return cache.value;\n try {\n const value = await fetchRedirects({\n apiKey: config.apiKey,\n baseUrl: config.apiBase,\n siteId: config.siteId,\n });\n cache = { expires: now + ttl, value };\n return value;\n } catch (err) {\n console.error(\n \"[@roottale/cms-renderer-next] fetchRedirects 실패 — 통과(fail-soft):\",\n err,\n );\n // stale 값이 있으면 그것으로, 없으면 빈 목록. 짧게 재시도 억제.\n if (cache) {\n cache = { expires: now + Math.min(ttl, 10_000), value: cache.value };\n return cache.value;\n }\n return [];\n }\n }\n\n return async function redirectMiddleware(\n request: Request,\n ): Promise<Response | null> {\n let pathname: string;\n let origin: string;\n try {\n const u = new URL(request.url);\n pathname = u.pathname;\n origin = u.origin;\n } catch {\n return null;\n }\n\n // 캐시 키가 stable 한 시각 — Date.now 는 미들웨어 런타임에서 허용.\n const now = Date.now();\n const redirects = await loadRedirects(now);\n const hit = matchRedirect(redirects, pathname);\n if (!hit) return null;\n\n // 내부 경로는 요청 origin 기준 절대화, 절대 URL 은 그대로.\n const dest = new URL(hit.toTarget, origin);\n // 같은 origin·같은 경로로의 redirect 는 무한 루프 — 통과시킨다. matchRedirect\n // 의 순수 루프 가드가 못 잡는 절대 URL(같은 origin) 자기참조까지 여기서 차단.\n if (\n dest.origin === origin &&\n normalizeRedirectPath(dest.pathname) === normalizeRedirectPath(pathname)\n ) {\n return null;\n }\n return Response.redirect(dest.toString(), hit.statusCode);\n };\n}\n","/**\n * `@roottale/cms-renderer-next/routes` — 블로그 글 메타데이터 빌더.\n *\n * 어드민 SEO 패널(`metaJson.seo`) 값을 Next.js `Metadata` 형태로 1줄 변환한다.\n * 고객사 `app/blog/[slug]/page.tsx` 의 `generateMetadata` 에서 매번 직접 쓰던\n * canonical/robots/openGraph 분기 보일러플레이트를 표준화 — 한 곳이라도\n * 빠뜨려 SEO 가 새는 사고(noindex 누락·canonical 미설정)를 방지한다.\n *\n * 설계 원칙(패키지 공통): `next` 에 직접 의존하지 않는다. 반환은 Next `Metadata`\n * 의 구조적 부분집합인 plain 객체이므로, 소비자는 `generateMetadata` 에서 그대로\n * `return` 하면 된다.\n */\n\nimport { resolvePostPath, type RouteCollection } from \"@roottale/cms-core\";\n\nimport { trimSlash } from \"./routes-shared.js\";\n\n/** 어드민 SEO 패널 override (`metaJson.seo`). 전부 선택. */\nexport interface PostSeoOverrides {\n /** 검색 제목 — 비면 글 제목 사용. */\n title?: string;\n /** 메타 설명 — 비면 글 요약 사용. */\n description?: string;\n /** 대표 URL. 비면 `siteUrl`+`path` 로 self-canonical 생성(옵션 제공 시). */\n canonical?: string;\n /** OG 이미지 — 비면 글 대표 이미지 사용. */\n ogImage?: string;\n /** 색인 제외. */\n noindex?: boolean;\n /** 링크 추적 제외. */\n nofollow?: boolean;\n}\n\n/** `buildPostMetadata` 입력 — `BlogPostMeta`·원본 post 모두 구조적으로 호환. */\nexport interface PostMetadataInput {\n /** 글 제목 (`seo.title` 없을 때 fallback). */\n title: string;\n /** 글 요약/설명 (`seo.description` 없을 때 fallback). */\n description?: string | null;\n /** 발행 ISO 일시 → `openGraph.publishedTime`. */\n date?: string | null;\n /** 대표 이미지 URL → `openGraph.images` (`seo.ogImage` 우선). */\n image?: string | null;\n /** 어드민 SEO 패널 override (`metaJson.seo`). */\n seo?: PostSeoOverrides | null;\n /** 글 slug — `collections` 기반 canonical 자동 해석에 사용. */\n slug?: string;\n /**\n * 소속 섹션 key (ADR-0060). `collections` 와 함께 주면 canonical basePath 를\n * 공지/블로그로 구분해 해석한다(`collectionKey:\"notice\"` → `/notice`).\n */\n collectionKey?: string | null;\n}\n\nexport interface BuildPostMetadataOptions {\n /**\n * self-canonical 기본값 origin (예: `https://example.com`).\n * `seo.canonical` override 가 있으면 무시된다. `path` 또는 `collections` 와\n * 함께 줄 때만 동작.\n */\n siteUrl?: string;\n /**\n * self-canonical 기본값 경로 (예: `/blog/my-post`). `siteUrl` 과 함께.\n * 명시하면 `collections` 파생보다 우선한다(단일 라우트 사이트용 명시 경로).\n */\n path?: string;\n /**\n * 섹션 정의 (ADR-0060). `path` 를 안 주면 글의 `collectionKey` 로 소속 섹션\n * basePath 를 찾아 canonical 경로를 **공지/블로그로 구분해 자동 해석**한다\n * (공지 글 → `/notice/{slug}`, 블로그 글 → `/blog/{slug}`). 어느 섹션에도\n * 안 속하면(미설정·미존재 key) canonical 을 생략한다. `post.slug` 필요.\n */\n collections?: readonly RouteCollection[];\n}\n\n/** Next `Metadata` 의 구조적 부분집합 — 소비자는 그대로 return 가능. */\nexport interface PostMetadata {\n title: string;\n description?: string;\n alternates?: { canonical: string };\n robots?: { index: boolean; follow: boolean };\n openGraph: {\n type: \"article\";\n title: string;\n description?: string;\n url?: string;\n publishedTime?: string;\n images?: { url: string; alt: string }[];\n };\n}\n\n/**\n * 글 메타데이터를 Next `Metadata` 형태로 빌드.\n *\n * - 제목/설명/OG 이미지는 어드민 SEO override 가 있으면 우선, 없으면 글 값 fallback.\n * - canonical: `seo.canonical` override → 없으면 `siteUrl`+`path`(명시) → 없으면\n * `siteUrl`+`collections`(글의 `collectionKey` 로 공지/블로그 basePath 자동 해석)\n * → 없으면 생략. 모든 글이 자기 자신을 가리키는 canonical 을 갖는 게 권장이므로\n * `siteUrl` 과 함께 `path` 또는 `collections` 전달을 권장한다.\n * - `noindex`/`nofollow` 중 하나라도 켜지면 `robots` 출력.\n *\n * ```ts\n * // 단일 블로그(레거시): 라우트가 곧 섹션이므로 path 를 직접 준다.\n * return buildPostMetadata(post, {\n * siteUrl: process.env.NEXT_PUBLIC_SITE_URL,\n * path: `/blog/${post.slug}`,\n * });\n *\n * // 다중 스트림(ADR-0060): collections 를 주면 공지/블로그를 글의\n * // collectionKey 로 구분해 canonical 을 자동 해석한다(공지→/notice, 블로그→/blog).\n * return buildPostMetadata(post, {\n * siteUrl: process.env.NEXT_PUBLIC_SITE_URL,\n * collections: COLLECTIONS,\n * });\n * ```\n */\nexport function buildPostMetadata(\n post: PostMetadataInput,\n options: BuildPostMetadataOptions = {},\n): PostMetadata {\n const seo = post.seo ?? undefined;\n const title = seo?.title || post.title;\n const description = seo?.description || post.description || undefined;\n const ogImage = seo?.ogImage || post.image || undefined;\n\n let canonical = seo?.canonical || undefined;\n if (!canonical && options.siteUrl) {\n // 경로 우선순위: 명시 path → collections 로 글의 섹션 basePath 자동 해석(공지/블로그).\n let path = options.path;\n if (!path && options.collections && post.slug) {\n path =\n resolvePostPath(\n { slug: post.slug, terms: [], collectionKey: post.collectionKey ?? null },\n options.collections,\n ) ?? undefined;\n }\n if (path) {\n const normalized = path.startsWith(\"/\") ? path : `/${path}`;\n canonical = `${trimSlash(options.siteUrl)}${normalized}`;\n }\n }\n\n const openGraph: PostMetadata[\"openGraph\"] = { type: \"article\", title };\n if (description) openGraph.description = description;\n if (canonical) openGraph.url = canonical;\n if (post.date) openGraph.publishedTime = post.date;\n if (ogImage) openGraph.images = [{ url: ogImage, alt: title }];\n\n const meta: PostMetadata = { title, openGraph };\n if (description) meta.description = description;\n if (canonical) meta.alternates = { canonical };\n if (seo?.noindex || seo?.nofollow) {\n meta.robots = { index: !seo.noindex, follow: !seo.nofollow };\n }\n return meta;\n}\n"],"mappings":"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","names":["resolvePostCollection","resolvePostUrl","resolvePostCollection","resolvePostUrl","isInCollectionMode","resolvePostUrl","isInCollectionMode","resolvePostUrl","isInCollectionMode","isInCollectionMode"]}
|
package/dist/server.d.ts
CHANGED
|
@@ -286,6 +286,12 @@ type RootTaleBlogListProps = {
|
|
|
286
286
|
categoryHref?: (slug: string | null) => string;
|
|
287
287
|
/** "전체" 칩 라벨. 기본 "전체". */
|
|
288
288
|
allCategoriesLabel?: string;
|
|
289
|
+
/**
|
|
290
|
+
* 작가 slug 로 글을 필터 — 작가 아카이브(/blog/author/{slug}) 페이지용. 지정 시
|
|
291
|
+
* 그 작가의 발행 글(post)만 fetch 한다. `fetchAuthors` 로 작가 메타(이름·소개)를
|
|
292
|
+
* 따로 가져와 페이지 헤더에 쓴다.
|
|
293
|
+
*/
|
|
294
|
+
author?: string;
|
|
289
295
|
/**
|
|
290
296
|
* RootTale 디자인 토큰. 생략 시 admin /design 에서 저장한 값을 자동 fetch.
|
|
291
297
|
* `null` 을 명시하면 자동 fetch 도 건너뛰고 CSS fallback 사용 (부모
|
package/dist/server.js
CHANGED
package/dist/templates.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roottale/cms-renderer-next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.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",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"zod": "^3.23.8",
|
|
45
|
-
"@roottale/cms-
|
|
46
|
-
"@roottale/cms-
|
|
45
|
+
"@roottale/cms-core": "^0.34.0",
|
|
46
|
+
"@roottale/cms-client": "^0.37.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"react": "^19.0.0"
|
|
@@ -1 +0,0 @@
|
|
|
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 type RootTaleBlogSettings,\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 postsForCollection,\n resolvePostPath,\n type RouteCollection,\n} from \"@roottale/cms-core\";\n\nimport { RootTalePostCard } from \"./PostCard.js\";\nimport { RootTaleTableOfContents } from \"./TableOfContents.js\";\nimport {\n collectCategories,\n collectCategoriesWithCounts,\n collectCollectionCategories,\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 /**\n * Build a customer-side URL for a single post. Defaults to `/blog/${slug}`,\n * 또는 `collections` 를 주면 글의 섹션 basePath 기준(`/notice/${slug}` 등).\n */\n postHref?: (post: CmsPostContent) => string;\n /** Rendered when the tenant has no published posts yet. */\n emptyMessage?: string;\n /**\n * 단일 섹션(stream)으로 스코프 (ADR-0060). `collections` 와 함께 줘야 동작한다.\n * 예: `collection=\"notice\"` → 공지 섹션 글만 렌더. **미지정 = 전체** 글(레거시\n * 단일 블로그). 이 prop 없이 다중 스트림 사이트에서 쓰면 공지·블로그가 한 목록에\n * 섞여 나오므로(`/blog` 에 공지가 새어 들어옴), 섹션을 나눈 사이트는 각 목록\n * 페이지에서 반드시 `collection` 으로 스코프해야 한다.\n */\n collection?: string;\n /**\n * 섹션 정의 (ADR-0060). `collection` 스코프와 collection-aware `postHref`(글을\n * 소속 섹션 basePath 로 라우팅) 해석에 쓰인다. admin 에서 동적으로 받으려면\n * `fetchCollections()` 결과를 넘긴다(`collections.md` 참고).\n */\n collections?: readonly RouteCollection[];\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 collection,\n collections,\n emptyMessage = \"아직 발행된 글이 없습니다.\",\n showCategoryFilter = false,\n activeCategory,\n categoryHref = defaultCategoryHref,\n allCategoriesLabel = \"전체\",\n theme: themeProp,\n } = props;\n // collections 를 주면 글을 소속 섹션 basePath 로 라우팅(공지→/notice 등),\n // 아니면 레거시 `/blog/${slug}`. 명시 postHref 가 있으면 그게 우선.\n const postHref =\n props.postHref ??\n (collections ? makeCollectionPostHref(collections) : defaultPostHref);\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 // 섹션 스코프(ADR-0060) — `collection`+`collections` 가 있으면 그 섹션 글만.\n // 미지정이면 전체(레거시). 공지·블로그가 한 목록에 섞이는 누출을 막는다.\n const items =\n collection && collections\n ? postsForCollection(page.items, collections, collection)\n : page.items;\n\n if (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(items) : [];\n const visiblePosts = showCategoryFilter\n ? filterPostsByCategory(items, activeCategory)\n : 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 /**\n * 단일 섹션으로 스코프 (ADR-0060). `collections` 와 함께 줘야 동작. 그 섹션의\n * `categories`(주제 allowlist) 안에서, 그 섹션에 귀속된 글의 카테고리만 집계한다.\n * 미지정 = 전체 글의 카테고리.\n */\n collection?: string;\n /** 섹션 정의 (ADR-0060). `collection` 스코프에 필요. */\n collections?: readonly RouteCollection[];\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 collection,\n collections,\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 =\n collection && collections\n ? collectCollectionCategories(page.items, collections, collection)\n : 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 {renderPostCta(blogSettings.postCta)}\n </article>\n );\n}\n\n/**\n * 글 하단 고정 CTA — 사이트 블로그 설정에 활성화돼 있으면 모든 글 본문 끝에\n * 동일 CTA 블록을 그린다(리드 전환 유도). href 는 안전 스킴만 통과 — 외부\n * 링크(http/mailto/tel 이 아닌 절대 URL 제외)는 새 탭으로 열되 그 외엔 차단.\n */\nfunction renderPostCta(cta: RootTaleBlogSettings[\"postCta\"]): ReactElement | null {\n if (!cta) return null;\n const href = cta.buttonHref.trim();\n const safe =\n href.startsWith(\"/\") ||\n href.startsWith(\"#\") ||\n /^(https?:|mailto:|tel:)/i.test(href);\n if (!safe) return null;\n const external = /^https?:/i.test(href);\n return (\n <aside data-roottale-cms=\"post-cta\" className=\"rt-cms-post-cta\">\n {cta.title ? <p className=\"rt-cms-post-cta__title\">{cta.title}</p> : null}\n {cta.description ? (\n <p className=\"rt-cms-post-cta__desc\">{cta.description}</p>\n ) : null}\n <a\n className=\"rt-cms-post-cta__btn\"\n href={href}\n target={external ? \"_blank\" : undefined}\n rel={external ? \"noopener noreferrer\" : undefined}\n >\n {cta.buttonLabel}\n </a>\n </aside>\n );\n}\n\nexport type RootTalePageProps = {\n apiKey: string;\n /** Page slug (kebab-case) or UUIDv7 id. */\n slugOrId: string;\n baseUrl?: string;\n /** Rendered when the slug is not found or not published. */\n notFoundElement?: ReactElement;\n /**\n * Render the page title as `<h1>`. Default true — set false when the\n * customer layout already owns the heading.\n */\n showTitle?: boolean;\n /** See `RootTaleBlogListProps['theme']`. */\n theme?: RootTaleThemeInput;\n};\n\n/**\n * 고정 페이지(posts type='page') 렌더러 — RootTaleBlogPost 의 경량판.\n * 날짜·작성자·author card·blog settings 등 블로그 chrome 없이\n * 제목 + 본문만 그린다. 회사 소개·서비스·이용약관 류의 페이지 용도.\n *\n * ```tsx\n * // app/[slug]/page.tsx\n * export default async function CmsPage({ params }) {\n * const { slug } = await params;\n * return <RootTalePage apiKey={process.env.ROOTTALE_API_KEY!} slugOrId={slug} />;\n * }\n * ```\n */\nexport async function RootTalePage(\n props: RootTalePageProps,\n): Promise<ReactElement> {\n const {\n apiKey,\n slugOrId,\n baseUrl,\n notFoundElement,\n showTitle = true,\n theme: themeProp,\n } = props;\n const [post, themeStyle] = await Promise.all([\n fetchPost({ apiKey, slugOrId, baseUrl }),\n resolveThemeStyle({ themeProp, apiKey, baseUrl }),\n ]);\n\n if (!post) {\n return (\n notFoundElement ?? (\n <div data-roottale-cms=\"page-missing\" style={themeStyle ?? undefined}>\n <p className=\"rt-cms-empty\">페이지를 찾을 수 없습니다.</p>\n </div>\n )\n );\n }\n\n return (\n <article\n data-roottale-cms=\"page\"\n className=\"rt-cms-article\"\n style={themeStyle ?? undefined}\n >\n {showTitle ? (\n <header className=\"rt-cms-post-header\">\n <h1 className=\"rt-cms-title\">{post.title}</h1>\n </header>\n ) : null}\n <div data-roottale-cms=\"body\" className=\"rt-cms-body\">\n <RenderTiptap doc={post.bodyJson} fields={post.fields} />\n </div>\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.secondary) vars[\"--rt-color-secondary\"] = c.secondary;\n if (c.secondaryForeground) vars[\"--rt-color-secondary-foreground\"] = c.secondaryForeground;\n if (c.accent) vars[\"--rt-color-accent\"] = c.accent;\n if (c.accentForeground) vars[\"--rt-color-accent-foreground\"] = c.accentForeground;\n if (c.muted) vars[\"--rt-color-muted\"] = c.muted;\n if (c.mutedForeground) vars[\"--rt-color-muted-foreground\"] = c.mutedForeground;\n if (c.destructive) vars[\"--rt-color-destructive\"] = c.destructive;\n if (c.destructiveForeground) vars[\"--rt-color-destructive-foreground\"] = c.destructiveForeground;\n if (c.border) vars[\"--rt-color-border\"] = c.border;\n if (c.input) vars[\"--rt-color-input\"] = c.input;\n if (c.ring) vars[\"--rt-color-ring\"] = c.ring;\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.sm) vars[\"--rt-radius-sm\"] = r.sm;\n if (r.md) vars[\"--rt-radius-md\"] = r.md;\n if (r.lg) vars[\"--rt-radius-lg\"] = r.lg;\n if (r.xl) vars[\"--rt-radius-xl\"] = r.xl;\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\n/**\n * collections 기반 postHref — 글을 소속 섹션 basePath 로 라우팅(공지→/notice,\n * 블로그→/blog). 어느 섹션에도 안 속하면 레거시 `/blog/${slug}` 로 fallback.\n */\nfunction makeCollectionPostHref(\n collections: readonly RouteCollection[],\n): (post: CmsPostContent) => string {\n return (post) => resolvePostPath(post, collections) ?? defaultPostHref(post);\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 // 목차 블록이 있으면 헤딩 id 부여 + 항목 수집(앵커 대상). astro 와 동일.\n const hasToc = content.some((n) => n?.type === \"tableOfContents\");\n const docRecord = doc as Record<string, unknown>;\n const entries = hasToc ? extractToc(docRecord) : [];\n const renderDoc =\n hasToc && entries.length > 0 ? attachHeadingIds(docRecord, entries) : docRecord;\n const nodes: TiptapNode[] = Array.isArray((renderDoc as TiptapDoc).content)\n ? ((renderDoc as TiptapDoc).content as TiptapNode[])\n : content;\n return (\n <>\n {nodes.map((node, i) =>\n node?.type === \"tableOfContents\" ? (\n <RootTaleTableOfContents\n key={i}\n entries={entries}\n title={\n typeof node.attrs?.title === \"string\" ? node.attrs.title : \"목차\"\n }\n />\n ) : (\n renderNode(node, i, fields ?? null)\n ),\n )}\n </>\n );\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\n/** 표 셀의 colSpan/rowSpan (1 초과일 때만 적용, 아니면 undefined). */\nfunction tableCellSpans(node: TiptapNode): {\n colSpan: number | undefined;\n rowSpan: number | undefined;\n} {\n const colspan = Number(node.attrs?.colspan);\n const rowspan = Number(node.attrs?.rowspan);\n return {\n colSpan: Number.isFinite(colspan) && colspan > 1 ? colspan : undefined,\n rowSpan: Number.isFinite(rowspan) && rowspan > 1 ? rowspan : undefined,\n };\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 // width/height — CLS 대책. CSS(max-inline-size:100%; block-size:auto)와\n // 조합돼 반응형 유지. 에디터/WP import 가 보존한 치수가 있을 때만 출력.\n const width =\n typeof node.attrs?.width === \"number\" && node.attrs.width > 0\n ? node.attrs.width\n : undefined;\n const height =\n typeof node.attrs?.height === \"number\" && node.attrs.height > 0\n ? node.attrs.height\n : undefined;\n return (\n <img\n key={key}\n src={src}\n alt={alt}\n title={title}\n width={width}\n height={height}\n loading=\"lazy\"\n />\n );\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 \"table\":\n return (\n <div key={key} className=\"rt-table-wrap\">\n <table className=\"rt-table\">\n <tbody>{children}</tbody>\n </table>\n </div>\n );\n case \"tableRow\":\n return <tr key={key}>{children}</tr>;\n case \"tableHeader\": {\n const { colSpan, rowSpan } = tableCellSpans(node);\n return <th key={key} colSpan={colSpan} rowSpan={rowSpan}>{children}</th>;\n }\n case \"tableCell\": {\n const { colSpan, rowSpan } = tableCellSpans(node);\n return <td key={key} colSpan={colSpan} rowSpan={rowSpan}>{children}</td>;\n }\n case \"embed\":\n return renderEmbed(node, key);\n case \"tableOfContents\":\n // 목차는 문서 전체 컨텍스트가 필요해 RenderTiptap 최상위에서 처리.\n // 중첩 등장 시엔 무시(헤딩 수집 컨텍스트 없음).\n return null;\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":";;;;;;;;;AAuBA;AAAA,EAKE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAOP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;;;ACIG,cAmBI,YAnBJ;AArBV,SAAS,kBAAkB,OAAuD;AAChF,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,MAAM,MAAM,KAAK,CAAC,MAAM,EAAE,aAAa,UAAU;AACvD,SAAO,MAAM,IAAI,OAAO;AAC1B;AAEO,SAAS,iBAAiB,OAA4C;AAC3E,QAAM,EAAE,MAAM,OAAO,aAAa,QAAQ,IAAI;AAC9C,QAAM,WAAW,UAAU,QAAQ,WAAW;AAC9C,QAAM,aAAa,UAAU,QAAQ,aAAa;AAClD,QAAM,YAAY,SAAS,cAAc;AACzC,QAAM,eAAe,SAAS,iBAAiB;AAC/C,QAAM,aAAc,KAA2B;AAC/C,QAAM,gBAAgB,WAAW,oBAAoB,KAAK,WAAW,IAAI;AACzE,QAAM,WAAW,YAAY,KAAK,mBAAmB;AACrD,QAAM,WAAW,eAAe,kBAAkB,KAAK,KAAK,IAAI;AAChE,SACE,oBAAC,aAAQ,qBAAkB,QAAO,WAAU,eAC1C,+BAAC,OAAE,WAAU,oBAAmB,MAAM,KAAK,IAAI,GAC5C;AAAA;AAAA;AAAA,MAEC;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,KAAK;AAAA,UACL,KAAI;AAAA,UACJ,SAAQ;AAAA,UACR,UAAS;AAAA;AAAA,MACX;AAAA,QACE;AAAA,IACH,WACC,oBAAC,UAAK,WAAU,wBAAwB,oBAAS,IAC/C;AAAA,IACH,iBAAkB,cAAc,aAC/B,qBAAC,OAAE,WAAU,eACV;AAAA,sBACC,oBAAC,UAAK,WAAU,oBAAmB,oBAAiB,QACjD,yBACH,IACE;AAAA,MACH,cAAc,aACb,qBAAC,UAAK,WAAU,oBAAmB,oBAAiB,UAAS;AAAA;AAAA,QACtD;AAAA,SACP,IACE;AAAA,OACN,IACE;AAAA,IACJ,oBAAC,QAAG,WAAU,gBAAgB,eAAK,OAAM;AAAA,IACxC,KAAK,UAAU,oBAAC,OAAE,WAAU,kBAAkB,eAAK,SAAQ,IAAO;AAAA,IACnE,oBAAC,UAAK,WAAU,sBAAqB,eAAY,QAAO,0BAExD;AAAA,KACF,GACF;AAEJ;AAEA,SAAS,YAAY,MAA8B;AACjD,SAAO,SAAS,KAAK,IAAI;AAC3B;AAEA,SAAS,oBAAoB,KAAqB;AAChD,QAAM,IAAI,IAAI,KAAK,GAAG;AACtB,MAAI,OAAO,MAAM,EAAE,QAAQ,CAAC,EAAG,QAAO;AACtC,SAAO,IAAI,KAAK,eAAe,SAAS;AAAA,IACtC,MAAM;AAAA,IACN,OAAO;AAAA,IACP,KAAK;AAAA,EACP,CAAC,EAAE,OAAO,CAAC;AACb;;;ACvEQ,SACW,OAAAA,MADX,QAAAC,aAAA;AARD,SAAS,wBACd,OACqB;AACrB,QAAM,EAAE,SAAS,QAAQ,eAAK,IAAI;AAClC,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,SACE,gBAAAD,KAAC,SAAI,qBAAkB,OAAM,WAAU,cAAa,cAAY,OAC9D,0BAAAC,MAAC,aAAQ,WAAU,sBAAqB,MAAI,MAC1C;AAAA,oBAAAA,MAAC,aAAQ,WAAU,sBAChB;AAAA,cAAQ,gBAAAD,KAAC,UAAK,WAAU,oBAAoB,iBAAM,IAAU;AAAA,MAC7D,gBAAAA,KAAC,UAAK,WAAU,qBAAoB,eAAY,QAAO;AAAA,OACzD;AAAA,IACA,gBAAAA,KAAC,QAAG,WAAU,mBACX,kBAAQ,IAAI,CAAC,UACZ,gBAAAA;AAAA,MAAC;AAAA;AAAA,QAEC,WAAW,oCAAoC,MAAM,KAAK;AAAA,QAE1D,0BAAAA,KAAC,OAAE,MAAM,IAAI,MAAM,EAAE,IAAK,gBAAM,MAAK;AAAA;AAAA,MAHhC,MAAM;AAAA,IAIb,CACD,GACH;AAAA,KACF,GACF;AAEJ;;;ACjBA,SAAS,SAAS,UAA4C;AAC5D,MAAI,WAAW,KAAK,WAAW,EAAG,QAAO;AACzC,MAAI,WAAW,EAAG,QAAO;AACzB,SAAO,aAAa,IAAI,IAAI;AAC9B;AAEO,SAAS,YAAY,MAAsB;AAChD,SAAO,KACJ,KAAK,EACL,YAAY,EACZ,QAAQ,sBAAsB,EAAE,EAChC,QAAQ,QAAQ,GAAG,EACnB,QAAQ,OAAO,GAAG,EAClB,QAAQ,UAAU,EAAE;AACzB;AAEA,SAAS,OAAO,MAA0B;AACxC,MAAI,OAAO,KAAK,SAAS,SAAU,QAAO,KAAK;AAC/C,MAAI,CAAC,MAAM,QAAQ,KAAK,OAAO,EAAG,QAAO;AACzC,SAAO,KAAK,QAAQ,IAAI,MAAM,EAAE,KAAK,EAAE;AACzC;AAEO,SAAS,WAAW,KAA0C;AACnE,QAAM,OAAO;AACb,QAAM,UAAU,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,UAAU,CAAC;AAC9D,QAAM,UAAsB,CAAC;AAC7B,QAAM,WAAW,oBAAI,IAAoB;AAEzC,aAAW,QAAQ,SAAS;AAC1B,QAAI,MAAM,SAAS,UAAW;AAC9B,UAAM,WAAW,OAAO,KAAK,OAAO,SAAS,CAAC;AAC9C,UAAM,QAAQ,SAAS,QAAQ;AAC/B,QAAI,UAAU,KAAM;AACpB,UAAM,OAAO,OAAO,IAAI,EAAE,KAAK;AAC/B,QAAI,CAAC,KAAM;AAEX,UAAM,OAAO,YAAY,IAAI,KAAK;AAClC,UAAM,QAAQ,SAAS,IAAI,IAAI,KAAK;AACpC,UAAM,KAAK,UAAU,IAAI,OAAO,GAAG,IAAI,IAAI,KAAK;AAChD,aAAS,IAAI,MAAM,QAAQ,CAAC;AAE5B,YAAQ,KAAK,EAAE,OAAO,MAAM,GAAG,CAAC;AAAA,EAClC;AAEA,SAAO;AACT;AAOO,SAAS,iBACd,KACA,SACyB;AACzB,QAAM,OAAO;AACb,QAAM,UAAU,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,UAAU,CAAC;AAC9D,MAAI,SAAS;AACb,QAAM,cAAc,QAAQ,IAAI,CAAC,SAAS;AACxC,QAAI,MAAM,SAAS,UAAW,QAAO;AACrC,UAAM,WAAW,OAAO,KAAK,OAAO,SAAS,CAAC;AAC9C,QAAI,WAAW,KAAK,WAAW,EAAG,QAAO;AACzC,QAAI,CAAC,OAAO,IAAI,EAAE,KAAK,EAAG,QAAO;AACjC,UAAM,QAAQ,QAAQ,QAAQ;AAC9B,QAAI,CAAC,MAAO,QAAO;AACnB,WAAO;AAAA,MACL,GAAG;AAAA,MACH,OAAO,EAAE,GAAI,KAAK,SAAS,CAAC,GAAI,IAAI,MAAM,GAAG;AAAA,IAC/C;AAAA,EACF,CAAC;AACD,SAAO,EAAE,GAAI,MAAkC,SAAS,YAAY;AACtE;;;ACxDU,SAOE,OAAAE,MAPF,QAAAC,aAAA;AAbH,SAAS,oBACd,OACqB;AACrB,QAAM,EAAE,SAAS,WAAW,eAAe,IAAI;AAC/C,MAAI,CAAC,WAAW,QAAQ,WAAW,EAAG,QAAO;AAC7C,SACE,gBAAAD;AAAA,IAAC;AAAA;AAAA,MACC,qBAAkB;AAAA,MAClB,WAAW,4CAA4C,QAAQ;AAAA,MAE9D,kBAAQ,IAAI,CAAC,KAAK,QAAQ;AACzB,cAAM,aAAa,IAAI,SAAS,WAAW,IAAI,SAAS;AACxD,eACE,gBAAAC;AAAA,UAAC;AAAA;AAAA,YAEC,MAAM,IAAI;AAAA,YACV,WAAW,sDAAsD,IAAI,IAAI;AAAA,YACzE,QAAQ,aAAa,WAAW;AAAA,YAChC,KAAK,aAAa,wBAAwB;AAAA,YAE1C;AAAA,8BAAAD,KAAC,UAAK,eAAY,QAAO,WAAU,6BAChC,cAAI,QAAQ,YAAY,IAAI,IAAI,GACnC;AAAA,cACA,gBAAAA,KAAC,UAAK,WAAU,8BAA8B,cAAI,OAAM;AAAA;AAAA;AAAA,UATnD,GAAG,IAAI,IAAI,IAAI,GAAG;AAAA,QAUzB;AAAA,MAEJ,CAAC;AAAA;AAAA,EACH;AAEJ;AAEA,SAAS,YAAY,MAA6B;AAChD,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,EACX;AACF;;;ACiBQ,SAWA,UAXA,OAAAE,MAaE,QAAAC,aAbF;AArCR,IAAM,iBAAmD;AAAA,EACvD,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,KAAK;AAAA,EACL,OAAO;AACT;AAEA,IAAM,mBAAiE;AAAA,EACrE,EAAE,OAAO,cAAc,OAAO,oCAAW;AAAA,EACzC,EAAE,OAAO,WAAW,OAAO,0EAAkB;AAAA,EAC7C,EAAE,OAAO,OAAO,OAAO,0DAAe;AAAA,EACtC,EAAE,OAAO,SAAS,OAAO,gEAAgB;AAC3C;AAEO,SAAS,iBACd,OACc;AACd,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc;AAAA,IACd;AAAA,EACF,IAAI;AAEJ,QAAM,YAAY,CAAC,oBAAoB,SAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAE1E,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,QAAO;AAAA,MACP;AAAA,MACA,qBAAkB;AAAA,MAClB,WAAW;AAAA,MAEV;AAAA,kBACC,gBAAAD,KAAC,QAAG,WAAU,uBAAuB,mBAAQ,IAC3C;AAAA,QACH,cACC,gBAAAA,KAAC,OAAE,WAAU,2BAA2B,uBAAY,IAClD;AAAA,QAEH,cACC,gBAAAA,KAAC,WAAM,MAAK,UAAS,MAAK,iBAAgB,OAAO,aAAa,IAC5D;AAAA,QAEH,WACC,gBAAAC,MAAA,YACE;AAAA,0BAAAD,KAAC,WAAM,MAAK,UAAS,MAAK,YAAW,OAAO,UAAU;AAAA,UACtD,gBAAAC,MAAC,OAAE,WAAU,8BAA6B;AAAA;AAAA,YACpC,gBAAAD,KAAC,YAAQ,yBAAe,QAAQ,GAAE;AAAA,aACxC;AAAA,WACF,IAEA,gBAAAC,MAAC,WAAM,WAAU,gBACf;AAAA,0BAAAA,MAAC,UAAK,WAAU,sBAAqB;AAAA;AAAA,YAChC,gBAAAD,KAAC,UAAK,eAAY,QAAO,eAAC;AAAA,aAC/B;AAAA,UACA,gBAAAC;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,UAAQ;AAAA,cACR,WAAU;AAAA,cACV,cAAa;AAAA,cAEb;AAAA,gCAAAD,KAAC,YAAO,OAAM,IAAG,UAAQ,MAAC,4CAE1B;AAAA,gBACC,iBAAiB,IAAI,CAAC,QACrB,gBAAAA,KAAC,YAAuB,OAAO,IAAI,OAChC,cAAI,SADM,IAAI,KAEjB,CACD;AAAA;AAAA;AAAA,UACH;AAAA,WACF;AAAA,QAGF,gBAAAC,MAAC,WAAM,WAAU,gBACf;AAAA,0BAAAA,MAAC,UAAK,WAAU,sBAAqB;AAAA;AAAA,YAChC,gBAAAD,KAAC,UAAK,eAAY,QAAO,eAAC;AAAA,aAC/B;AAAA,UACA,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,MAAK;AAAA,cACL,UAAQ;AAAA,cACR,cAAa;AAAA,cACb,WAAU;AAAA;AAAA,UACZ;AAAA,WACF;AAAA,QAEA,gBAAAC,MAAC,WAAM,WAAU,gBACf;AAAA,0BAAAA,MAAC,UAAK,WAAU,sBAAqB;AAAA;AAAA,YAC9B,gBAAAD,KAAC,UAAK,eAAY,QAAO,eAAC;AAAA,aACjC;AAAA,UACA,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,MAAK;AAAA,cACL,UAAQ;AAAA,cACR,cAAa;AAAA,cACb,WAAU;AAAA;AAAA,UACZ;AAAA,WACF;AAAA,QAEA,gBAAAC,MAAC,WAAM,WAAU,gBACf;AAAA,0BAAAA,MAAC,UAAK,WAAU,sBAAqB;AAAA;AAAA,YAC/B,gBAAAD,KAAC,UAAK,eAAY,QAAO,eAAC;AAAA,aAChC;AAAA,UACA,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,MAAK;AAAA,cACL,UAAQ;AAAA,cACR,cAAa;AAAA,cACb,WAAU;AAAA;AAAA,UACZ;AAAA,WACF;AAAA,QAEA,gBAAAC,MAAC,WAAM,WAAU,gBACf;AAAA,0BAAAA,MAAC,UAAK,WAAU,sBAAqB;AAAA;AAAA,YAC9B,gBAAAD,KAAC,UAAK,eAAY,QAAO,eAAC;AAAA,aACjC;AAAA,UACA,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,MAAK;AAAA,cACL,UAAQ;AAAA,cACR,cAAa;AAAA,cACb,WAAU;AAAA,cACV,SAAS;AAAA,cACT,aAAY;AAAA,cACZ,WAAU;AAAA;AAAA,UACZ;AAAA,WACF;AAAA,QAEA,gBAAAC,MAAC,WAAM,WAAU,gBACf;AAAA,0BAAAD,KAAC,UAAK,WAAU,sBAAqB,gEAAe;AAAA,UACpD,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,MAAK;AAAA,cACL,aAAY;AAAA,cACZ,WAAU;AAAA;AAAA,UACZ;AAAA,UACA,gBAAAA,KAAC,UAAK,WAAU,qBAAoB,gEAAU;AAAA,WAChD;AAAA,QAEA,gBAAAC,MAAC,WAAM,WAAU,uBACf;AAAA,0BAAAD,KAAC,WAAM,MAAK,YAAW,MAAK,mBAAkB,UAAQ,MAAC;AAAA,UACvD,gBAAAC,MAAC,UACC;AAAA,4BAAAD,KAAC,YAAO,4BAAI;AAAA,YAAS;AAAA,aAEvB;AAAA,WACF;AAAA,QAEC,aAAa,YACZ,gBAAAC,MAAC,WAAM,WAAU,uBACf;AAAA,0BAAAD;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,MAAK;AAAA,cACL,UAAQ;AAAA;AAAA,UACV;AAAA,UACA,gBAAAC,MAAC,UACC;AAAA,4BAAAD,KAAC,YAAO,0CAAQ;AAAA,YAAS;AAAA,aAE3B;AAAA,WACF,IACE;AAAA,QAEJ,gBAAAA,KAAC,SAAI,WAAU,uBACb,0BAAAA,KAAC,YAAO,MAAK,UAAS,WAAU,sBAC7B,uBACH,GACF;AAAA;AAAA;AAAA,EACF;AAEJ;;;ACvNA;AAAA,EAGE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AA8BC,gBAAAE,MA+BA,QAAAC,aA/BA;AA5BR,SAAS,SAAS,OAAiC;AACjD,SAAO,OAAO,UAAU;AAC1B;AAEA,SAAS,YAAY,MAAkC;AAKrD,SAAO,EAAE,QAAQ,aAAa,IAAI,EAAE;AACtC;AAEA,SAAS,YAAY,QAA0C;AAC7D,SAAO,OAAO,IAAI,CAAC,GAAG,MAAM,YAAY,GAAG,CAAC,CAAC;AAC/C;AAKO,SAAS,YAAY,OAAc,MAAc,GAAiB;AACvE,QAAM,cAAc,MAAM;AAE1B,UAAQ,MAAM,MAAM;AAAA,IAClB,KAAK,kBAAkB;AACrB,YAAM,OAAO,SAAS,MAAM,WAAW,OAAO,IAC1C,MAAM,WAAW,UACjB,MAAM,WAAW;AACrB,aACE,gBAAAD;AAAA,QAAC;AAAA;AAAA,UAEC,iBAAe;AAAA,UACf,yBAAyB,YAAY,IAAI;AAAA;AAAA,QAFpC;AAAA,MAGP;AAAA,IAEJ;AAAA,IACA,KAAK,gBAAgB;AACnB,YAAM,WAAW,MAAM,WAAW;AAClC,YAAM,QACJ,OAAO,aAAa,YAAY,YAAY,KAAK,YAAY,IACzD,WACA;AACN,YAAM,OAAO,SAAS,MAAM,WAAW,OAAO,IAC1C,MAAM,WAAW,UACjB,MAAM,WAAW;AACrB,YAAM,QAAQ;AAAA,QACZ,iBAAiB;AAAA,QACjB,yBAAyB,YAAY,IAAI;AAAA,MAC3C;AACA,UAAI,UAAU,EAAG,QAAO,gBAAAA,KAAC,QAAc,GAAG,SAAT,GAAgB;AACjD,UAAI,UAAU,EAAG,QAAO,gBAAAA,KAAC,QAAc,GAAG,SAAT,GAAgB;AACjD,aAAO,gBAAAA,KAAC,QAAc,GAAG,SAAT,GAAgB;AAAA,IAClC;AAAA,IACA,KAAK,cAAc;AACjB,YAAM,MAAM,SAAS,MAAM,WAAW,GAAG,IAAI,MAAM,WAAW,MAAM;AACpE,YAAM,MAAM,SAAS,MAAM,WAAW,GAAG,IAAI,MAAM,WAAW,MAAM;AACpE,YAAM,UAAU,SAAS,MAAM,WAAW,OAAO,IAC7C,MAAM,WAAW,UACjB;AACJ,aACE,gBAAAC,MAAC,YAAiB,iBAAe,aAE/B;AAAA,wBAAAD,KAAC,SAAI,KAAU,KAAU,SAAQ,QAAO;AAAA,QACvC,WAAW,gBAAAA,KAAC,gBAAY,mBAAQ;AAAA,WAHtB,GAIb;AAAA,IAEJ;AAAA,IACA,KAAK,aAAa;AAChB,YAAM,UAAU,MAAM,WAAW,YAAY;AAC7C,YAAM,SAAS,MAAM,eAAe,CAAC,GAAG;AAAA,QACtC,CAAC,MAAM,MACL,KAAK,UACH,gBAAAA;AAAA,UAAC;AAAA;AAAA,YAEC,yBAAyB,YAAY,KAAK,OAAO;AAAA;AAAA,UAD5C;AAAA,QAEP,IAEA,gBAAAA,KAAC,QAAY,sBAAY,KAAK,eAAe,CAAC,CAAC,KAAtC,CAAwC;AAAA,MAEvD;AACA,aAAO,UACL,gBAAAA,KAAC,QAAa,iBAAe,aAC1B,mBADM,GAET,IAEA,gBAAAA,KAAC,QAAa,iBAAe,aAC1B,mBADM,GAET;AAAA,IAEJ;AAAA,IACA,KAAK,cAAc;AACjB,YAAM,QAAQ,YAAY,MAAM,eAAe,CAAC,CAAC;AACjD,YAAM,WAAW,SAAS,MAAM,WAAW,QAAQ,IAC/C,MAAM,WAAW,WACjB;AACJ,aACE,gBAAAC,MAAC,gBAAqB,iBAAe,aAClC;AAAA;AAAA,QACA,YAAY,gBAAAD,KAAC,UAAM,oBAAS;AAAA,WAFd,GAGjB;AAAA,IAEJ;AAAA,IACA,KAAK,aAAa;AAChB,YAAM,OAAO,SAAS,MAAM,WAAW,OAAO,IAC1C,MAAM,WAAW,UACjB,MAAM,WAAW;AACrB,YAAM,OAAO,SAAS,MAAM,WAAW,QAAQ,IAC3C,MAAM,WAAW,WACjB;AACJ,aACE,gBAAAA;AAAA,QAAC;AAAA;AAAA,UAEC,iBAAe;AAAA,UACf,iBAAe;AAAA,UAEf,0BAAAA,KAAC,UAAM,gBAAK;AAAA;AAAA,QAJP;AAAA,MAKP;AAAA,IAEJ;AAAA,IACA,KAAK,gBAAgB;AACnB,aACE,gBAAAA;AAAA,QAAC;AAAA;AAAA,UAEC,WAAU;AAAA,UACV,iBAAe;AAAA,UAEd,sBAAY,MAAM,eAAe,CAAC,CAAC;AAAA;AAAA,QAJ/B;AAAA,MAKP;AAAA,IAEJ;AAAA,IACA,KAAK;AACH,aAAO,gBAAAA,KAAC,QAAa,iBAAe,eAApB,GAAiC;AAAA,IACnD,KAAK,eAAe;AAClB,YAAM,YAAY,MAAM,WAAW;AACnC,YAAM,SAAS,SAAS,SAAS,IAAI,YAAY;AACjD,aACE,gBAAAA;AAAA,QAAC;AAAA;AAAA,UAEC,WAAU;AAAA,UACV,iBAAe;AAAA,UACf,OAAO,EAAE,OAAO;AAAA;AAAA,QAHX;AAAA,MAIP;AAAA,IAEJ;AAAA,IACA,KAAK,cAAc;AACjB,aACE,gBAAAA;AAAA,QAAC;AAAA;AAAA,UAEC,WAAU;AAAA,UACV,iBAAe;AAAA,UAEd,sBAAY,MAAM,eAAe,CAAC,CAAC;AAAA;AAAA,QAJ/B;AAAA,MAKP;AAAA,IAEJ;AAAA,IACA,SAAS;AAMP,YAAM,UAAU,MAAM;AACtB,aACE,gBAAAA;AAAA,QAAC;AAAA;AAAA,UAEC,WAAU;AAAA,UACV,iBAAe;AAAA,UACf,mBAAiB,MAAM;AAAA,UACvB,2BAAyB,UAAU,SAAS;AAAA,UAE3C,sBAAY,MAAM,eAAe,CAAC,CAAC;AAAA;AAAA,QAN/B;AAAA,MAOP;AAAA,IAEJ;AAAA,EACF;AACF;AAGO,SAAS,aAAa,QAA0C;AACrE,SAAO,OAAO,IAAI,CAAC,GAAG,MAAM,YAAY,GAAG,CAAC,CAAC;AAC/C;AAOO,SAAS,kBACd,QACA,OAGI,CAAC,GACW;AAChB,QAAM,WAAW,KAAK,YAAY,2BAA2B,KAAK,MAAM;AACxE,SAAO,aAAa,mBAAmB,QAAQ,QAAQ,CAAC;AAC1D;;;AN7EqB,qBAAAE,WAAA,OAAAC,MA+Gb,QAAAC,aA/Ga;AArErB,SAAS,qBAAqB,QAAiD;AAC7E,MAAI,WAAW,SAAU,QAAO;AAChC,MAAI,WAAW,UAAW,QAAO;AACjC,SAAO;AACT;AA4DO,SAAS,sBAAsB,OAGrB;AACf,QAAM,QAAQ,eAAe,MAAM,SAAS,IAAI;AAChD,MAAI,CAAC,MAAO,QAAO,gBAAAD,KAAAD,WAAA,EAAG,gBAAM,UAAS;AACrC,SACE,gBAAAC,KAAC,SAAI,qBAAkB,cAAa,OACjC,gBAAM,UACT;AAEJ;AAoDA,eAAsB,iBACpB,OACuB;AACvB,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA,eAAe;AAAA,IACf,qBAAqB;AAAA,IACrB;AAAA,IACA,eAAe;AAAA,IACf,qBAAqB;AAAA,IACrB,OAAO;AAAA,EACT,IAAI;AAGJ,QAAM,WACJ,MAAM,aACL,cAAc,uBAAuB,WAAW,IAAI;AAEvD,QAAM,CAAC,MAAM,YAAY,YAAY,IAAI,MAAM,QAAQ,IAAI;AAAA,IACzD,WAAW,EAAE,QAAQ,SAAS,OAAO,KAAK,CAAC;AAAA,IAC3C,kBAAkB,EAAE,WAAW,QAAQ,QAAQ,CAAC;AAAA,IAChD,kBAAkB,EAAE,QAAQ,QAAQ,CAAC,EAAE,MAAM,MAAM,qBAAqB;AAAA,EAC1E,CAAC;AAID,QAAM,QACJ,cAAc,cACV,mBAAmB,KAAK,OAAO,aAAa,UAAU,IACtD,KAAK;AAEX,MAAI,MAAM,WAAW,GAAG;AACtB,WACE,gBAAAA,KAAC,SAAI,qBAAkB,QAAO,OAAO,cAAc,QACjD,0BAAAA,KAAC,OAAE,WAAU,gBAAgB,wBAAa,GAC5C;AAAA,EAEJ;AAGA,QAAM,aAAa,qBAAqB,kBAAkB,KAAK,IAAI,CAAC;AACpE,QAAM,eAAe,qBACjB,sBAAsB,OAAO,cAAc,IAC3C;AAEJ,SACE,gBAAAC,MAAC,SAAI,qBAAkB,QAAO,OAAO,cAAc,QAChD;AAAA,0BAAsB,WAAW,SAAS,IACzC,gBAAAA,MAAC,SAAI,WAAU,iBAAgB,cAAW,yCACxC;AAAA,sBAAAD;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,MAAM,aAAa,IAAI;AAAA,UACvB,eAAa,CAAC,iBAAiB,KAAK;AAAA,UACpC,gBAAc,CAAC,iBAAiB,SAAS;AAAA,UAExC;AAAA;AAAA,MACH;AAAA,MACC,WAAW,IAAI,CAAC,MACf,gBAAAA;AAAA,QAAC;AAAA;AAAA,UAEC,WAAU;AAAA,UACV,MAAM,aAAa,EAAE,IAAI;AAAA,UACzB,eAAa,mBAAmB,EAAE,OAAO,KAAK;AAAA,UAC9C,gBAAc,mBAAmB,EAAE,OAAO,SAAS;AAAA,UAElD,YAAE;AAAA;AAAA,QANE,EAAE;AAAA,MAOT,CACD;AAAA,OACH,IACE;AAAA,IACH,aAAa,WAAW,IACvB,gBAAAA,KAAC,OAAE,WAAU,gBAAgB,wBAAa,IAE1C,gBAAAA,KAAC,QAAG,WAAU,eACX,uBAAa,IAAI,CAAC,SACjB,gBAAAA,KAAC,QAAiB,WAAU,oBAC1B,0BAAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,MAAM;AAAA,QACN,SAAS,mBAAmB,MAAM,YAAY;AAAA;AAAA,IAChD,KALO,KAAK,EAMd,CACD,GACH;AAAA,KAEJ;AAEJ;AAsBA,eAAsB,uBACpB,OACuB;AACvB,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,eAAe,CAAC,SAAS,oBAAoB,IAAI;AAAA,IACjD,eAAe;AAAA,IACf,OAAO;AAAA,EACT,IAAI;AAEJ,QAAM,CAAC,MAAM,UAAU,IAAI,MAAM,QAAQ,IAAI;AAAA,IAC3C,WAAW,EAAE,QAAQ,SAAS,OAAO,MAAM,OAAO,CAAC;AAAA,IACnD,kBAAkB,EAAE,WAAW,QAAQ,QAAQ,CAAC;AAAA,EAClD,CAAC;AACD,QAAM,aACJ,cAAc,cACV,4BAA4B,KAAK,OAAO,aAAa,UAAU,IAC/D,4BAA4B,KAAK,KAAK;AAE5C,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,qBAAkB;AAAA,MAClB,WAAU;AAAA,MACV,cAAW;AAAA,MACX,OAAO,cAAc;AAAA,MAEpB,qBAAW,WAAW,IACrB,gBAAAA,KAAC,OAAE,WAAU,gBAAgB,wBAAa,IAE1C,gBAAAA,KAAC,QAAG,WAAU,wBACX,qBAAW,IAAI,CAAC,aACf,gBAAAA,KAAC,QAAuB,WAAU,6BAChC,0BAAAC;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,MAAM,aAAa,SAAS,IAAI;AAAA,UAEhC;AAAA,4BAAAD,KAAC,UAAK,WAAU,wBAAwB,mBAAS,MAAK;AAAA,YACtD,gBAAAA,KAAC,UAAK,WAAU,yBAAyB,mBAAS,OAAM;AAAA;AAAA;AAAA,MAC1D,KAPO,SAAS,IAQlB,CACD,GACH;AAAA;AAAA,EAEJ;AAEJ;AAoBA,eAAsB,iBACpB,OACuB;AACvB,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO;AAAA,EACT,IAAI;AACJ,QAAM,CAAC,MAAM,YAAY,YAAY,IAAI,MAAM,QAAQ,IAAI;AAAA,IACzD,UAAU,EAAE,QAAQ,UAAU,QAAQ,CAAC;AAAA,IACvC,kBAAkB,EAAE,WAAW,QAAQ,QAAQ,CAAC;AAAA;AAAA,IAEhD,kBAAkB,EAAE,QAAQ,QAAQ,CAAC,EAAE,MAAM,MAAM,qBAAqB;AAAA,EAC1E,CAAC;AAED,MAAI,CAAC,MAAM;AACT,WACE,mBACE,gBAAAA,KAAC,SAAI,qBAAkB,gBAAe,OAAO,cAAc,QACzD,0BAAAA,KAAC,OAAE,WAAU,gBAAe,wEAAa,GAC3C;AAAA,EAGN;AAGA,QAAM,UAAU,mBAAmB,MAAM,cAAc;AAAA,IACrD;AAAA,IACA;AAAA,EACF,CAAC;AACD,QAAM,MAAM,QAAQ,sBAAsB,WAAW,KAAK,QAAQ,IAAI,CAAC;AACvE,QAAM,YACJ,IAAI,SAAS,IAAI,iBAAiB,KAAK,UAAU,GAAG,IAAI,KAAK;AAC/D,QAAM,aAAc,KAA2B;AAC/C,QAAM,iBAAkB,KAA2B;AACnD,QAAM,gBAAiB,KAA2B;AAClD,QAAM,gBAAgB,QAAQ,WAC1BE,qBAAoB,KAAK,WAAW,IACpC;AACJ,QAAM,gBAAgB,QAAQ,cAAc,QAAQ,UAAU;AAC9D,QAAM,oBAAoB,cAAc,QAAQ;AAChD,QAAM,YACJ,iBAAiB,QAAQ,oBAAoB,QAAQ;AACvD,QAAM,wBACJ,kBAAkB,QAAQ;AAC5B,QAAM,iBAAiB,GAAG,qBAAqB,EAAE,mCAAU,KAAK;AAChE,QAAM,mBAAkC;AAAA,IACtC,cAAc,qBAAqB,QAAQ,wBAAwB;AAAA,IACnE,gBAAgB,GAAG,QAAQ,2BAA2B,KAAK,QAAQ,2BAA2B;AAAA,EAChG;AACA,QAAM,iBAAiB,QAAQ,kBAAkB,QAAQ,iBAAiB;AAE1E,SACE,gBAAAD;AAAA,IAAC;AAAA;AAAA,MACC,qBAAkB;AAAA,MAClB,WAAU;AAAA,MACV,OAAO,cAAc;AAAA,MAErB;AAAA,wBAAAA,MAAC,YAAO,WAAU,sBACf;AAAA,2BAAiB,gBAChB,gBAAAA,MAAC,OAAE,WAAU,eACV;AAAA,4BACC,gBAAAD,KAAC,UAAK,WAAU,oBAAmB,oBAAiB,QACjD,yBACH,IACE;AAAA,YACH,gBACC,gBAAAC,MAAC,UAAK,WAAU,oBAAmB,oBAAiB,UAAS;AAAA;AAAA,cACtD;AAAA,eACP,IACE;AAAA,aACN,IACE;AAAA,UACJ,gBAAAD,KAAC,QAAG,WAAU,gBAAgB,eAAK,OAAM;AAAA,UACxC,KAAK,UAAU,gBAAAA,KAAC,OAAE,WAAU,kBAAkB,eAAK,SAAQ,IAAO;AAAA,WACrE;AAAA,QACC,IAAI,SAAS,IACZ,gBAAAA,KAAC,2BAAwB,SAAS,KAAK,OAAO,QAAQ,UAAU,IAC9D;AAAA,QACJ,gBAAAA,KAAC,SAAI,qBAAkB,QAAO,WAAU,eAEtC,0BAAAA,KAAC,gBAAa,KAAK,WAAW,QAAQ,KAAK,QAAQ,GACrD;AAAA,QACC,kBAAkB,oBACjB,gBAAAC,MAAC,aAAQ,WAAU,sBAAqB,cAAW,sBAChD;AAAA,kCACC,gBAAAD;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,KAAK;AAAA,cACL,KAAK;AAAA,cACL,SAAQ;AAAA,cACR,OAAO;AAAA;AAAA,UACT,IAEA,gBAAAA,KAAC,SAAI,WAAU,8BAA6B,eAAY,QACrD,yBAAe,iBAAiB,GACnC;AAAA,UAEF,gBAAAC,MAAC,SAAI,WAAU,4BACb;AAAA,4BAAAD,KAAC,OAAE,WAAU,+BACV,kBAAQ,mBACX;AAAA,YACA,gBAAAA,KAAC,OAAE,WAAU,4BAA4B,6BAAkB;AAAA,YAC1D,YACC,gBAAAA,KAAC,OAAE,WAAU,mCACV,qBACH,IACE;AAAA,aACN;AAAA,WACF,IACE;AAAA,QACH,cAAc,aAAa,OAAO;AAAA;AAAA;AAAA,EACrC;AAEJ;AAOA,SAAS,cAAc,KAA2D;AAChF,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,OAAO,IAAI,WAAW,KAAK;AACjC,QAAM,OACJ,KAAK,WAAW,GAAG,KACnB,KAAK,WAAW,GAAG,KACnB,2BAA2B,KAAK,IAAI;AACtC,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,WAAW,YAAY,KAAK,IAAI;AACtC,SACE,gBAAAC,MAAC,WAAM,qBAAkB,YAAW,WAAU,mBAC3C;AAAA,QAAI,QAAQ,gBAAAD,KAAC,OAAE,WAAU,0BAA0B,cAAI,OAAM,IAAO;AAAA,IACpE,IAAI,cACH,gBAAAA,KAAC,OAAE,WAAU,yBAAyB,cAAI,aAAY,IACpD;AAAA,IACJ,gBAAAA;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV;AAAA,QACA,QAAQ,WAAW,WAAW;AAAA,QAC9B,KAAK,WAAW,wBAAwB;AAAA,QAEvC,cAAI;AAAA;AAAA,IACP;AAAA,KACF;AAEJ;AA+BA,eAAsB,aACpB,OACuB;AACvB,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAY;AAAA,IACZ,OAAO;AAAA,EACT,IAAI;AACJ,QAAM,CAAC,MAAM,UAAU,IAAI,MAAM,QAAQ,IAAI;AAAA,IAC3C,UAAU,EAAE,QAAQ,UAAU,QAAQ,CAAC;AAAA,IACvC,kBAAkB,EAAE,WAAW,QAAQ,QAAQ,CAAC;AAAA,EAClD,CAAC;AAED,MAAI,CAAC,MAAM;AACT,WACE,mBACE,gBAAAA,KAAC,SAAI,qBAAkB,gBAAe,OAAO,cAAc,QACzD,0BAAAA,KAAC,OAAE,WAAU,gBAAe,oFAAe,GAC7C;AAAA,EAGN;AAEA,SACE,gBAAAC;AAAA,IAAC;AAAA;AAAA,MACC,qBAAkB;AAAA,MAClB,WAAU;AAAA,MACV,OAAO,cAAc;AAAA,MAEpB;AAAA,oBACC,gBAAAD,KAAC,YAAO,WAAU,sBAChB,0BAAAA,KAAC,QAAG,WAAU,gBAAgB,eAAK,OAAM,GAC3C,IACE;AAAA,QACJ,gBAAAA,KAAC,SAAI,qBAAkB,QAAO,WAAU,eACtC,0BAAAA,KAAC,gBAAa,KAAK,KAAK,UAAU,QAAQ,KAAK,QAAQ,GACzD;AAAA;AAAA;AAAA,EACF;AAEJ;AAEA,SAAS,eAAe,MAAsB;AAC5C,QAAM,UAAU,KAAK,KAAK;AAC1B,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,QAAQ,QAAQ,MAAM,KAAK,EAAE,OAAO,OAAO;AACjD,MAAI,MAAM,UAAU,GAAG;AACrB,WAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,YAAY;AAAA,EACpE;AACA,SAAO,CAAC,GAAG,OAAO,EAAE,MAAM,GAAG,CAAC,EAAE,KAAK,EAAE,EAAE,YAAY;AACvD;AAOA,eAAe,kBAAkB,OAIC;AAChC,QAAM,EAAE,WAAW,QAAQ,QAAQ,IAAI;AACvC,MAAI,cAAc,KAAM,QAAO;AAC/B,MAAI,cAAc,OAAW,QAAO,eAAe,SAAS;AAE5D,MAAI;AACF,UAAM,UAAU,MAAM,WAAW,EAAE,QAAQ,QAAQ,CAAC;AACpD,WAAO,eAAe,OAAO;AAAA,EAC/B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,eAAe,OAAmD;AACzE,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,OAA+B,CAAC;AACtC,QAAM,IAAI,MAAM,UAAU,CAAC;AAC3B,MAAI,EAAE,QAAS,MAAK,oBAAoB,IAAI,EAAE;AAC9C,MAAI,EAAE,kBAAmB,MAAK,+BAA+B,IAAI,EAAE;AACnE,MAAI,EAAE,WAAY,MAAK,uBAAuB,IAAI,EAAE;AACpD,MAAI,EAAE,WAAY,MAAK,uBAAuB,IAAI,EAAE;AACpD,MAAI,EAAE,UAAW,MAAK,sBAAsB,IAAI,EAAE;AAClD,MAAI,EAAE,oBAAqB,MAAK,iCAAiC,IAAI,EAAE;AACvE,MAAI,EAAE,OAAQ,MAAK,mBAAmB,IAAI,EAAE;AAC5C,MAAI,EAAE,iBAAkB,MAAK,8BAA8B,IAAI,EAAE;AACjE,MAAI,EAAE,MAAO,MAAK,kBAAkB,IAAI,EAAE;AAC1C,MAAI,EAAE,gBAAiB,MAAK,6BAA6B,IAAI,EAAE;AAC/D,MAAI,EAAE,YAAa,MAAK,wBAAwB,IAAI,EAAE;AACtD,MAAI,EAAE,sBAAuB,MAAK,mCAAmC,IAAI,EAAE;AAC3E,MAAI,EAAE,OAAQ,MAAK,mBAAmB,IAAI,EAAE;AAC5C,MAAI,EAAE,MAAO,MAAK,kBAAkB,IAAI,EAAE;AAC1C,MAAI,EAAE,KAAM,MAAK,iBAAiB,IAAI,EAAE;AACxC,QAAM,IAAI,MAAM,SAAS,CAAC;AAC1B,MAAI,EAAE,KAAM,MAAK,gBAAgB,IAAI,EAAE;AACvC,MAAI,EAAE,QAAS,MAAK,mBAAmB,IAAI,EAAE;AAC7C,QAAM,IAAI,MAAM,UAAU,CAAC;AAC3B,MAAI,EAAE,GAAI,MAAK,gBAAgB,IAAI,EAAE;AACrC,MAAI,EAAE,GAAI,MAAK,gBAAgB,IAAI,EAAE;AACrC,MAAI,EAAE,GAAI,MAAK,gBAAgB,IAAI,EAAE;AACrC,MAAI,EAAE,GAAI,MAAK,gBAAgB,IAAI,EAAE;AACrC,MAAI,OAAO,KAAK,IAAI,EAAE,WAAW,EAAG,QAAO;AAC3C,SAAO;AACT;AAEA,SAAS,gBAAgB,MAA8B;AACrD,SAAO,SAAS,KAAK,IAAI;AAC3B;AAMA,SAAS,uBACP,aACkC;AAClC,SAAO,CAAC,SAAS,gBAAgB,MAAM,WAAW,KAAK,gBAAgB,IAAI;AAC7E;AAEA,SAASE,qBAAoB,KAAqB;AAChD,QAAM,IAAI,IAAI,KAAK,GAAG;AACtB,MAAI,OAAO,MAAM,EAAE,QAAQ,CAAC,EAAG,QAAO;AACtC,SAAO,IAAI,KAAK,eAAe,SAAS;AAAA,IACtC,MAAM;AAAA,IACN,OAAO;AAAA,IACP,KAAK;AAAA,EACP,CAAC,EAAE,OAAO,CAAC;AACb;AAYO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AACF,GAQiB;AACf,QAAM,aAAc,IAAkB;AACtC,QAAM,UAAwB,MAAM,QAAQ,UAAU,IAAI,aAAa,CAAC;AAExE,QAAM,SAAS,QAAQ,KAAK,CAAC,MAAM,GAAG,SAAS,iBAAiB;AAChE,QAAM,YAAY;AAClB,QAAM,UAAU,SAAS,WAAW,SAAS,IAAI,CAAC;AAClD,QAAM,YACJ,UAAU,QAAQ,SAAS,IAAI,iBAAiB,WAAW,OAAO,IAAI;AACxE,QAAM,QAAsB,MAAM,QAAS,UAAwB,OAAO,IACpE,UAAwB,UAC1B;AACJ,SACE,gBAAAF,KAAAD,WAAA,EACG,gBAAM;AAAA,IAAI,CAAC,MAAM,MAChB,MAAM,SAAS,oBACb,gBAAAC;AAAA,MAAC;AAAA;AAAA,QAEC;AAAA,QACA,OACE,OAAO,KAAK,OAAO,UAAU,WAAW,KAAK,MAAM,QAAQ;AAAA;AAAA,MAHxD;AAAA,IAKP,IAEA,WAAW,MAAM,GAAG,UAAU,IAAI;AAAA,EAEtC,GACF;AAEJ;AAKA,SAAS,iBACP,MACA,QACoB;AACpB,SAAO;AAAA,IACL,gBAAgB,KAAK,OAAO,UAAU,WAAW,MAAM;AAAA,EACzD;AACF;AAmBA,SAAS,WAAW,OAAiC;AACnD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,IAAI,MAAM,KAAK;AACrB,MAAI,EAAE,WAAW,EAAG,QAAO;AAC3B,MAAI,EAAE,WAAW,GAAG,KAAK,EAAE,WAAW,GAAG,KAAK,EAAE,WAAW,GAAG,EAAG,QAAO;AACxE,SAAO,2BAA2B,KAAK,CAAC;AAC1C;AAEA,SAAS,WAAW,MAA6C;AAC/D,QAAM,QAAQ,KAAK,OAAO;AAC1B,MAAI,OAAO,UAAU,YAAY,UAAU,MAAM,UAAU,OAAQ,QAAO;AAC1E,MAAI,UAAU,YAAY,UAAU,WAAW,UAAU,WAAW;AAClE,WAAO,EAAE,WAAW,MAAM;AAAA,EAC5B;AACA,SAAO;AACT;AAGA,SAAS,eAAe,MAGtB;AACA,QAAM,UAAU,OAAO,KAAK,OAAO,OAAO;AAC1C,QAAM,UAAU,OAAO,KAAK,OAAO,OAAO;AAC1C,SAAO;AAAA,IACL,SAAS,OAAO,SAAS,OAAO,KAAK,UAAU,IAAI,UAAU;AAAA,IAC7D,SAAS,OAAO,SAAS,OAAO,KAAK,UAAU,IAAI,UAAU;AAAA,EAC/D;AACF;AAEA,SAAS,WACP,MACA,KACA,SAAuB,MACO;AAC9B,MAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;AAC9C,QAAM,WAAW,MAAM,QAAQ,KAAK,OAAO,IACvC,KAAK,QAAQ,IAAI,CAAC,OAAO,MAAM,WAAW,OAAO,GAAG,MAAM,CAAC,IAC3D;AAEJ,UAAQ,KAAK,MAAM;AAAA,IACjB,KAAK,aAAa;AAChB,YAAM,QAAQ,iBAAiB,MAAM,MAAM;AAC3C,aACE,gBAAAA,KAAC,OAAY,OAAO,WAAW,IAAI,GAChC,mBAAS,YADJ,GAER;AAAA,IAEJ;AAAA,IACA,KAAK,WAAW;AACd,YAAM,WAAW,OAAO,KAAK,OAAO,SAAS,CAAC;AAC9C,YAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,OAAO,SAAS,QAAQ,IAAI,WAAW,GAAG,CAAC,GAAG,CAAC;AAC/E,YAAM,SAAS,KAAK,OAAO;AAC3B,YAAM,KAAK,OAAO,WAAW,YAAY,OAAO,SAAS,IAAI,SAAS;AACtE,YAAM,QAAQ,WAAW,IAAI;AAC7B,YAAM,QAAQ,iBAAiB,MAAM,MAAM,KAAK;AAChD,UAAI,UAAU,EAAG,QAAO,gBAAAA,KAAC,QAAa,IAAQ,OAAe,mBAA5B,GAAkC;AACnE,UAAI,UAAU,EAAG,QAAO,gBAAAA,KAAC,QAAa,IAAQ,OAAe,mBAA5B,GAAkC;AACnE,aAAO,gBAAAA,KAAC,QAAa,IAAQ,OAAe,mBAA5B,GAAkC;AAAA,IACpD;AAAA,IACA,KAAK;AACH,aAAO,gBAAAA,KAAC,QAAc,YAAN,GAAe;AAAA,IACjC,KAAK;AACH,aAAO,gBAAAA,KAAC,QAAc,YAAN,GAAe;AAAA,IACjC,KAAK;AACH,aAAO,gBAAAA,KAAC,QAAc,YAAN,GAAe;AAAA,IACjC,KAAK;AACH,aAAO,gBAAAA,KAAC,gBAAsB,YAAN,GAAe;AAAA,IACzC,KAAK;AACH,aACE,gBAAAA,KAAC,SACC,0BAAAA,KAAC,UAAM,UAAS,KADR,GAEV;AAAA,IAEJ,KAAK;AACH,aAAO,gBAAAA,KAAC,UAAQ,GAAK;AAAA,IACvB,KAAK;AACH,aAAO,gBAAAA,KAAC,UAAQ,GAAK;AAAA,IACvB,KAAK,SAAS;AAEZ,YAAM,WAAW;AAAA,QACf,gBAAgB,KAAK,OAAO,UAAU,OAAO,MAAM;AAAA,MACrD;AACA,YAAM,MAAM,YAAY,KAAK,OAAO;AACpC,UAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,EAAG,QAAO;AACxD,YAAM,MAAM,OAAO,KAAK,OAAO,QAAQ,WAAW,KAAK,MAAM,MAAM;AACnE,YAAM,QAAQ,OAAO,KAAK,OAAO,UAAU,WAAW,KAAK,MAAM,QAAQ;AAGzE,YAAM,QACJ,OAAO,KAAK,OAAO,UAAU,YAAY,KAAK,MAAM,QAAQ,IACxD,KAAK,MAAM,QACX;AACN,YAAM,SACJ,OAAO,KAAK,OAAO,WAAW,YAAY,KAAK,MAAM,SAAS,IAC1D,KAAK,MAAM,SACX;AACN,aACE,gBAAAA;AAAA,QAAC;AAAA;AAAA,UAEC;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,SAAQ;AAAA;AAAA,QANH;AAAA,MAOP;AAAA,IAEJ;AAAA,IACA,KAAK;AACH,aAAO,gBAAAA,KAAC,SAAc,WAAU,cAAc,YAA7B,GAAsC;AAAA,IACzD,KAAK;AACH,aAAO,gBAAAA,KAAC,SAAc,WAAU,aAAa,YAA5B,GAAqC;AAAA,IACxD,KAAK;AACH,aACE,gBAAAA,KAAC,SAAc,WAAU,iBACvB,0BAAAA,KAAC,WAAM,WAAU,YACf,0BAAAA,KAAC,WAAO,UAAS,GACnB,KAHQ,GAIV;AAAA,IAEJ,KAAK;AACH,aAAO,gBAAAA,KAAC,QAAc,YAAN,GAAe;AAAA,IACjC,KAAK,eAAe;AAClB,YAAM,EAAE,SAAS,QAAQ,IAAI,eAAe,IAAI;AAChD,aAAO,gBAAAA,KAAC,QAAa,SAAkB,SAAmB,YAA1C,GAAmD;AAAA,IACrE;AAAA,IACA,KAAK,aAAa;AAChB,YAAM,EAAE,SAAS,QAAQ,IAAI,eAAe,IAAI;AAChD,aAAO,gBAAAA,KAAC,QAAa,SAAkB,SAAmB,YAA1C,GAAmD;AAAA,IACrE;AAAA,IACA,KAAK;AACH,aAAO,YAAY,MAAM,GAAG;AAAA,IAC9B,KAAK;AAGH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,WAAW,MAAM,GAAG;AAAA,IAC7B;AACE,aAAO,WAAW,gBAAAA,KAAC,UAAgB,YAAN,GAAe,IAAU;AAAA,EAC1D;AACF;AAMA,SAAS,YAAY,MAAkB,KAAkC;AACvE,QAAM,MAAM,KAAK,OAAO;AACxB,MAAI,kBAAkB,GAAG,GAAG;AAC1B,UAAM,WACJ,OAAO,KAAK,OAAO,aAAa,WAAW,KAAK,MAAM,WAAW;AACnE,WACE,gBAAAA;AAAA,MAAC;AAAA;AAAA,QAEC,WAAU;AAAA,QACV,iBAAe;AAAA,QACf,OAAO,EAAE,aAAa,SAAS;AAAA,QAE/B,0BAAAA;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA,OAAM;AAAA,YACN,SAAQ;AAAA,YACR,iBAAe;AAAA,YACf,gBAAe;AAAA,YACf,OAAM;AAAA,YACN,OAAO,EAAE,OAAO,QAAQ,QAAQ,QAAQ,QAAQ,EAAE;AAAA;AAAA,QACpD;AAAA;AAAA,MAbK;AAAA,IAcP;AAAA,EAEJ;AACA,QAAM,OAAO,KAAK,OAAO;AACzB,MAAI,WAAW,IAAI,GAAG;AACpB,WACE,gBAAAA,KAAC,OAAY,WAAU,qBACrB,0BAAAA,KAAC,OAAE,MAAM,MAAM,KAAI,uBAAsB,QAAO,UAC7C,gBACH,KAHM,GAIR;AAAA,EAEJ;AACA,SAAO;AACT;AAEA,SAAS,WAAW,MAAkB,KAAoC;AACxE,QAAM,OAAO,KAAK,QAAQ;AAC1B,QAAM,QAAQ,KAAK,SAAS,CAAC;AAC7B,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,MAAI,UAAiC;AACrC,aAAW,QAAQ,OAAO;AACxB,cAAU,UAAU,MAAM,OAAO;AAAA,EACnC;AACA,SAAO,gBAAAA,KAAC,UAAgB,qBAAN,GAAc;AAClC;AAEA,SAAS,UAAU,MAAkB,OAA4C;AAC/E,UAAQ,KAAK,MAAM;AAAA,IACjB,KAAK;AACH,aAAO,gBAAAA,KAAC,YAAQ,iBAAM;AAAA,IACxB,KAAK;AACH,aAAO,gBAAAA,KAAC,QAAI,iBAAM;AAAA,IACpB,KAAK;AACH,aAAO,gBAAAA,KAAC,OAAG,iBAAM;AAAA,IACnB,KAAK;AACH,aAAO,gBAAAA,KAAC,OAAG,iBAAM;AAAA,IACnB,KAAK;AACH,aAAO,gBAAAA,KAAC,UAAM,iBAAM;AAAA,IACtB,KAAK,aAAa;AAChB,YAAM,QAAQ,KAAK,OAAO;AAC1B,YAAM,QACJ,OAAO,UAAU,YAAY,MAAM,SAAS,IACxC,EAAE,YAAY,MAAM,IACpB;AACN,aAAO,gBAAAA,KAAC,UAAK,OAAe,iBAAM;AAAA,IACpC;AAAA,IACA,KAAK,aAAa;AAChB,YAAM,QAAQ,KAAK,OAAO;AAC1B,YAAM,QACJ,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,EAAE,MAAM,IAAI;AAC9D,UAAI,CAAC,MAAO,QAAO,gBAAAA,KAAAD,WAAA,EAAG,iBAAM;AAC5B,aAAO,gBAAAC,KAAC,UAAK,OAAe,iBAAM;AAAA,IACpC;AAAA,IACA,KAAK,QAAQ;AACX,YAAM,MAAM,KAAK,OAAO;AACxB,YAAM,OAAO,WAAW,GAAG,IAAI,MAAM;AACrC,aACE,gBAAAA,KAAC,OAAE,MAAY,KAAI,uBAAsB,QAAO,UAC7C,iBACH;AAAA,IAEJ;AAAA,IACA;AACE,aAAO,gBAAAA,KAAC,UAAM,iBAAM;AAAA,EACxB;AACF;","names":["jsx","jsxs","jsx","jsxs","jsx","jsxs","jsx","jsxs","Fragment","jsx","jsxs","formatPublishedDate"]}
|