@roottale/cms-renderer-next 0.53.0 → 0.55.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/dist/server.d.ts CHANGED
@@ -3,7 +3,8 @@ import { CmsPostContent, RootTaleTheme, CmsPostType } from '@roottale/cms-client
3
3
  export { RootTaleTheme } from '@roottale/cms-client/server';
4
4
  import { RootTaleThemeStyle } from '@roottale/ui-react/theme';
5
5
  import { Block, BindingResolver, RouteCollection } from '@roottale/cms-core';
6
- export { a as BlogCategory, B as BlogCategoryWithCount, c as collectCategories, b as collectCategoriesWithCounts, d as defaultCategoryHref, f as filterPostsByCategory } from './blog-filter-BKCGM0qd.js';
6
+ export { a as BlogCategory, B as BlogCategoryWithCount, c as collectCategories, b as collectCategoriesWithCounts, d as defaultCategoryHref, f as filterPostsByCategory } from './blog-filter-BQNQL3Mk.js';
7
+ import * as react_jsx_runtime from 'react/jsx-runtime';
7
8
 
8
9
  /**
9
10
  * `<RootTalePostCard>` — server-only summary card for a single CmsPostContent.
@@ -237,7 +238,16 @@ declare function RootTaleBlogCategories(props: RootTaleBlogCategoriesProps): Pro
237
238
 
238
239
  type RootTaleBlogPostProps = {
239
240
  apiKey: string;
240
- slugOrId: string;
241
+ /** 발행 글 slug 또는 id. `previewToken` 을 줄 때는 생략할 수 있다. */
242
+ slugOrId?: string;
243
+ /**
244
+ * 어드민 편집기가 발급한 미리보기 토큰(URL `?token=`). 주면 `slugOrId` 대신
245
+ * `fetchPostPreview` 로 초안을 받아 **같은 템플릿**으로 그린다 — 미리보기 = 결과물.
246
+ * 만료(410)면 `previewExpiredElement`, 없으면(404) `notFoundElement`.
247
+ */
248
+ previewToken?: string;
249
+ /** 미리보기 토큰이 만료됐을 때 대신 그릴 요소(기본: 만료 안내 문구). */
250
+ previewExpiredElement?: ReactElement;
241
251
  baseUrl?: string;
242
252
  notFoundElement?: ReactElement;
243
253
  showTitle?: boolean;
@@ -269,6 +279,21 @@ declare function RootTaleFaqJsonLd({ doc, }: {
269
279
  readonly doc: unknown;
270
280
  }): ReactElement | null;
271
281
 
282
+ interface RootTalePreviewNoticeProps {
283
+ /** `ko`(기본) · `en`. */
284
+ locale?: string;
285
+ /** 기본 안내 문구를 바꾸고 싶을 때. */
286
+ message?: string;
287
+ /** 토큰 만료 시각(ISO) — 주면 "HH:MM까지" 를 덧붙인다. */
288
+ expiresAt?: string;
289
+ }
290
+ /**
291
+ * 미리보기 화면 상단 안내 띠. 본문 템플릿은 발행 화면과 완전히 같게 두고, "이건
292
+ * 미리보기다" 라는 신호만 이 띠 하나로 준다. 검색엔진 색인 차단은 metadata
293
+ * (`buildPreviewMetadata`) 몫이며, 이 컴포넌트는 사람용 안내다.
294
+ */
295
+ declare function RootTalePreviewNotice({ locale, message, expiresAt, }: RootTalePreviewNoticeProps): ReactElement;
296
+
272
297
  type RootTalePageProps = {
273
298
  apiKey: string;
274
299
  slugOrId: string;
@@ -277,9 +302,23 @@ type RootTalePageProps = {
277
302
  showTitle?: boolean;
278
303
  theme?: RootTaleThemeInput;
279
304
  locale?: string;
305
+ /** 페이지 JSON을 렌더하기 직전 콘텐츠 참조를 실제 섹션 props로 바꾸는 서버 함수. */
306
+ prepareBody?: (document: Record<string, unknown>) => Promise<Record<string, unknown>>;
280
307
  };
281
308
  declare function RootTalePage(props: RootTalePageProps): Promise<ReactElement>;
282
309
 
310
+ interface RootTaleExposureSlotProps {
311
+ readonly apiKey: string;
312
+ readonly baseUrl?: string;
313
+ readonly siteId?: string;
314
+ readonly slotKey: string;
315
+ readonly path: string;
316
+ readonly allowedVariants: readonly string[];
317
+ readonly revalidate?: number;
318
+ }
319
+ /** FRONT가 코드로 만든 슬롯 안에서만 활성 배너·팝업을 렌더한다. */
320
+ declare function RootTaleExposureSlot(props: RootTaleExposureSlotProps): Promise<react_jsx_runtime.JSX.Element | null>;
321
+
283
322
  type RenderTiptapProps = {
284
323
  readonly doc: Record<string, unknown>;
285
324
  readonly fields?: Record<string, unknown> | null;
@@ -288,4 +327,33 @@ type RenderTiptapProps = {
288
327
  };
289
328
  declare function RenderTiptap({ doc, fields, apiKey, baseUrl, }: RenderTiptapProps): ReactElement;
290
329
 
291
- export { type BlogPostBreadcrumbOptions, type CtaButton, type CtaButtonType, type FloatingCtaPosition, type LeadFormVertical, RenderTiptap, RootTaleBlogCategories, type RootTaleBlogCategoriesProps, RootTaleBlogList, type RootTaleBlogListProps, RootTaleBlogPost, type RootTaleBlogPostProps, type RootTaleCssVars, RootTaleFaqJsonLd, RootTaleFloatingCta, type RootTaleFloatingCtaProps, RootTaleLeadForm, type RootTaleLeadFormProps, RootTalePage, type RootTalePageProps, RootTalePostCard, type RootTalePostCardProps, RootTaleTableOfContents, type RootTaleTableOfContentsProps, type RootTaleThemeInput, RootTaleThemeProvider, type TocEntry, attachHeadingIds, extractToc, headingToId, renderBlock, renderBlocks, renderBoundBlocks, themeToCssVars };
330
+ /**
331
+ * 본문 렌더 소스 결정 — HTML 정본 우선, 없으면 Block JSON 폴백.
332
+ *
333
+ * 배경: 외부에서 이관한 글은 **HTML 이 정본**이고 body_json 은 그 파생/대체본이다.
334
+ * 파생본은 표·중첩 목록 같은 구조에서 원문과 미묘하게 달라질 수 있으므로, 원문이
335
+ * 남아 있으면 그것을 보여주는 편이 정확하다. 반대로 에디터에서 편집된 글은
336
+ * body_html 이 비워지므로(저장 경로가 무효화) 자동으로 JSON 경로로 돌아온다.
337
+ *
338
+ * 안전성: HTML 은 신뢰하지 않는다. 최종 sink 는 공용 `sanitizeHtml` 하나뿐이며
339
+ * (cms-core), 렌더 직전 여기서 통과시킨다 — 저장 시점에 정제됐더라도 다시 한다
340
+ * (저장 경로가 여럿이고 sanitizer 규칙은 갱신되므로, 방어는 렌더 시점 기준).
341
+ *
342
+ * sanitize 결과가 실질적으로 비면(태그만 남고 내용이 없음) HTML 을 쓰지 않고
343
+ * JSON 으로 폴백한다 — 빈 화면보다 파생본이 낫다.
344
+ */
345
+ type PostBodySource = {
346
+ readonly kind: "html";
347
+ readonly html: string;
348
+ } | {
349
+ readonly kind: "json";
350
+ };
351
+ /**
352
+ * 렌더에 쓸 본문 소스를 고른다.
353
+ *
354
+ * @param bodyHtml 원문 HTML 정본 (null/미포함 = 구 서버이거나 JSON 이 정본).
355
+ * @returns `kind: "html"` 이면 `html` 은 **이미 sanitize 된** 값이라 그대로 주입 가능.
356
+ */
357
+ declare function resolvePostBodySource(bodyHtml: string | null | undefined): PostBodySource;
358
+
359
+ export { type BlogPostBreadcrumbOptions, type CtaButton, type CtaButtonType, type FloatingCtaPosition, type LeadFormVertical, type PostBodySource, RenderTiptap, RootTaleBlogCategories, type RootTaleBlogCategoriesProps, RootTaleBlogList, type RootTaleBlogListProps, RootTaleBlogPost, type RootTaleBlogPostProps, type RootTaleCssVars, RootTaleExposureSlot, type RootTaleExposureSlotProps, RootTaleFaqJsonLd, RootTaleFloatingCta, type RootTaleFloatingCtaProps, RootTaleLeadForm, type RootTaleLeadFormProps, RootTalePage, type RootTalePageProps, RootTalePostCard, type RootTalePostCardProps, RootTalePreviewNotice, type RootTalePreviewNoticeProps, RootTaleTableOfContents, type RootTaleTableOfContentsProps, type RootTaleThemeInput, RootTaleThemeProvider, type TocEntry, attachHeadingIds, extractToc, headingToId, renderBlock, renderBlocks, renderBoundBlocks, resolvePostBodySource, themeToCssVars };
package/dist/server.js CHANGED
@@ -5,13 +5,16 @@ import {
5
5
  defaultCategoryHref,
6
6
  filterPostsByCategory,
7
7
  trimSlash
8
- } from "./chunk-HM5VP7SR.js";
8
+ } from "./chunk-FJANIAZN.js";
9
9
  import {
10
10
  getSectionNodeRenderer
11
11
  } from "./chunk-KSPPYX22.js";
12
12
  import {
13
13
  LeadFormMeta
14
14
  } from "./chunk-VG6UUF4D.js";
15
+ import {
16
+ RootTaleExposure
17
+ } from "./chunk-GVKENK6H.js";
15
18
 
16
19
  // src/PostCard.tsx
17
20
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -81,6 +84,8 @@ var DICTS = {
81
84
  relatedPostsTitle: "\uAD00\uB828 \uAE00",
82
85
  postNotFound: "\uAE00\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
83
86
  pageNotFound: "\uD398\uC774\uC9C0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
87
+ previewExpired: "\uBBF8\uB9AC\uBCF4\uAE30 \uB9C1\uD06C\uAC00 \uB9CC\uB8CC\uB410\uC5B4\uC694. \uAD00\uB9AC\uC790 \uD3B8\uC9D1\uAE30\uC5D0\uC11C \uBBF8\uB9AC\uBCF4\uAE30\uB97C \uB2E4\uC2DC \uC5F4\uC5B4 \uC8FC\uC138\uC694.",
88
+ previewNotice: "\uBBF8\uB9AC\uBCF4\uAE30 \u2014 \uC544\uC9C1 \uACF5\uAC1C\uB418\uC9C0 \uC54A\uC740 \uB0B4\uC6A9\uC774\uBA70 \uAC80\uC0C9\uC5D0 \uB178\uCD9C\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
84
89
  tableOfContentsTitle: "\uBAA9\uCC28"
85
90
  },
86
91
  en: {
@@ -90,6 +95,8 @@ var DICTS = {
90
95
  relatedPostsTitle: "Related posts",
91
96
  postNotFound: "Post not found.",
92
97
  pageNotFound: "Page not found.",
98
+ previewExpired: "This preview link has expired. Open the preview again from the admin editor.",
99
+ previewNotice: "Preview \u2014 this content is not public and is hidden from search engines.",
93
100
  tableOfContentsTitle: "Table of contents"
94
101
  }
95
102
  };
@@ -623,7 +630,6 @@ function attachHeadingIds(doc, entries) {
623
630
 
624
631
  // src/server/blog-list.tsx
625
632
  import {
626
- fetchPosts,
627
633
  resolvePostDisplay
628
634
  } from "@roottale/cms-client/server";
629
635
  import {
@@ -640,6 +646,18 @@ async function fetchBlogSettingsFailSoft(input) {
640
646
  return fetchBlogSettings(input).catch(() => DEFAULT_BLOG_SETTINGS);
641
647
  }
642
648
 
649
+ // src/server/fetch-posts-fail-soft.ts
650
+ import {
651
+ fetchPosts
652
+ } from "@roottale/cms-client/server";
653
+ async function fetchPostsFailSoft(options) {
654
+ try {
655
+ return await fetchPosts(options);
656
+ } catch {
657
+ return { items: [], hasMore: false, nextCursor: null };
658
+ }
659
+ }
660
+
643
661
  // src/server/post-links.ts
644
662
  import {
645
663
  resolvePostCollection,
@@ -726,7 +744,7 @@ async function RootTaleBlogList(props) {
726
744
  } = props;
727
745
  const postHref = props.postHref ?? (collections ? makeCollectionPostHref(collections) : defaultPostHref);
728
746
  const [page, themeStyle, blogSettings] = await Promise.all([
729
- fetchPosts({
747
+ fetchPostsFailSoft({
730
748
  apiKey,
731
749
  baseUrl,
732
750
  limit,
@@ -787,9 +805,6 @@ async function RootTaleBlogList(props) {
787
805
  }
788
806
 
789
807
  // src/server/blog-categories.tsx
790
- import {
791
- fetchPosts as fetchPosts2
792
- } from "@roottale/cms-client/server";
793
808
  import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
794
809
  async function RootTaleBlogCategories(props) {
795
810
  const {
@@ -804,7 +819,7 @@ async function RootTaleBlogCategories(props) {
804
819
  theme: themeProp
805
820
  } = props;
806
821
  const [page, themeStyle] = await Promise.all([
807
- fetchPosts2({
822
+ fetchPostsFailSoft({
808
823
  apiKey,
809
824
  baseUrl,
810
825
  limit,
@@ -839,7 +854,9 @@ async function RootTaleBlogCategories(props) {
839
854
  // src/server/blog-post.tsx
840
855
  import {
841
856
  fetchPost,
842
- fetchPosts as fetchPosts3,
857
+ fetchPostPreview,
858
+ fetchPosts as fetchPosts2,
859
+ isPreviewExpiredError,
843
860
  resolvePostDisplay as resolvePostDisplay2,
844
861
  selectRelatedPosts
845
862
  } from "@roottale/cms-client/server";
@@ -931,12 +948,30 @@ function renderFieldFacts(post) {
931
948
  ] }, meta.name)) }) });
932
949
  }
933
950
 
951
+ // src/server/post-body-source.ts
952
+ import { sanitizeHtml as sanitizeHtml2 } from "@roottale/cms-core";
953
+ function hasVisibleContent(html) {
954
+ if (/<(?:img|hr|br|iframe|video|audio)\b/i.test(html)) return true;
955
+ return html.replace(/<[^>]*>/g, "").replace(/&nbsp;/gi, " ").trim().length > 0;
956
+ }
957
+ function resolvePostBodySource(bodyHtml) {
958
+ if (typeof bodyHtml !== "string") return { kind: "json" };
959
+ const trimmed = bodyHtml.trim();
960
+ if (trimmed.length === 0) return { kind: "json" };
961
+ const sanitized = sanitizeHtml2(trimmed);
962
+ if (!hasVisibleContent(sanitized)) return { kind: "json" };
963
+ return { kind: "html", html: sanitized };
964
+ }
965
+
934
966
  // src/server/tiptap-node-renderer.tsx
967
+ import { Fragment as Fragment4 } from "react";
935
968
  import {
936
969
  bindingSrcValue,
937
970
  bindingTextValue,
938
971
  boundFieldValue,
939
- normalizeImageAlt
972
+ ensureImportedHtmlDocumentRoots,
973
+ normalizeImageAlt,
974
+ scopeImportedCss
940
975
  } from "@roottale/cms-core";
941
976
 
942
977
  // src/server/tiptap-embed-renderer.tsx
@@ -1151,6 +1186,33 @@ function renderTiptapNode(value, key, context) {
1151
1186
  key
1152
1187
  );
1153
1188
  }
1189
+ case "importedHtml": {
1190
+ const rawHtml = node.attrs?.html;
1191
+ if (typeof rawHtml !== "string" || rawHtml.length === 0) return null;
1192
+ const scope = typeof node.attrs?.scope === "string" ? node.attrs.scope : "";
1193
+ const assetUrls = Array.isArray(node.attrs?.assetUrls) ? node.attrs.assetUrls.filter(
1194
+ (value2) => typeof value2 === "string" && /^https:\/\//i.test(value2)
1195
+ ) : [];
1196
+ const scopedCss = scopeImportedCss(
1197
+ typeof node.attrs?.css === "string" ? node.attrs.css : "",
1198
+ scope,
1199
+ assetUrls
1200
+ );
1201
+ return /* @__PURE__ */ jsxs9(Fragment4, { children: [
1202
+ scopedCss ? /* @__PURE__ */ jsx12("style", { "data-rt-imported-style": scope, children: scopedCss }) : null,
1203
+ /* @__PURE__ */ jsx12(
1204
+ "div",
1205
+ {
1206
+ className: "rt-imported-html",
1207
+ "data-rt-import-scope": scopedCss ? scope : void 0,
1208
+ style: { position: "relative", isolation: "isolate" },
1209
+ dangerouslySetInnerHTML: {
1210
+ __html: ensureImportedHtmlDocumentRoots(rawHtml, assetUrls)
1211
+ }
1212
+ }
1213
+ )
1214
+ ] }, key);
1215
+ }
1154
1216
  case "columns":
1155
1217
  return /* @__PURE__ */ jsx12("div", { className: "rt-columns", children }, key);
1156
1218
  case "column":
@@ -1217,7 +1279,7 @@ function renderTiptapNode(value, key, context) {
1217
1279
  }
1218
1280
 
1219
1281
  // src/server/render-tiptap.tsx
1220
- import { Fragment as Fragment4, jsx as jsx13 } from "react/jsx-runtime";
1282
+ import { Fragment as Fragment5, jsx as jsx13 } from "react/jsx-runtime";
1221
1283
  function isDevelopmentRuntime() {
1222
1284
  const processValue = Reflect.get(globalThis, "process");
1223
1285
  if (!isRecord(processValue)) return true;
@@ -1252,7 +1314,7 @@ function RenderTiptap({
1252
1314
  fields: fields ?? null,
1253
1315
  section: createSectionContextWithBlogListNetworkFetch(apiKey, baseUrl)
1254
1316
  };
1255
- return /* @__PURE__ */ jsx13(Fragment4, { children: contentNodes(renderDoc).map((value, index) => {
1317
+ return /* @__PURE__ */ jsx13(Fragment5, { children: contentNodes(renderDoc).map((value, index) => {
1256
1318
  const node = parseTiptapNode(value);
1257
1319
  if (node?.type !== "tableOfContents") {
1258
1320
  return renderTiptapNode(value, index, context);
@@ -1270,7 +1332,7 @@ function RenderTiptap({
1270
1332
  }
1271
1333
 
1272
1334
  // src/server/blog-post-body.tsx
1273
- import { Fragment as Fragment5, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
1335
+ import { Fragment as Fragment6, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
1274
1336
  function BlogPostBody(props) {
1275
1337
  const {
1276
1338
  doc,
@@ -1279,10 +1341,17 @@ function BlogPostBody(props) {
1279
1341
  baseUrl,
1280
1342
  toc,
1281
1343
  tocTitle,
1282
- tocPosition
1344
+ tocPosition,
1345
+ bodyHtml
1283
1346
  } = props;
1284
1347
  const tableOfContents = toc.length > 0 ? /* @__PURE__ */ jsx14(RootTaleTableOfContents, { entries: toc, title: tocTitle }) : null;
1285
- const body = /* @__PURE__ */ jsx14("div", { "data-roottale-cms": "body", className: "rt-cms-body", children: /* @__PURE__ */ jsx14(
1348
+ const source = resolvePostBodySource(bodyHtml);
1349
+ const body = /* @__PURE__ */ jsx14("div", { "data-roottale-cms": "body", className: "rt-cms-body", children: source.kind === "html" ? (
1350
+ // source.html 은 resolvePostBodySource 가 공용 sanitizeHtml 로 정제한 값이다
1351
+ // (이 컴포넌트에서 한 번 더 정제하지 않는 이유 — sink 가 한 곳이어야 눈으로
1352
+ // 추적 가능하고, 이중 정제는 안전을 더 만들지 않으면서 비용만 든다).
1353
+ /* @__PURE__ */ jsx14("div", { dangerouslySetInnerHTML: { __html: source.html } })
1354
+ ) : /* @__PURE__ */ jsx14(
1286
1355
  RenderTiptap,
1287
1356
  {
1288
1357
  doc,
@@ -1297,7 +1366,7 @@ function BlogPostBody(props) {
1297
1366
  /* @__PURE__ */ jsx14("aside", { className: "rt-cms-toc-side", children: tableOfContents })
1298
1367
  ] });
1299
1368
  }
1300
- return /* @__PURE__ */ jsxs10(Fragment5, { children: [
1369
+ return /* @__PURE__ */ jsxs10(Fragment6, { children: [
1301
1370
  tableOfContents,
1302
1371
  body
1303
1372
  ] });
@@ -1311,7 +1380,7 @@ import {
1311
1380
  import {
1312
1381
  resolvePostCollection as resolvePostCollection2
1313
1382
  } from "@roottale/cms-core";
1314
- import { Fragment as Fragment6, jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
1383
+ import { Fragment as Fragment7, jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
1315
1384
  function absoluteUrl(siteUrl, path) {
1316
1385
  const normalizedPath = path.startsWith("/") ? path : `/${path}`;
1317
1386
  return `${trimSlash(siteUrl)}${normalizedPath}`;
@@ -1346,7 +1415,7 @@ function renderBlogPostBreadcrumb(input) {
1346
1415
  url: absoluteUrl(siteUrl, crumb.href)
1347
1416
  }))
1348
1417
  ) : null;
1349
- return /* @__PURE__ */ jsxs11(Fragment6, { children: [
1418
+ return /* @__PURE__ */ jsxs11(Fragment7, { children: [
1350
1419
  /* @__PURE__ */ jsx15("nav", { "aria-label": "\uBE0C\uB808\uB4DC\uD06C\uB7FC", className: "rt-cms-breadcrumb", children: /* @__PURE__ */ jsx15("ol", { children: crumbs.map(
1351
1420
  (crumb, index) => index === crumbs.length - 1 ? /* @__PURE__ */ jsx15("li", { "aria-current": "page", children: crumb.label }, index) : /* @__PURE__ */ jsx15("li", { children: /* @__PURE__ */ jsx15("a", { href: crumb.href, children: crumb.label }) }, index)
1352
1421
  ) }) }),
@@ -1426,7 +1495,7 @@ function RootTaleFaqJsonLd({
1426
1495
  import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
1427
1496
  async function fetchRelatedPostsFailSoft(input) {
1428
1497
  if (input.count <= 0) return [];
1429
- const candidates = await fetchPosts3({
1498
+ const candidates = await fetchPosts2({
1430
1499
  apiKey: input.apiKey,
1431
1500
  baseUrl: input.baseUrl,
1432
1501
  type: "post",
@@ -1475,10 +1544,35 @@ function authorImageRadiusCss(radius) {
1475
1544
  if (radius === "rounded") return "0.875rem";
1476
1545
  return "0";
1477
1546
  }
1547
+ async function lookupPost(input) {
1548
+ if (input.previewToken) {
1549
+ try {
1550
+ const preview = await fetchPostPreview({
1551
+ apiKey: input.apiKey,
1552
+ baseUrl: input.baseUrl,
1553
+ token: input.previewToken
1554
+ });
1555
+ return preview ? { kind: "post", post: preview } : { kind: "missing" };
1556
+ } catch (error) {
1557
+ if (isPreviewExpiredError(error)) return { kind: "preview-expired" };
1558
+ throw error;
1559
+ }
1560
+ }
1561
+ if (!input.slugOrId) return { kind: "missing" };
1562
+ const post = await fetchPost({
1563
+ apiKey: input.apiKey,
1564
+ slugOrId: input.slugOrId,
1565
+ baseUrl: input.baseUrl,
1566
+ ...input.locale ? { locale: input.locale } : {}
1567
+ });
1568
+ return post ? { kind: "post", post } : { kind: "missing" };
1569
+ }
1478
1570
  async function RootTaleBlogPost(props) {
1479
1571
  const {
1480
1572
  apiKey,
1481
1573
  slugOrId,
1574
+ previewToken,
1575
+ previewExpiredElement,
1482
1576
  baseUrl,
1483
1577
  notFoundElement,
1484
1578
  showTitle = true,
@@ -1493,14 +1587,18 @@ async function RootTaleBlogPost(props) {
1493
1587
  authorHref = defaultAuthorHref,
1494
1588
  breadcrumb
1495
1589
  } = props;
1496
- const [post, themeStyle, blogSettings] = await Promise.all([
1497
- fetchPost({ apiKey, slugOrId, baseUrl, ...locale ? { locale } : {} }),
1590
+ const [lookup, themeStyle, blogSettings] = await Promise.all([
1591
+ lookupPost({ apiKey, baseUrl, slugOrId, previewToken, locale }),
1498
1592
  resolveThemeStyleFailSoft({ themeProp, apiKey, baseUrl }),
1499
1593
  fetchBlogSettingsFailSoft({ apiKey, baseUrl })
1500
1594
  ]);
1501
- if (!post) {
1595
+ if (lookup.kind === "preview-expired") {
1596
+ return previewExpiredElement ?? /* @__PURE__ */ jsx17("div", { "data-roottale-cms": "post-preview-expired", style: themeStyle ?? void 0, children: /* @__PURE__ */ jsx17("p", { className: "rt-cms-empty", children: t(locale, "previewExpired") }) });
1597
+ }
1598
+ if (lookup.kind === "missing") {
1502
1599
  return notFoundElement ?? /* @__PURE__ */ jsx17("div", { "data-roottale-cms": "post-missing", style: themeStyle ?? void 0, children: /* @__PURE__ */ jsx17("p", { className: "rt-cms-empty", children: t(locale, "postNotFound") }) });
1503
1600
  }
1601
+ const post = lookup.post;
1504
1602
  const display = resolvePostDisplay2(post, blogSettings, {
1505
1603
  showTableOfContents,
1506
1604
  tableOfContentsTitle
@@ -1611,7 +1709,8 @@ async function RootTaleBlogPost(props) {
1611
1709
  baseUrl,
1612
1710
  toc,
1613
1711
  tocTitle: display.tocTitle,
1614
- tocPosition: display.tocPosition
1712
+ tocPosition: display.tocPosition,
1713
+ bodyHtml: post.bodyHtml
1615
1714
  }
1616
1715
  ),
1617
1716
  showAuthorCard && authorName ? /* @__PURE__ */ jsxs13("section", { className: "rt-cms-author-card", "aria-label": "\uC791\uC131\uC790", children: [
@@ -1639,9 +1738,32 @@ async function RootTaleBlogPost(props) {
1639
1738
  );
1640
1739
  }
1641
1740
 
1741
+ // src/server/preview-notice.tsx
1742
+ import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
1743
+ function formatExpiry(locale, iso) {
1744
+ const date = new Date(iso);
1745
+ if (Number.isNaN(date.getTime())) return null;
1746
+ const time = new Intl.DateTimeFormat(locale === "en" ? "en-US" : "ko-KR", {
1747
+ hour: "2-digit",
1748
+ minute: "2-digit"
1749
+ }).format(date);
1750
+ return locale === "en" ? `Valid until ${time}.` : `${time}\uAE4C\uC9C0 \uBCFC \uC218 \uC788\uC5B4\uC694.`;
1751
+ }
1752
+ function RootTalePreviewNotice({
1753
+ locale,
1754
+ message,
1755
+ expiresAt
1756
+ }) {
1757
+ const expiry = expiresAt ? formatExpiry(locale, expiresAt) : null;
1758
+ return /* @__PURE__ */ jsxs14("div", { "data-roottale-cms": "preview-notice", className: "rt-cms-preview-notice", role: "status", children: [
1759
+ /* @__PURE__ */ jsx18("span", { children: message ?? t(locale, "previewNotice") }),
1760
+ expiry ? /* @__PURE__ */ jsx18("span", { className: "rt-cms-preview-notice__expiry", children: expiry }) : null
1761
+ ] });
1762
+ }
1763
+
1642
1764
  // src/server/page.tsx
1643
1765
  import { fetchPost as fetchPost2 } from "@roottale/cms-client/server";
1644
- import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
1766
+ import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
1645
1767
  async function RootTalePage(props) {
1646
1768
  const {
1647
1769
  apiKey,
@@ -1650,27 +1772,30 @@ async function RootTalePage(props) {
1650
1772
  notFoundElement,
1651
1773
  showTitle = true,
1652
1774
  theme: themeProp,
1653
- locale
1775
+ locale,
1776
+ prepareBody
1654
1777
  } = props;
1655
1778
  const [post, themeStyle] = await Promise.all([
1656
1779
  fetchPost2({ apiKey, slugOrId, baseUrl, ...locale ? { locale } : {} }),
1657
1780
  resolveThemeStyleFailSoft({ themeProp, apiKey, baseUrl })
1658
1781
  ]);
1659
1782
  if (!post) {
1660
- return notFoundElement ?? /* @__PURE__ */ jsx18("div", { "data-roottale-cms": "page-missing", style: themeStyle ?? void 0, children: /* @__PURE__ */ jsx18("p", { className: "rt-cms-empty", children: t(locale, "pageNotFound") }) });
1783
+ return notFoundElement ?? /* @__PURE__ */ jsx19("div", { "data-roottale-cms": "page-missing", style: themeStyle ?? void 0, children: /* @__PURE__ */ jsx19("p", { className: "rt-cms-empty", children: t(locale, "pageNotFound") }) });
1661
1784
  }
1662
- return /* @__PURE__ */ jsxs14(
1785
+ const pageBodySource = resolvePostBodySource(post.bodyHtml);
1786
+ const bodyJson = pageBodySource.kind === "json" && prepareBody ? await prepareBody(post.bodyJson) : post.bodyJson;
1787
+ return /* @__PURE__ */ jsxs15(
1663
1788
  "article",
1664
1789
  {
1665
1790
  "data-roottale-cms": "page",
1666
1791
  className: "rt-cms-article",
1667
1792
  style: themeStyle ?? void 0,
1668
1793
  children: [
1669
- showTitle ? /* @__PURE__ */ jsx18("header", { className: "rt-cms-post-header", children: /* @__PURE__ */ jsx18("h1", { className: "rt-cms-title", children: post.title }) }) : null,
1670
- /* @__PURE__ */ jsx18("div", { "data-roottale-cms": "body", className: "rt-cms-body", children: /* @__PURE__ */ jsx18(
1794
+ showTitle ? /* @__PURE__ */ jsx19("header", { className: "rt-cms-post-header", children: /* @__PURE__ */ jsx19("h1", { className: "rt-cms-title", children: post.title }) }) : null,
1795
+ /* @__PURE__ */ jsx19("div", { "data-roottale-cms": "body", className: "rt-cms-body", children: pageBodySource.kind === "html" ? /* @__PURE__ */ jsx19("div", { dangerouslySetInnerHTML: { __html: pageBodySource.html } }) : /* @__PURE__ */ jsx19(
1671
1796
  RenderTiptap,
1672
1797
  {
1673
- doc: post.bodyJson,
1798
+ doc: bodyJson,
1674
1799
  fields: post.fields,
1675
1800
  apiKey,
1676
1801
  baseUrl
@@ -1680,16 +1805,34 @@ async function RootTalePage(props) {
1680
1805
  }
1681
1806
  );
1682
1807
  }
1808
+
1809
+ // src/server/exposure-slot.tsx
1810
+ import { fetchExposure } from "@roottale/cms-client/server";
1811
+ import { jsx as jsx20 } from "react/jsx-runtime";
1812
+ async function RootTaleExposureSlot(props) {
1813
+ const campaign = await fetchExposure({
1814
+ apiKey: props.apiKey,
1815
+ baseUrl: props.baseUrl,
1816
+ siteId: props.siteId,
1817
+ slotKey: props.slotKey,
1818
+ path: props.path,
1819
+ revalidate: props.revalidate
1820
+ });
1821
+ if (!campaign || !props.allowedVariants.includes(campaign.content.variant)) return null;
1822
+ return /* @__PURE__ */ jsx20(RootTaleExposure, { campaign, allowedVariants: props.allowedVariants });
1823
+ }
1683
1824
  export {
1684
1825
  RenderTiptap,
1685
1826
  RootTaleBlogCategories,
1686
1827
  RootTaleBlogList,
1687
1828
  RootTaleBlogPost,
1829
+ RootTaleExposureSlot,
1688
1830
  RootTaleFaqJsonLd,
1689
1831
  RootTaleFloatingCta,
1690
1832
  RootTaleLeadForm,
1691
1833
  RootTalePage,
1692
1834
  RootTalePostCard,
1835
+ RootTalePreviewNotice,
1693
1836
  RootTaleTableOfContents,
1694
1837
  RootTaleThemeProvider,
1695
1838
  attachHeadingIds,
@@ -1702,6 +1845,7 @@ export {
1702
1845
  renderBlock,
1703
1846
  renderBlocks,
1704
1847
  renderBoundBlocks,
1848
+ resolvePostBodySource,
1705
1849
  themeToCssVars2 as themeToCssVars
1706
1850
  };
1707
1851
  //# sourceMappingURL=server.js.map