@roottale/cms-renderer-next 0.46.0 → 0.48.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.
@@ -50,11 +50,55 @@ function collectCollectionCategories(posts, collections, key) {
50
50
  return collectCategoriesWithCounts(owned).filter((c) => allowed.has(c.slug));
51
51
  }
52
52
 
53
+ // src/routes-shared.ts
54
+ import { fetchPosts } from "@roottale/cms-client/server";
55
+ import { resolvePostCollection as resolvePostCollection2, resolvePostUrl as resolvePostUrl2 } from "@roottale/cms-core";
56
+ async function resolveCollectionsResult(input) {
57
+ if (typeof input === "function") {
58
+ try {
59
+ return { collections: await input(), failed: false };
60
+ } catch (err) {
61
+ console.error(
62
+ "[@roottale/cms-renderer-next/routes] collections resolver \uC2E4\uD328:",
63
+ err
64
+ );
65
+ return { collections: void 0, failed: true };
66
+ }
67
+ }
68
+ return { collections: input, failed: false };
69
+ }
70
+ async function resolveCollections(input) {
71
+ return (await resolveCollectionsResult(input)).collections;
72
+ }
73
+ function trimSlash(u) {
74
+ return u.replace(/\/+$/, "");
75
+ }
76
+ async function safeFetchPosts(config, limit) {
77
+ try {
78
+ const page = await fetchPosts({
79
+ apiKey: config.apiKey,
80
+ baseUrl: config.apiBase,
81
+ type: "post",
82
+ limit
83
+ });
84
+ return page.items;
85
+ } catch (err) {
86
+ console.error("[@roottale/cms-renderer-next/routes] fetchPosts \uC2E4\uD328:", err);
87
+ return [];
88
+ }
89
+ }
90
+
53
91
  export {
54
92
  collectCategories,
55
93
  collectCategoriesWithCounts,
56
94
  filterPostsByCategory,
57
95
  defaultCategoryHref,
58
- collectCollectionCategories
96
+ collectCollectionCategories,
97
+ resolveCollectionsResult,
98
+ resolveCollections,
99
+ trimSlash,
100
+ safeFetchPosts,
101
+ resolvePostCollection2 as resolvePostCollection,
102
+ resolvePostUrl2 as resolvePostUrl
59
103
  };
60
- //# sourceMappingURL=chunk-FUA5H6JE.js.map
104
+ //# sourceMappingURL=chunk-U2IM6OCQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/blog-filter.ts","../src/routes-shared.ts"],"sourcesContent":["/**\n * `RootTaleBlogList` 카테고리 필터 — 순수 헬퍼(서버 필터, client JS 불요).\n *\n * SSR-only 정책 유지: 필터는 `?category=<slug>` URL 파라미터로 동작하고(칩 =\n * 서버 렌더 `<a>` 링크), consumer 가 searchParams 의 category 를 `activeCategory`\n * 로 주입한다. hydration 없이 SEO 친화 + 공유 가능한 URL.\n */\n\nimport type { CmsPostContent, CmsTermRef } from \"@roottale/cms-client/server\";\n\nexport interface BlogCategory {\n name: string;\n slug: string;\n}\n\nexport interface BlogCategoryWithCount extends BlogCategory {\n count: number;\n}\n\nfunction categoryTerms(post: CmsPostContent): CmsTermRef[] {\n return (post.terms ?? []).filter((t) => t.taxonomy === \"category\");\n}\n\n/**\n * 글 목록에 등장하는 카테고리를 slug 기준 unique 로 수집. 서버가 보내는 정렬\n * (category → name asc)을 보존하기 위해 처음 등장 순서를 유지한다.\n */\nexport function collectCategories(posts: CmsPostContent[]): BlogCategory[] {\n return collectCategoriesWithCounts(posts).map(({ name, slug }) => ({\n name,\n slug,\n }));\n}\n\nexport function collectCategoriesWithCounts(\n posts: CmsPostContent[],\n): BlogCategoryWithCount[] {\n const bySlug = new Map<string, BlogCategoryWithCount>();\n const out: BlogCategoryWithCount[] = [];\n for (const post of posts) {\n for (const t of categoryTerms(post)) {\n const current = bySlug.get(t.slug);\n if (current) {\n current.count += 1;\n } else {\n const category = { name: t.name, slug: t.slug, count: 1 };\n bySlug.set(t.slug, category);\n out.push(category);\n }\n }\n }\n return out;\n}\n\n/** activeSlug 가 비면 전체 반환. 아니면 그 카테고리를 가진 글만. */\nexport function filterPostsByCategory(\n posts: CmsPostContent[],\n activeSlug: string | null | undefined,\n): CmsPostContent[] {\n if (!activeSlug) return posts;\n return posts.filter((p) => categoryTerms(p).some((t) => t.slug === activeSlug));\n}\n\n/** 기본 카테고리 링크: 전체 → `/blog`, 특정 → `/blog/categories/<slug>`. */\nexport function defaultCategoryHref(slug: string | null): string {\n return slug ? `/blog/categories/${encodeURIComponent(slug)}` : \"/blog\";\n}\n\n// ── Collections (ADR-0060) — cms-core 라우팅 프리미티브 재export + 렌더 헬퍼 ──────\n//\n// 라우팅 코어(RouteCollection/resolvePostCollection/resolvePostUrl/postsForCollection/\n// validateCollections)는 `@roottale/cms-core` 에 있어 next/astro 가 공유한다\n// (ADR-0029 parity — codex consult `019ef577` 반영). 여기서는 next 소비자 편의를\n// 위해 재export + CmsPostContent 기반 스트림 스코프 카테고리 집계만 추가한다.\nimport {\n type RouteCollection,\n postsForCollection,\n} from \"@roottale/cms-core\";\n\nexport {\n type RouteCollection,\n type RoutablePost,\n isInCollectionMode,\n resolvePostCollection,\n resolvePostUrl,\n postsForCollection,\n postsInAnyCollection,\n validateCollections,\n} from \"@roottale/cms-core\";\n\n/**\n * 특정 collection 안에서 등장하는 카테고리 집계 — 그 스트림의 카테고리 칩/아카이브용.\n * **전체 정렬 리스트로 first-wins 해석**해 그 스트림에 최종 귀속된 글만 대상(한 글이\n * 여러 스트림 리스트에 중복되는 것을 방지 — codex 지적).\n */\nexport function collectCollectionCategories(\n posts: CmsPostContent[],\n collections: readonly RouteCollection[],\n key: string,\n): BlogCategoryWithCount[] {\n const owned = postsForCollection(posts, collections, key);\n const collection = collections.find((c) => c.key === key);\n const allowed = new Set(collection?.categories ?? []);\n return collectCategoriesWithCounts(owned).filter((c) => allowed.has(c.slug));\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"],"mappings":";AA0EA;AAAA,EAEE;AAAA,OACK;AAEP;AAAA,EAGE;AAAA,EACA;AAAA,EACA;AAAA,EACA,sBAAAA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AArEP,SAAS,cAAc,MAAoC;AACzD,UAAQ,KAAK,SAAS,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,aAAa,UAAU;AACnE;AAMO,SAAS,kBAAkB,OAAyC;AACzE,SAAO,4BAA4B,KAAK,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO;AAAA,IACjE;AAAA,IACA;AAAA,EACF,EAAE;AACJ;AAEO,SAAS,4BACd,OACyB;AACzB,QAAM,SAAS,oBAAI,IAAmC;AACtD,QAAM,MAA+B,CAAC;AACtC,aAAW,QAAQ,OAAO;AACxB,eAAW,KAAK,cAAc,IAAI,GAAG;AACnC,YAAM,UAAU,OAAO,IAAI,EAAE,IAAI;AACjC,UAAI,SAAS;AACX,gBAAQ,SAAS;AAAA,MACnB,OAAO;AACL,cAAM,WAAW,EAAE,MAAM,EAAE,MAAM,MAAM,EAAE,MAAM,OAAO,EAAE;AACxD,eAAO,IAAI,EAAE,MAAM,QAAQ;AAC3B,YAAI,KAAK,QAAQ;AAAA,MACnB;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,sBACd,OACA,YACkB;AAClB,MAAI,CAAC,WAAY,QAAO;AACxB,SAAO,MAAM,OAAO,CAAC,MAAM,cAAc,CAAC,EAAE,KAAK,CAAC,MAAM,EAAE,SAAS,UAAU,CAAC;AAChF;AAGO,SAAS,oBAAoB,MAA6B;AAC/D,SAAO,OAAO,oBAAoB,mBAAmB,IAAI,CAAC,KAAK;AACjE;AA6BO,SAAS,4BACd,OACA,aACA,KACyB;AACzB,QAAM,QAAQ,mBAAmB,OAAO,aAAa,GAAG;AACxD,QAAM,aAAa,YAAY,KAAK,CAAC,MAAM,EAAE,QAAQ,GAAG;AACxD,QAAM,UAAU,IAAI,IAAI,YAAY,cAAc,CAAC,CAAC;AACpD,SAAO,4BAA4B,KAAK,EAAE,OAAO,CAAC,MAAM,QAAQ,IAAI,EAAE,IAAI,CAAC;AAC7E;;;ACnGA,SAAS,kBAAuC;AAKhD,SAAS,yBAAAC,wBAAuB,kBAAAC,uBAAsB;AAmCtD,eAAsB,yBACpB,OAC0E;AAC1E,MAAI,OAAO,UAAU,YAAY;AAC/B,QAAI;AACF,aAAO,EAAE,aAAa,MAAM,MAAM,GAAG,QAAQ,MAAM;AAAA,IACrD,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN;AAAA,QACA;AAAA,MACF;AACA,aAAO,EAAE,aAAa,QAAW,QAAQ,KAAK;AAAA,IAChD;AAAA,EACF;AACA,SAAO,EAAE,aAAa,OAAO,QAAQ,MAAM;AAC7C;AAOA,eAAsB,mBACpB,OACwC;AACxC,UAAQ,MAAM,yBAAyB,KAAK,GAAG;AACjD;AAEO,SAAS,UAAU,GAAmB;AAC3C,SAAO,EAAE,QAAQ,QAAQ,EAAE;AAC7B;AAEA,eAAsB,eACpB,QACA,OAC2B;AAC3B,MAAI;AACF,UAAM,OAAO,MAAM,WAAW;AAAA,MAC5B,QAAQ,OAAO;AAAA,MACf,SAAS,OAAO;AAAA,MAChB,MAAM;AAAA,MACN;AAAA,IACF,CAAC;AACD,WAAO,KAAK;AAAA,EACd,SAAS,KAAK;AAEZ,YAAQ,MAAM,iEAAuD,GAAG;AACxE,WAAO,CAAC;AAAA,EACV;AACF;","names":["postsForCollection","resolvePostCollection","resolvePostUrl"]}
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ // src/lead-form-meta.client.tsx
4
+ import { useEffect, useRef } from "react";
5
+ import { readAttribution } from "@roottale/cms-client/attribution";
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
+ var JOURNEY_STORAGE_KEY = "_rt_journey";
8
+ function LeadFormMeta({ formId }) {
9
+ const attributionRef = useRef(null);
10
+ const journeyRef = useRef(null);
11
+ const startedRef = useRef(false);
12
+ useEffect(() => {
13
+ try {
14
+ const attribution = readAttribution();
15
+ if (attribution && attributionRef.current) {
16
+ attributionRef.current.value = JSON.stringify(attribution);
17
+ }
18
+ } catch {
19
+ }
20
+ const form = attributionRef.current?.form ?? journeyRef.current?.form;
21
+ if (!form) return;
22
+ const handleFocusIn = () => {
23
+ if (startedRef.current) return;
24
+ startedRef.current = true;
25
+ window.__rtTrack?.("form_start", { form_id: formId });
26
+ };
27
+ const handleFormData = (event) => {
28
+ try {
29
+ const raw = window.sessionStorage.getItem(JOURNEY_STORAGE_KEY);
30
+ event.formData.set("journey", raw ?? "");
31
+ } catch {
32
+ }
33
+ };
34
+ const handleSubmit = () => {
35
+ window.__rtTrack?.("form_submit_attempt", { form_id: formId });
36
+ };
37
+ form.addEventListener("focusin", handleFocusIn);
38
+ form.addEventListener("formdata", handleFormData);
39
+ form.addEventListener("submit", handleSubmit);
40
+ return () => {
41
+ form.removeEventListener("focusin", handleFocusIn);
42
+ form.removeEventListener("formdata", handleFormData);
43
+ form.removeEventListener("submit", handleSubmit);
44
+ };
45
+ }, [formId]);
46
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
47
+ /* @__PURE__ */ jsx("input", { type: "hidden", name: "attribution", ref: attributionRef, defaultValue: "" }),
48
+ /* @__PURE__ */ jsx("input", { type: "hidden", name: "journey", ref: journeyRef, defaultValue: "" })
49
+ ] });
50
+ }
51
+
52
+ export {
53
+ LeadFormMeta
54
+ };
55
+ //# sourceMappingURL=chunk-VG6UUF4D.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lead-form-meta.client.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * `LeadFormMeta` — `RootTaleLeadForm`(RSC, `LeadForm.tsx`) 내부에 심는\n * 어트리뷰션·여정 hidden input 배선 + 폼 계측 발화 island (측정 설계 v2 §2/§3/§5\n * — `measurement-design-draft.md`, 워커 브리프 B). \"LeadForm 어트리뷰션 배선\n * 복구\" — starter `/api/inquiry` 가 지금까지 `attribution`/`journey` 를 전혀\n * 동반하지 않던 끊긴 고리를 여기서 다시 잇는다.\n *\n * RSC 경계(설계 §5): `LeadForm.tsx` 는 계속 서버 컴포넌트로 남고, 이 파일만\n * \"use client\" — form 태그 **내부** 자식으로 삽입한다. tsup 독립 entry\n * (`lead-form-meta-client`, tsup.config.ts) + `esbuild-plugin-preserve-directives`\n * 가 이 청크만 \"use client\" 지시어를 보존한다 — `hero-carousel-client.tsx` 가\n * `templates.js` 에서 격리되는 것과 동일한 관행을 `server.js`(LeadForm 이\n * 속한 entry)에 적용한다.\n *\n * 배선:\n * - `attribution` hidden input — `@roottale/cms-client/attribution` 의\n * `readAttribution()` 으로 localStorage `_rt_attr` first-touch 를 마운트\n * 시 1회 판독해 채운다. first-touch 봉투는 제출 시점까지 안정적이므로\n * 마운트 스냅샷으로 충분하다(설계 §2). `readAttribution()` 을 쓰는 이유 —\n * 비콘이 저장하는 압축 키(`lp/rs/us/um/uc/rf`)를 서버 `parseAttributionJson()`\n * 이 기대하는 확장 키(`landing_path/rt_src/...`)로 변환하는 매핑이 이미\n * 이 함수에 있다(파일 헤더 JSDoc 사용례와 동일 패턴) — 압축 원문을 그대로\n * 넘기면 서버가 못 읽는다.\n * - `journey` hidden input — sessionStorage `_rt_journey` 를 **마운트가\n * 아니라 `formdata` 이벤트 시점에 재직렬화**한다(설계 §3 — 방문 중 계속\n * 쌓이는 값이라 마운트 스냅샷은 진부해진다). hidden input 의 DOM `value`\n * 는 항상 빈 문자열로 SSR/초기 클라 렌더가 일치(hydration-safe) — 실제\n * 전송값은 `formdata` 이벤트에서 `event.formData.set()` 으로 직접 주입해\n * DOM 갱신·리렌더가 필요 없다. `_rt_journey` 키 자체는\n * analytics-runtime(별도 워커가 동시 작업 중이라 이 패키지를 import 하지\n * 않는다)과의 공유 계약 — 값 자체가 계약(문자열 중복은 beacon.ts\n * `ATTRIBUTION_STORAGE_KEY` 를 cms-client `attribution.ts` 가 이미 중복\n * 선언하는 기존 관행과 동일).\n * - `form_start` — form 내 첫 focusin 1회.\n * - `form_submit_attempt` — form `submit` 이벤트(제출 버튼 클릭·Enter 제출\n * 공통 — 브라우저 스펙상 `submit` 이 `formdata` 보다 먼저 fire 한다).\n *\n * `window.__rtTrack` 은 `@roottale/analytics-runtime` 의 비콘 런타임(beacon.ts)\n * 이 노출하는 전역 — 이 패키지가 그 패키지를 직접 import 하지 않고 전역\n * 계약만 소비한다(빌드 의존 최소화 + 비콘 미설정 사이트에서 자동 no-op,\n * `not-found.tsx` `trackCustomEvent` 관행과 동일 계약, 다만 여기선 전역을\n * 직접 optional-chain 호출한다).\n */\nimport { useEffect, useRef } from \"react\";\nimport type { ReactElement } from \"react\";\nimport { readAttribution } from \"@roottale/cms-client/attribution\";\n\n/** sessionStorage 여정 로그 키 — analytics-runtime 관측자와의 공유 계약(설계 §3). */\nconst JOURNEY_STORAGE_KEY = \"_rt_journey\";\n\ndeclare global {\n interface Window {\n /** beacon.ts RUNTIME 이 노출하는 `send()` — 비콘 미설정 사이트에선 undefined. */\n __rtTrack?: (ev: string, extra?: Record<string, unknown>) => void;\n }\n}\n\nexport interface LeadFormMetaProps {\n /** `form_start`/`form_submit_attempt` 차원(form_id). 예: `\"contact\"`. */\n formId: string;\n}\n\n/**\n * `RootTaleLeadForm` 의 `<form>` 내부에 렌더 — hidden input 2개(어트리뷰션·\n * 여정) + 폼 계측 리스너. 시각 출력 없음(레이아웃 영향 0).\n */\nexport function LeadFormMeta({ formId }: LeadFormMetaProps): ReactElement {\n const attributionRef = useRef<HTMLInputElement>(null);\n const journeyRef = useRef<HTMLInputElement>(null);\n const startedRef = useRef(false);\n\n useEffect(() => {\n try {\n const attribution = readAttribution();\n if (attribution && attributionRef.current) {\n attributionRef.current.value = JSON.stringify(attribution);\n }\n } catch {\n /* storage 접근 불가(프라이빗 모드 등) — 무동반 제출, 폼 자체는 정상 동작 */\n }\n\n const form = attributionRef.current?.form ?? journeyRef.current?.form;\n if (!form) return;\n\n const handleFocusIn = (): void => {\n if (startedRef.current) return;\n startedRef.current = true;\n window.__rtTrack?.(\"form_start\", { form_id: formId });\n };\n\n const handleFormData = (event: FormDataEvent): void => {\n // 설계 §3 — 마운트 스냅샷이 아니라 제출 시점 재직렬화. FormData 엔트리를\n // 직접 덮어써 hidden input 의 실제 DOM value 갱신 없이 최신값을 싣는다.\n try {\n const raw = window.sessionStorage.getItem(JOURNEY_STORAGE_KEY);\n event.formData.set(\"journey\", raw ?? \"\");\n } catch {\n /* noop — journey 없이 제출(문의 저장 자체는 서버에서 계속 진행) */\n }\n };\n\n const handleSubmit = (): void => {\n window.__rtTrack?.(\"form_submit_attempt\", { form_id: formId });\n };\n\n form.addEventListener(\"focusin\", handleFocusIn);\n form.addEventListener(\"formdata\", handleFormData as EventListener);\n form.addEventListener(\"submit\", handleSubmit);\n\n return () => {\n form.removeEventListener(\"focusin\", handleFocusIn);\n form.removeEventListener(\"formdata\", handleFormData as EventListener);\n form.removeEventListener(\"submit\", handleSubmit);\n };\n }, [formId]);\n\n return (\n <>\n <input type=\"hidden\" name=\"attribution\" ref={attributionRef} defaultValue=\"\" />\n <input type=\"hidden\" name=\"journey\" ref={journeyRef} defaultValue=\"\" />\n </>\n );\n}\n"],"mappings":";AA6CA,SAAS,WAAW,cAAc;AAElC,SAAS,uBAAuB;AAwE5B,mBACE,KADF;AArEJ,IAAM,sBAAsB;AAkBrB,SAAS,aAAa,EAAE,OAAO,GAAoC;AACxE,QAAM,iBAAiB,OAAyB,IAAI;AACpD,QAAM,aAAa,OAAyB,IAAI;AAChD,QAAM,aAAa,OAAO,KAAK;AAE/B,YAAU,MAAM;AACd,QAAI;AACF,YAAM,cAAc,gBAAgB;AACpC,UAAI,eAAe,eAAe,SAAS;AACzC,uBAAe,QAAQ,QAAQ,KAAK,UAAU,WAAW;AAAA,MAC3D;AAAA,IACF,QAAQ;AAAA,IAER;AAEA,UAAM,OAAO,eAAe,SAAS,QAAQ,WAAW,SAAS;AACjE,QAAI,CAAC,KAAM;AAEX,UAAM,gBAAgB,MAAY;AAChC,UAAI,WAAW,QAAS;AACxB,iBAAW,UAAU;AACrB,aAAO,YAAY,cAAc,EAAE,SAAS,OAAO,CAAC;AAAA,IACtD;AAEA,UAAM,iBAAiB,CAAC,UAA+B;AAGrD,UAAI;AACF,cAAM,MAAM,OAAO,eAAe,QAAQ,mBAAmB;AAC7D,cAAM,SAAS,IAAI,WAAW,OAAO,EAAE;AAAA,MACzC,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,UAAM,eAAe,MAAY;AAC/B,aAAO,YAAY,uBAAuB,EAAE,SAAS,OAAO,CAAC;AAAA,IAC/D;AAEA,SAAK,iBAAiB,WAAW,aAAa;AAC9C,SAAK,iBAAiB,YAAY,cAA+B;AACjE,SAAK,iBAAiB,UAAU,YAAY;AAE5C,WAAO,MAAM;AACX,WAAK,oBAAoB,WAAW,aAAa;AACjD,WAAK,oBAAoB,YAAY,cAA+B;AACpE,WAAK,oBAAoB,UAAU,YAAY;AAAA,IACjD;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,SACE,iCACE;AAAA,wBAAC,WAAM,MAAK,UAAS,MAAK,eAAc,KAAK,gBAAgB,cAAa,IAAG;AAAA,IAC7E,oBAAC,WAAM,MAAK,UAAS,MAAK,WAAU,KAAK,YAAY,cAAa,IAAG;AAAA,KACvE;AAEJ;","names":[]}
@@ -362,6 +362,53 @@
362
362
  overflow-wrap: anywhere;
363
363
  }
364
364
 
365
+ /* 시각 브레드크럼 (avcd 구조, opt-in — RootTaleBlogPost `breadcrumb` prop). */
366
+ [data-roottale-cms] :where(.rt-cms-breadcrumb) {
367
+ max-inline-size: var(--rt-cms-measure, 42.5rem);
368
+ margin-inline: auto;
369
+ margin-block-end: 1rem;
370
+ font-family: var(--rt-font-body, system-ui, sans-serif);
371
+ font-size: 0.8125rem;
372
+ color: color-mix(in srgb, var(--rt-color-foreground, #06162f) 52%, transparent);
373
+ }
374
+
375
+ [data-roottale-cms] :where(.rt-cms-breadcrumb ol) {
376
+ display: flex;
377
+ flex-wrap: wrap;
378
+ align-items: center;
379
+ list-style: none;
380
+ margin: 0;
381
+ padding: 0;
382
+ }
383
+
384
+ [data-roottale-cms] :where(.rt-cms-breadcrumb li) {
385
+ display: inline-flex;
386
+ align-items: center;
387
+ }
388
+
389
+ [data-roottale-cms] :where(.rt-cms-breadcrumb li + li)::before {
390
+ content: "›";
391
+ margin-inline: 0.5rem;
392
+ color: color-mix(in srgb, var(--rt-color-foreground, #06162f) 40%, transparent);
393
+ }
394
+
395
+ [data-roottale-cms] :where(.rt-cms-breadcrumb a) {
396
+ color: inherit;
397
+ text-decoration: none;
398
+ }
399
+
400
+ [data-roottale-cms] :where(.rt-cms-breadcrumb a:hover) {
401
+ color: var(--rt-color-foreground, #06162f);
402
+ }
403
+
404
+ /* 마지막 항목(현재 글) — 긴 제목은 1줄 말줄임, 모바일은 자연 줄바꿈 허용. */
405
+ [data-roottale-cms] :where(.rt-cms-breadcrumb li[aria-current="page"]) {
406
+ overflow: hidden;
407
+ text-overflow: ellipsis;
408
+ white-space: nowrap;
409
+ max-inline-size: 18rem;
410
+ }
411
+
365
412
  /* 네이버 프리미엄콘텐츠 류 크롬리스 헤더 — 장식 카드 대신 타이포+헤어라인. */
366
413
  [data-roottale-cms] :where(.rt-cms-post-header) {
367
414
  max-inline-size: var(--rt-cms-measure, 42.5rem);
@@ -392,8 +439,9 @@
392
439
  font-variant-numeric: tabular-nums;
393
440
  }
394
441
 
442
+ /* 네이버 프리미엄콘텐츠·avcd 기준 문단 간 ≈24px. */
395
443
  [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] > * + *) {
396
- margin-block-start: 0.9em;
444
+ margin-block-start: 1.35em;
397
445
  }
398
446
 
399
447
  /* 읽기 measure — rem 고정값. 72ch 는 각 블록의 font-size 기준으로 해석돼
@@ -541,22 +589,22 @@
541
589
  }
542
590
 
543
591
  [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] h2) {
544
- margin-block: 1.8em 0.55em;
592
+ margin-block: 2.8em 0.7em;
545
593
  font-size: var(--rt-font-size-3xl, 1.875rem);
546
594
  font-weight: var(--rt-font-weight-bold, 700);
547
595
  }
548
596
 
549
597
  [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] h3) {
550
- margin-block: 1.4em 0.4em;
598
+ margin-block: 1.7em 0.5em;
551
599
  font-family: var(--rt-font-body, system-ui, sans-serif);
552
- font-size: var(--rt-font-size-xl, 1.25rem);
600
+ font-size: 1.375rem;
553
601
  font-weight: var(--rt-font-weight-semibold, 600);
554
602
  }
555
603
 
556
604
  [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] h4) {
557
- margin-block: 1.15em 0.3em;
605
+ margin-block: 1.4em 0.35em;
558
606
  font-family: var(--rt-font-body, system-ui, sans-serif);
559
- font-size: var(--rt-font-size-lg, 1.125rem);
607
+ font-size: 1.1875rem;
560
608
  font-weight: var(--rt-font-weight-semibold, 600);
561
609
  }
562
610
 
@@ -569,7 +617,7 @@
569
617
  [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] h2 + p),
570
618
  [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] h3 + p),
571
619
  [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] h4 + p) {
572
- margin-block-start: 0.35em;
620
+ margin-block-start: 0;
573
621
  }
574
622
 
575
623
  [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] ul),
@@ -599,18 +647,42 @@
599
647
  text-wrap: pretty;
600
648
  }
601
649
 
650
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] li + li) {
651
+ margin-block-start: 0.4em;
652
+ }
653
+
602
654
  [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] li > p) {
603
655
  margin: 0;
604
656
  }
605
657
 
658
+ /* avcd 식 액센트 마커. */
659
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] ul li)::marker {
660
+ color: color-mix(in srgb, var(--rt-color-primary, #06162f) 72%, transparent);
661
+ }
662
+
663
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] ol li)::marker {
664
+ color: color-mix(in srgb, var(--rt-color-primary, #06162f) 85%, transparent);
665
+ font-weight: 600;
666
+ font-variant-numeric: tabular-nums;
667
+ }
668
+
606
669
  [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] blockquote) {
607
- border-inline-start: 2px solid color-mix(in srgb, var(--rt-color-primary, #06162f) 52%, var(--rt-color-border, #dceef3));
608
- padding-inline-start: var(--rt-spacing-4, 1rem);
609
- padding-block: 0.125rem;
610
- color: color-mix(in srgb, var(--rt-color-foreground, #06162f) 76%, transparent);
670
+ border: 1px solid color-mix(in srgb, var(--rt-color-border, #dceef3) 88%, transparent);
671
+ border-radius: min(var(--rt-radius-md, 0.5rem), 0.625rem);
672
+ background: color-mix(in srgb, var(--rt-color-muted, #f7f8f7) 32%, transparent);
673
+ padding: 1.125rem 1.25rem;
674
+ /* 본문 최상위 블록 — margin 단축 금지(중앙 정렬 유지), margin-block 만. */
675
+ margin-block: 1.6em;
676
+ color: color-mix(in srgb, var(--rt-color-foreground, #06162f) 82%, transparent);
611
677
  /* font-style: italic 제거 — 한글 가짜 기울임 방지. */
612
678
  }
613
679
 
680
+ /* 카드 내부 리듬 — 본문 owl(> * + *)은 최상위 전용이라 인용구 안 문단 간격은
681
+ 여기서 별도 부여. */
682
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] blockquote > * + *) {
683
+ margin-block-start: 0.75em;
684
+ }
685
+
614
686
  [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] a) {
615
687
  color: var(--rt-color-primary, #06162f);
616
688
  text-decoration: underline;
@@ -674,12 +746,31 @@
674
746
  max-inline-size: 100%;
675
747
  block-size: auto;
676
748
  border-radius: var(--rt-radius-md, 0.5rem);
749
+ margin-block: 2em;
750
+ }
751
+
752
+ /* figure — 최상위 블록. margin 은 figure 가 소유(margin-block 만, 중앙 정렬 유지). */
753
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] figure) {
754
+ margin-block: 2em;
755
+ }
756
+
757
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] figure img) {
758
+ margin-block: 0;
759
+ }
760
+
761
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] figcaption) {
762
+ margin-block-start: 0.625rem;
763
+ text-align: center;
764
+ color: color-mix(in srgb, var(--rt-color-foreground, #06162f) 52%, transparent);
765
+ font-size: 0.875rem;
766
+ line-height: 1.55;
767
+ /* italic 쓰지 말 것 — 한글 가짜 기울임 금지. */
677
768
  }
678
769
 
679
770
  [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] hr) {
680
771
  border: 0;
681
- border-block-start: 1px solid var(--rt-color-border, #dceef3);
682
- margin-block: var(--rt-spacing-7, 2.25rem);
772
+ border-block-start: 1px solid color-mix(in srgb, var(--rt-color-border, #dceef3) 90%, transparent);
773
+ margin-block: 2.5rem;
683
774
  }
684
775
 
685
776
  /* 본문 텍스트 선택 색 — 프라이머리 색으로 브랜드 일관성. */
@@ -698,23 +789,43 @@
698
789
  [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] .rt-table),
699
790
  [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] table) {
700
791
  inline-size: 100%;
701
- border-collapse: collapse;
702
- font-size: 0.95em;
792
+ border-collapse: separate;
793
+ border-spacing: 0;
794
+ border: 1px solid var(--rt-color-border, #dceef3);
795
+ border-radius: min(var(--rt-radius-md, 0.5rem), 0.625rem);
796
+ overflow: hidden;
797
+ font-size: 0.9375em;
703
798
  }
704
799
 
705
800
  [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] .rt-table th),
706
801
  [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] .rt-table td),
707
802
  [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] table th),
708
803
  [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] table td) {
709
- border: 1px solid var(--rt-color-border, #dceef3);
710
- padding: var(--rt-spacing-2, 0.5rem) var(--rt-spacing-3, 0.75rem);
804
+ border: 0;
805
+ border-block-end: 1px solid color-mix(in srgb, var(--rt-color-border, #dceef3) 80%, transparent);
806
+ border-inline-end: 1px solid color-mix(in srgb, var(--rt-color-border, #dceef3) 55%, transparent);
807
+ padding: 0.625rem 0.875rem;
711
808
  text-align: start;
712
809
  vertical-align: top;
713
810
  }
714
811
 
812
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] .rt-table th:last-child),
813
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] .rt-table td:last-child),
814
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] table th:last-child),
815
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] table td:last-child) {
816
+ border-inline-end: 0;
817
+ }
818
+
819
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] .rt-table tr:last-child > th),
820
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] .rt-table tr:last-child > td),
821
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] table tr:last-child > th),
822
+ [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] table tr:last-child > td) {
823
+ border-block-end: 0;
824
+ }
825
+
715
826
  [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] .rt-table th),
716
827
  [data-roottale-cms] :where(.rt-cms-article [data-roottale-cms="body"] table th) {
717
- background: color-mix(in srgb, var(--rt-color-muted, #f7f8f7) 70%, white);
828
+ background: color-mix(in srgb, var(--rt-color-muted, #f7f8f7) 72%, white);
718
829
  font-weight: 600;
719
830
  }
720
831
 
@@ -1137,6 +1248,14 @@
1137
1248
  [data-roottale-cms] :where([data-roottale-cms="body"]) {
1138
1249
  font-size: var(--rt-cms-body-size, calc(var(--rt-font-size-base, 1rem) * 1.0625));
1139
1250
  }
1251
+
1252
+ /* 브레드크럼 — 좁은 화면은 말줄임 대신 자연 줄바꿈 허용(가로 스크롤 방지). */
1253
+ [data-roottale-cms] :where(.rt-cms-breadcrumb li[aria-current="page"]) {
1254
+ overflow: visible;
1255
+ text-overflow: clip;
1256
+ white-space: normal;
1257
+ max-inline-size: 100%;
1258
+ }
1140
1259
  }
1141
1260
 
1142
1261
  /* RootTaleFloatingCta */
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { C as ConcernsSection } from './site-config-CeByrPJF.js';
2
+ import { C as ConcernsSection } from './site-config-Dicv57oY.js';
3
3
  import 'zod';
4
4
 
5
5
  type ConcernRegion = ConcernsSection["props"]["regions"][number];
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { E as ExpertiseSection } from './site-config-CeByrPJF.js';
2
+ import { E as ExpertiseSection } from './site-config-Dicv57oY.js';
3
3
  import 'zod';
4
4
 
5
5
  type ExpertiseItem = ExpertiseSection["props"]["items"][number];
@@ -0,0 +1,19 @@
1
+ import { ReactElement } from 'react';
2
+
3
+ declare global {
4
+ interface Window {
5
+ /** beacon.ts RUNTIME 이 노출하는 `send()` — 비콘 미설정 사이트에선 undefined. */
6
+ __rtTrack?: (ev: string, extra?: Record<string, unknown>) => void;
7
+ }
8
+ }
9
+ interface LeadFormMetaProps {
10
+ /** `form_start`/`form_submit_attempt` 차원(form_id). 예: `"contact"`. */
11
+ formId: string;
12
+ }
13
+ /**
14
+ * `RootTaleLeadForm` 의 `<form>` 내부에 렌더 — hidden input 2개(어트리뷰션·
15
+ * 여정) + 폼 계측 리스너. 시각 출력 없음(레이아웃 영향 0).
16
+ */
17
+ declare function LeadFormMeta({ formId }: LeadFormMetaProps): ReactElement;
18
+
19
+ export { LeadFormMeta, type LeadFormMetaProps };
@@ -0,0 +1,8 @@
1
+ "use client";
2
+ import {
3
+ LeadFormMeta
4
+ } from "./chunk-VG6UUF4D.js";
5
+ export {
6
+ LeadFormMeta
7
+ };
8
+ //# sourceMappingURL=lead-form-meta-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/dist/routes.d.ts CHANGED
@@ -426,6 +426,20 @@ interface PostMetadataInput {
426
426
  * `alternates.languages` 를 계산한다. 없거나 자기 자신 1건뿐이면 languages 생략.
427
427
  */
428
428
  translations?: readonly CmsPostTranslationRef[];
429
+ /**
430
+ * avcd 구조(og article 확장) — 최근 수정 ISO 일시 → `openGraph.modifiedTime`.
431
+ * `CmsPostContent.updatedAt` 등을 그대로 넘기면 된다. 미지정 시 생략.
432
+ */
433
+ modified?: string | null;
434
+ /** avcd 구조 — 글 섹션/카테고리명 → `openGraph.section`. 미지정 시 생략. */
435
+ section?: string | null;
436
+ /** avcd 구조 — 글 태그명 목록 → `openGraph.tags`. 빈 배열/미지정 시 생략. */
437
+ tags?: readonly string[] | null;
438
+ /**
439
+ * avcd 구조 — 저자 프로필 URL 목록 → `openGraph.authors` (Next `Metadata`
440
+ * article 확장). 빈 배열/미지정 시 생략.
441
+ */
442
+ authors?: readonly string[] | null;
429
443
  }
430
444
  interface BuildPostMetadataOptions {
431
445
  /**
@@ -452,6 +466,12 @@ interface BuildPostMetadataOptions {
452
466
  * (opt-in — 명시 안 한 기존 소비자는 canonical-only 로 diff 0).
453
467
  */
454
468
  locales?: PostLocaleOptions;
469
+ /**
470
+ * avcd 구조 — RSS 피드 절대 경로 → `alternates.types["application/rss+xml"]`.
471
+ * 기본 `"/feed.xml"`. `siteUrl` 이 있으면 canonical 유무와 무관하게 항상 emit
472
+ * (RSS 발견성은 개별 글의 canonical 해석 성공 여부와 무관한 사이트 차원 신호).
473
+ */
474
+ feedPath?: string;
455
475
  }
456
476
  /**
457
477
  * ADR-0052 A안 (W4-6 PR C1) — hreflang 경로 계산 옵션. `buildPostMetadata` 와
@@ -472,8 +492,11 @@ interface PostMetadata {
472
492
  title: string;
473
493
  description?: string;
474
494
  alternates?: {
475
- canonical: string;
495
+ /** `types` 만 emit 되는 경우(예: siteUrl 만 있고 path/collections 미해석)도 있어 optional. */
496
+ canonical?: string;
476
497
  languages?: Record<string, string>;
498
+ /** avcd 구조 — RSS 등 대체 표현. Next `Metadata.alternates.types` 그대로. */
499
+ types?: Record<string, string>;
477
500
  };
478
501
  robots?: {
479
502
  index: boolean;
@@ -485,11 +508,27 @@ interface PostMetadata {
485
508
  description?: string;
486
509
  url?: string;
487
510
  publishedTime?: string;
511
+ /** avcd 구조 — og article 확장. */
512
+ modifiedTime?: string;
513
+ section?: string;
514
+ tags?: string[];
515
+ authors?: string[];
488
516
  images?: {
489
517
  url: string;
490
518
  alt: string;
491
519
  }[];
492
520
  };
521
+ /**
522
+ * avcd 구조 — Twitter Card. og 와 동일 소스(seo override → 글 값)에서 title/
523
+ * description/images 를 채운다. `card`+`title` 은 항상 emit(og 이미지 유무와
524
+ * 무관), `images` 는 og 이미지가 있을 때만.
525
+ */
526
+ twitter?: {
527
+ card: "summary_large_image";
528
+ title: string;
529
+ description?: string;
530
+ images?: string[];
531
+ };
493
532
  /** ADR-0065 — `<meta name="rt:content-id">` 등 임의 head meta (조회수 식별자). */
494
533
  other?: Record<string, string>;
495
534
  }
@@ -502,6 +541,9 @@ interface PostMetadata {
502
541
  * → 없으면 생략. 모든 글이 자기 자신을 가리키는 canonical 을 갖는 게 권장이므로
503
542
  * `siteUrl` 과 함께 `path` 또는 `collections` 전달을 권장한다.
504
543
  * - `noindex`/`nofollow` 중 하나라도 켜지면 `robots` 출력.
544
+ * - avcd 구조 — `siteUrl` 이 있으면 `alternates.types["application/rss+xml"]`
545
+ * (기본 `/feed.xml`, `feedPath` 로 override)을 canonical 유무와 무관하게 emit.
546
+ * Twitter Card(`summary_large_image`)는 og 와 같은 소스로 항상 emit.
505
547
  *
506
548
  * ```ts
507
549
  * // 단일 블로그(레거시): 라우트가 곧 섹션이므로 path 를 직접 준다.
package/dist/routes.js CHANGED
@@ -1,54 +1,22 @@
1
1
  import {
2
2
  collectCategories,
3
- collectCollectionCategories
4
- } from "./chunk-FUA5H6JE.js";
3
+ collectCollectionCategories,
4
+ resolveCollections,
5
+ resolveCollectionsResult,
6
+ resolvePostCollection,
7
+ resolvePostUrl,
8
+ safeFetchPosts,
9
+ trimSlash
10
+ } from "./chunk-U2IM6OCQ.js";
5
11
 
6
12
  // src/routes.ts
7
13
  import { CMS_CLIENT_VERSION } from "@roottale/cms-client/server";
8
14
 
9
- // src/routes-shared.ts
10
- import { fetchPosts } from "@roottale/cms-client/server";
11
- import { resolvePostCollection, resolvePostUrl } from "@roottale/cms-core";
12
- async function resolveCollectionsResult(input) {
13
- if (typeof input === "function") {
14
- try {
15
- return { collections: await input(), failed: false };
16
- } catch (err) {
17
- console.error(
18
- "[@roottale/cms-renderer-next/routes] collections resolver \uC2E4\uD328:",
19
- err
20
- );
21
- return { collections: void 0, failed: true };
22
- }
23
- }
24
- return { collections: input, failed: false };
25
- }
26
- async function resolveCollections(input) {
27
- return (await resolveCollectionsResult(input)).collections;
28
- }
29
- function trimSlash(u) {
30
- return u.replace(/\/+$/, "");
31
- }
32
- async function safeFetchPosts(config, limit) {
33
- try {
34
- const page = await fetchPosts({
35
- apiKey: config.apiKey,
36
- baseUrl: config.apiBase,
37
- type: "post",
38
- limit
39
- });
40
- return page.items;
41
- } catch (err) {
42
- console.error("[@roottale/cms-renderer-next/routes] fetchPosts \uC2E4\uD328:", err);
43
- return [];
44
- }
45
- }
46
-
47
15
  // src/routes-feed-sitemap.ts
48
16
  import {
49
17
  fetchAuthors,
50
18
  fetchBlogSettings,
51
- fetchPosts as fetchPosts2,
19
+ fetchPosts,
52
20
  generateRssXml,
53
21
  rssItemsFromPosts
54
22
  } from "@roottale/cms-client/server";
@@ -83,6 +51,10 @@ function buildPostMetadata(post, options = {}) {
83
51
  if (description) openGraph.description = description;
84
52
  if (canonical) openGraph.url = canonical;
85
53
  if (post.date) openGraph.publishedTime = post.date;
54
+ if (post.modified) openGraph.modifiedTime = post.modified;
55
+ if (post.section) openGraph.section = post.section;
56
+ if (post.tags && post.tags.length > 0) openGraph.tags = [...post.tags];
57
+ if (post.authors && post.authors.length > 0) openGraph.authors = [...post.authors];
86
58
  if (ogImage) openGraph.images = [{ url: ogImage, alt: title }];
87
59
  const meta = { title, openGraph };
88
60
  if (description) meta.description = description;
@@ -97,10 +69,24 @@ function buildPostMetadata(post, options = {}) {
97
69
  if (languages) meta.alternates.languages = languages;
98
70
  }
99
71
  }
72
+ if (options.siteUrl) {
73
+ const feedPath = options.feedPath ?? "/feed.xml";
74
+ const normalizedFeedPath = feedPath.startsWith("/") ? feedPath : `/${feedPath}`;
75
+ meta.alternates = {
76
+ ...meta.alternates,
77
+ types: {
78
+ "application/rss+xml": `${trimSlash(options.siteUrl)}${normalizedFeedPath}`
79
+ }
80
+ };
81
+ }
100
82
  if (seo?.noindex || seo?.nofollow) {
101
83
  meta.robots = { index: !seo.noindex, follow: !seo.nofollow };
102
84
  }
103
85
  if (post.id) meta.other = { "rt:content-id": post.id };
86
+ const twitter = { card: "summary_large_image", title };
87
+ if (description) twitter.description = description;
88
+ if (ogImage) twitter.images = [ogImage];
89
+ meta.twitter = twitter;
104
90
  return meta;
105
91
  }
106
92
  function buildHreflangLanguages(translations, siteUrl, locales) {
@@ -185,7 +171,7 @@ async function localesWithPublishedContent(config, locales) {
185
171
  const checks = await Promise.all(
186
172
  locales.map(async (locale) => {
187
173
  try {
188
- const page = await fetchPosts2({
174
+ const page = await fetchPosts({
189
175
  apiKey: config.apiKey,
190
176
  baseUrl: config.apiBase,
191
177
  type: "post",
@@ -671,7 +657,7 @@ function createFleetInfoRoute(config = {}) {
671
657
  site: config.site ?? env.VERCEL_PROJECT_PRODUCTION_URL ?? null,
672
658
  framework: "next",
673
659
  cms_client: CMS_CLIENT_VERSION,
674
- cms_renderer: true ? "0.46.0" : "dev",
660
+ cms_renderer: true ? "0.48.0" : "dev",
675
661
  build_sha: env.VERCEL_GIT_COMMIT_SHA?.slice(0, 7) ?? null
676
662
  },
677
663
  { headers: { "cache-control": "public, max-age=300" } }