@roottale/cms-renderer-next 0.55.0 → 0.57.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.js CHANGED
@@ -4,8 +4,10 @@ import {
4
4
  collectCollectionCategories,
5
5
  defaultCategoryHref,
6
6
  filterPostsByCategory,
7
+ makeCategoryHref,
8
+ storedPostPath,
7
9
  trimSlash
8
- } from "./chunk-FJANIAZN.js";
10
+ } from "./chunk-GDP2VGJR.js";
9
11
  import {
10
12
  getSectionNodeRenderer
11
13
  } from "./chunk-KSPPYX22.js";
@@ -664,20 +666,22 @@ import {
664
666
  resolvePostPath
665
667
  } from "@roottale/cms-core";
666
668
  function defaultPostHref(post) {
667
- return `/blog/${post.slug}`;
669
+ return storedPostPath(post) ?? `/blog/${post.slug}`;
668
670
  }
669
671
  function defaultAuthorHref(slug) {
670
672
  return `/blog/author/${slug}`;
671
673
  }
672
674
  function makeCollectionPostHref(collections) {
673
675
  return (post) => {
676
+ const stored = storedPostPath(post);
677
+ if (stored) return stored;
674
678
  const path = resolvePostPath(post, collections);
675
679
  if (path) return path;
676
680
  const collection = resolvePostCollection(post, collections);
677
681
  if (collection?.detail === false && collection.basePath) {
678
682
  return collection.basePath;
679
683
  }
680
- return defaultPostHref(post);
684
+ return `/blog/${post.slug}`;
681
685
  };
682
686
  }
683
687
 
@@ -853,115 +857,27 @@ async function RootTaleBlogCategories(props) {
853
857
 
854
858
  // src/server/blog-post.tsx
855
859
  import {
860
+ POST_FOOTER_PATTERN_SLOT as POST_FOOTER_PATTERN_SLOT2,
856
861
  fetchPost,
857
862
  fetchPostPreview,
858
- fetchPosts as fetchPosts2,
863
+ fetchPosts as fetchPosts3,
859
864
  isPreviewExpiredError,
860
865
  resolvePostDisplay as resolvePostDisplay2,
861
866
  selectRelatedPosts
862
867
  } from "@roottale/cms-client/server";
863
868
 
864
- // src/server/blog-post-archive.tsx
865
- import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
866
- var FIELD_FACT_TYPES = /* @__PURE__ */ new Set([
867
- "text",
868
- "textarea",
869
- "email",
870
- "url",
871
- "select",
872
- "radio",
873
- "button_group",
874
- "number",
875
- "range"
876
- ]);
877
- function archiveGroupTitle(group) {
878
- if (group.collectionKey === "conditions") {
879
- return `\uC774 \uC99D\uC0C1\uACFC \uAD00\uB828\uB41C \uC9C8\uD658 ${group.items.length}`;
880
- }
881
- if (group.collectionKey === "treatments") {
882
- return `\uAD00\uB828 \uCE58\uB8CC\uBC95 ${group.items.length}`;
883
- }
884
- if (group.collectionKey === "symptoms") {
885
- return `\uAD00\uB828 \uC99D\uC0C1 ${group.items.length}`;
886
- }
887
- return `${group.label} ${group.items.length}`;
888
- }
889
- function defaultArchiveBasePath(collectionKey) {
890
- if (collectionKey === "symptoms") return "/symptoms";
891
- if (collectionKey === "conditions") return "/conditions";
892
- if (collectionKey === "treatments") return "/treatments";
893
- return "/blog";
894
- }
895
- function archivePostHref(collectionKey, slug, collections) {
896
- const collection = collections?.find((item) => item.key === collectionKey);
897
- const basePath = collection?.basePath || defaultArchiveBasePath(collectionKey);
898
- const trimmedBase = basePath.replace(/\/+$/, "");
899
- if (collection?.detail === false) return trimmedBase;
900
- return `${trimmedBase}/${slug}`;
901
- }
902
- function renderArchiveRelated(groups, collections) {
903
- const visibleGroups = groups.filter((group) => group.items.length > 0);
904
- if (visibleGroups.length === 0) return null;
905
- return /* @__PURE__ */ jsx9("aside", { className: "rt-archive-related", "aria-label": "\uC5F0\uAD00 \uC815\uBCF4", children: visibleGroups.map((group) => /* @__PURE__ */ jsxs8("section", { className: "rt-archive-related__group", children: [
906
- /* @__PURE__ */ jsx9("h2", { className: "rt-archive-related__title", children: archiveGroupTitle(group) }),
907
- /* @__PURE__ */ jsx9("ul", { className: "rt-archive-related__list", children: group.items.map((item) => /* @__PURE__ */ jsx9("li", { className: "rt-archive-related__item", children: /* @__PURE__ */ jsxs8(
908
- "a",
909
- {
910
- className: "rt-archive-related__link",
911
- href: archivePostHref(
912
- item.collectionKey,
913
- item.slug,
914
- collections
915
- ),
916
- children: [
917
- /* @__PURE__ */ jsx9("span", { className: "rt-archive-related__item-title", children: item.title }),
918
- item.excerpt ? /* @__PURE__ */ jsx9("span", { className: "rt-archive-related__item-excerpt", children: item.excerpt }) : null
919
- ]
920
- }
921
- ) }, item.id)) })
922
- ] }, group.collectionKey)) });
923
- }
924
- function fieldFactValue(raw, meta) {
925
- if (raw === null || raw === void 0) return null;
926
- if (typeof raw === "number") return raw.toLocaleString("ko-KR");
927
- if (typeof raw !== "string" || raw.trim() === "") return null;
928
- if (!meta.choices) return raw;
929
- const choice = meta.choices.find((item) => item.value === raw);
930
- return choice ? choice.label : raw;
931
- }
932
- function renderFieldFacts(post) {
933
- if (!post.fields) return null;
934
- const rows = post.fieldsMeta.filter((meta) => FIELD_FACT_TYPES.has(meta.type)).flatMap((meta) => {
935
- const value = fieldFactValue(post.fields?.[meta.name], meta);
936
- return value === null ? [] : [{ meta, value }];
937
- });
938
- if (rows.length === 0) return null;
939
- return /* @__PURE__ */ jsx9("aside", { className: "rt-archive-facts", "aria-label": "\uC0C1\uC138 \uC815\uBCF4", children: /* @__PURE__ */ jsx9("dl", { className: "rt-archive-facts__list", children: rows.map(({ meta, value }) => /* @__PURE__ */ jsxs8("div", { className: "rt-archive-facts__row", children: [
940
- /* @__PURE__ */ jsx9("dt", { className: "rt-archive-facts__label", children: meta.label }),
941
- /* @__PURE__ */ jsx9(
942
- "dd",
943
- {
944
- className: meta.type === "textarea" ? "rt-archive-facts__value rt-archive-facts__value--multiline" : "rt-archive-facts__value",
945
- children: value
946
- }
947
- )
948
- ] }, meta.name)) }) });
949
- }
869
+ // src/server/site-patterns.tsx
870
+ import {
871
+ POST_FOOTER_PATTERN_SLOT,
872
+ SITE_PATTERNS_CACHE_TAG,
873
+ fetchSitePatterns,
874
+ selectSitePatternForSlot
875
+ } from "@roottale/cms-client/server";
950
876
 
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
- }
877
+ // src/server/render-tiptap.tsx
878
+ import {
879
+ internalContentLinkResolverFrom
880
+ } from "@roottale/cms-core";
965
881
 
966
882
  // src/server/tiptap-node-renderer.tsx
967
883
  import { Fragment as Fragment4 } from "react";
@@ -971,12 +887,13 @@ import {
971
887
  boundFieldValue,
972
888
  ensureImportedHtmlDocumentRoots,
973
889
  normalizeImageAlt,
890
+ renderInternalContentLinksInHtml,
974
891
  scopeImportedCss
975
892
  } from "@roottale/cms-core";
976
893
 
977
894
  // src/server/tiptap-embed-renderer.tsx
978
895
  import { isAllowedEmbedSrc } from "@roottale/cms-core";
979
- import { jsx as jsx10 } from "react/jsx-runtime";
896
+ import { jsx as jsx9 } from "react/jsx-runtime";
980
897
  function isSafeFallbackHref(value) {
981
898
  if (typeof value !== "string") return false;
982
899
  const href = value.trim();
@@ -990,13 +907,13 @@ function renderEmbedNode(node, key) {
990
907
  const src = node.attrs?.src;
991
908
  if (isAllowedEmbedSrc(src)) {
992
909
  const provider = typeof node.attrs?.provider === "string" ? node.attrs.provider : void 0;
993
- return /* @__PURE__ */ jsx10(
910
+ return /* @__PURE__ */ jsx9(
994
911
  "div",
995
912
  {
996
913
  className: "rt-embed",
997
914
  "data-provider": provider,
998
915
  style: { aspectRatio: "16 / 9" },
999
- children: /* @__PURE__ */ jsx10(
916
+ children: /* @__PURE__ */ jsx9(
1000
917
  "iframe",
1001
918
  {
1002
919
  src,
@@ -1014,9 +931,17 @@ function renderEmbedNode(node, key) {
1014
931
  }
1015
932
  const originalUrl = node.attrs?.originalUrl;
1016
933
  if (!isSafeFallbackHref(originalUrl)) return null;
1017
- return /* @__PURE__ */ jsx10("p", { className: "rt-embed-fallback", children: /* @__PURE__ */ jsx10("a", { href: originalUrl, rel: "noopener noreferrer", target: "_blank", children: originalUrl }) }, key);
934
+ return /* @__PURE__ */ jsx9("p", { className: "rt-embed-fallback", children: /* @__PURE__ */ jsx9("a", { href: originalUrl, rel: "noopener noreferrer", target: "_blank", children: originalUrl }) }, key);
1018
935
  }
1019
936
 
937
+ // src/server/tiptap-inline-renderer.tsx
938
+ import {
939
+ findInternalContentLinkTokens,
940
+ INTERNAL_CONTENT_LINK_CLASS,
941
+ INTERNAL_CONTENT_LINK_PENDING_ATTR,
942
+ INTERNAL_CONTENT_LINK_PENDING_CLASS
943
+ } from "@roottale/cms-core";
944
+
1020
945
  // src/server/tiptap-types.ts
1021
946
  function isRecord(value) {
1022
947
  return typeof value === "object" && value !== null;
@@ -1040,7 +965,7 @@ function parseTiptapMark(value) {
1040
965
  }
1041
966
 
1042
967
  // src/server/tiptap-inline-renderer.tsx
1043
- import { Fragment as Fragment3, jsx as jsx11 } from "react/jsx-runtime";
968
+ import { Fragment as Fragment3, jsx as jsx10 } from "react/jsx-runtime";
1044
969
  function isSafeHref(value) {
1045
970
  if (typeof value !== "string") return false;
1046
971
  const href = value.trim();
@@ -1053,52 +978,86 @@ function isSafeHref(value) {
1053
978
  function applyMark(mark, child) {
1054
979
  switch (mark.type) {
1055
980
  case "bold":
1056
- return /* @__PURE__ */ jsx11("strong", { children: child });
981
+ return /* @__PURE__ */ jsx10("strong", { children: child });
1057
982
  case "italic":
1058
- return /* @__PURE__ */ jsx11("em", { children: child });
983
+ return /* @__PURE__ */ jsx10("em", { children: child });
1059
984
  case "underline":
1060
- return /* @__PURE__ */ jsx11("u", { children: child });
985
+ return /* @__PURE__ */ jsx10("u", { children: child });
1061
986
  case "strike":
1062
- return /* @__PURE__ */ jsx11("s", { children: child });
987
+ return /* @__PURE__ */ jsx10("s", { children: child });
1063
988
  case "code":
1064
- return /* @__PURE__ */ jsx11("code", { children: child });
989
+ return /* @__PURE__ */ jsx10("code", { children: child });
1065
990
  case "highlight": {
1066
991
  const color = mark.attrs?.color;
1067
992
  const style = typeof color === "string" && color.length > 0 ? { background: color } : void 0;
1068
- return /* @__PURE__ */ jsx11("mark", { style, children: child });
993
+ return /* @__PURE__ */ jsx10("mark", { style, children: child });
1069
994
  }
1070
995
  case "textStyle": {
1071
996
  const color = mark.attrs?.color;
1072
997
  if (typeof color !== "string" || color.length === 0) {
1073
- return /* @__PURE__ */ jsx11(Fragment3, { children: child });
998
+ return /* @__PURE__ */ jsx10(Fragment3, { children: child });
1074
999
  }
1075
- return /* @__PURE__ */ jsx11("span", { style: { color }, children: child });
1000
+ return /* @__PURE__ */ jsx10("span", { style: { color }, children: child });
1076
1001
  }
1077
1002
  case "link": {
1078
1003
  const rawHref = mark.attrs?.href;
1079
1004
  const href = isSafeHref(rawHref) ? rawHref : "#";
1080
- return /* @__PURE__ */ jsx11("a", { href, rel: "noopener noreferrer", target: "_blank", children: child });
1005
+ return /* @__PURE__ */ jsx10("a", { href, rel: "noopener noreferrer", target: "_blank", children: child });
1081
1006
  }
1082
1007
  default:
1083
- return /* @__PURE__ */ jsx11("span", { children: child });
1008
+ return /* @__PURE__ */ jsx10("span", { children: child });
1084
1009
  }
1085
1010
  }
1086
- function renderTextNode(node, key) {
1011
+ function isSafeInternalPath(path) {
1012
+ return typeof path === "string" && path.startsWith("/") && !path.startsWith("//");
1013
+ }
1014
+ function renderInternalLinkText(text, resolve) {
1015
+ const tokens = findInternalContentLinkTokens(text);
1016
+ if (tokens.length === 0) return text;
1017
+ const parts = [];
1018
+ let cursor = 0;
1019
+ tokens.forEach((token, index) => {
1020
+ if (token.index > cursor) parts.push(text.slice(cursor, token.index));
1021
+ const path = resolve(token.key);
1022
+ parts.push(
1023
+ isSafeInternalPath(path) ? /* @__PURE__ */ jsx10("a", { href: path, className: INTERNAL_CONTENT_LINK_CLASS, children: token.label }, index) : /* @__PURE__ */ jsx10(
1024
+ "span",
1025
+ {
1026
+ className: `${INTERNAL_CONTENT_LINK_CLASS} ${INTERNAL_CONTENT_LINK_PENDING_CLASS}`,
1027
+ ...{ [INTERNAL_CONTENT_LINK_PENDING_ATTR]: token.key },
1028
+ children: token.label
1029
+ },
1030
+ index
1031
+ )
1032
+ );
1033
+ cursor = token.index + token.raw.length;
1034
+ });
1035
+ if (cursor < text.length) parts.push(text.slice(cursor));
1036
+ return /* @__PURE__ */ jsx10(Fragment3, { children: parts });
1037
+ }
1038
+ function renderTextNode(node, key, internalLinks = null) {
1087
1039
  const text = node.text ?? "";
1088
1040
  const marks = (node.marks ?? []).flatMap((value) => {
1089
1041
  const mark = parseTiptapMark(value);
1090
1042
  return mark ? [mark] : [];
1091
1043
  });
1092
- if (marks.length === 0) return text;
1093
- let element = text;
1044
+ const canLink = internalLinks !== null && !marks.some((mark) => mark.type === "code" || mark.type === "link");
1045
+ const content = canLink && internalLinks ? renderInternalLinkText(text, internalLinks) : text;
1046
+ if (marks.length === 0) {
1047
+ return typeof content === "string" ? content : /* @__PURE__ */ jsx10("span", { children: content }, key);
1048
+ }
1049
+ let element = typeof content === "string" ? content : /* @__PURE__ */ jsx10(Fragment3, { children: content });
1094
1050
  for (const mark of marks) {
1095
1051
  element = applyMark(mark, element);
1096
1052
  }
1097
- return /* @__PURE__ */ jsx11("span", { children: element }, key);
1053
+ return /* @__PURE__ */ jsx10("span", { children: element }, key);
1098
1054
  }
1099
1055
 
1100
1056
  // src/server/tiptap-node-renderer.tsx
1101
- import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
1057
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
1058
+ function withInternalLinksInHtml(html, internalLinks) {
1059
+ return internalLinks ? renderInternalContentLinksInHtml(html, internalLinks).html : html;
1060
+ }
1102
1061
  function boundContentText(node, fields) {
1103
1062
  return bindingTextValue(
1104
1063
  boundFieldValue(node.attrs?.bindings, "content", fields)
@@ -1131,7 +1090,7 @@ function renderTiptapNode(value, key, context) {
1131
1090
  switch (node.type) {
1132
1091
  case "paragraph": {
1133
1092
  const bound = boundContentText(node, context.fields);
1134
- return /* @__PURE__ */ jsx12("p", { style: alignStyle(node), children: bound ?? children }, key);
1093
+ return /* @__PURE__ */ jsx11("p", { style: alignStyle(node), children: bound ?? children }, key);
1135
1094
  }
1136
1095
  case "heading": {
1137
1096
  const rawLevel = Number(node.attrs?.level ?? 2);
@@ -1142,27 +1101,29 @@ function renderTiptapNode(value, key, context) {
1142
1101
  const style = alignStyle(node);
1143
1102
  const inner = boundContentText(node, context.fields) ?? children;
1144
1103
  if (level === 2) {
1145
- return /* @__PURE__ */ jsx12("h2", { id, style, children: inner }, key);
1104
+ return /* @__PURE__ */ jsx11("h2", { id, style, children: inner }, key);
1146
1105
  }
1147
1106
  if (level === 3) {
1148
- return /* @__PURE__ */ jsx12("h3", { id, style, children: inner }, key);
1107
+ return /* @__PURE__ */ jsx11("h3", { id, style, children: inner }, key);
1149
1108
  }
1150
- return /* @__PURE__ */ jsx12("h4", { id, style, children: inner }, key);
1109
+ return /* @__PURE__ */ jsx11("h4", { id, style, children: inner }, key);
1151
1110
  }
1152
1111
  case "bulletList":
1153
- return /* @__PURE__ */ jsx12("ul", { children }, key);
1112
+ return /* @__PURE__ */ jsx11("ul", { children }, key);
1154
1113
  case "orderedList":
1155
- return /* @__PURE__ */ jsx12("ol", { children }, key);
1114
+ return /* @__PURE__ */ jsx11("ol", { children }, key);
1156
1115
  case "listItem":
1157
- return /* @__PURE__ */ jsx12("li", { children }, key);
1116
+ return /* @__PURE__ */ jsx11("li", { children }, key);
1158
1117
  case "blockquote":
1159
- return /* @__PURE__ */ jsx12("blockquote", { children }, key);
1118
+ return /* @__PURE__ */ jsx11("blockquote", { children }, key);
1160
1119
  case "codeBlock":
1161
- return /* @__PURE__ */ jsx12("pre", { children: /* @__PURE__ */ jsx12("code", { children }) }, key);
1120
+ return /* @__PURE__ */ jsx11("pre", { children: /* @__PURE__ */ jsx11("code", { children: node.content?.map(
1121
+ (child, index) => renderTiptapNode(child, index, { ...context, internalLinks: null })
1122
+ ) }) }, key);
1162
1123
  case "horizontalRule":
1163
- return /* @__PURE__ */ jsx12("hr", {}, key);
1124
+ return /* @__PURE__ */ jsx11("hr", {}, key);
1164
1125
  case "hardBreak":
1165
- return /* @__PURE__ */ jsx12("br", {}, key);
1126
+ return /* @__PURE__ */ jsx11("br", {}, key);
1166
1127
  case "image": {
1167
1128
  const boundSrc = bindingSrcValue(
1168
1129
  boundFieldValue(node.attrs?.bindings, "src", context.fields)
@@ -1173,7 +1134,7 @@ function renderTiptapNode(value, key, context) {
1173
1134
  const title = typeof node.attrs?.title === "string" ? node.attrs.title : void 0;
1174
1135
  const width = typeof node.attrs?.width === "number" && node.attrs.width > 0 ? node.attrs.width : void 0;
1175
1136
  const height = typeof node.attrs?.height === "number" && node.attrs.height > 0 ? node.attrs.height : void 0;
1176
- return /* @__PURE__ */ jsx12(
1137
+ return /* @__PURE__ */ jsx11(
1177
1138
  "img",
1178
1139
  {
1179
1140
  src,
@@ -1198,59 +1159,62 @@ function renderTiptapNode(value, key, context) {
1198
1159
  scope,
1199
1160
  assetUrls
1200
1161
  );
1201
- return /* @__PURE__ */ jsxs9(Fragment4, { children: [
1202
- scopedCss ? /* @__PURE__ */ jsx12("style", { "data-rt-imported-style": scope, children: scopedCss }) : null,
1203
- /* @__PURE__ */ jsx12(
1162
+ return /* @__PURE__ */ jsxs8(Fragment4, { children: [
1163
+ scopedCss ? /* @__PURE__ */ jsx11("style", { "data-rt-imported-style": scope, children: scopedCss }) : null,
1164
+ /* @__PURE__ */ jsx11(
1204
1165
  "div",
1205
1166
  {
1206
1167
  className: "rt-imported-html",
1207
1168
  "data-rt-import-scope": scopedCss ? scope : void 0,
1208
1169
  style: { position: "relative", isolation: "isolate" },
1209
1170
  dangerouslySetInnerHTML: {
1210
- __html: ensureImportedHtmlDocumentRoots(rawHtml, assetUrls)
1171
+ __html: withInternalLinksInHtml(
1172
+ ensureImportedHtmlDocumentRoots(rawHtml, assetUrls),
1173
+ context.internalLinks ?? null
1174
+ )
1211
1175
  }
1212
1176
  }
1213
1177
  )
1214
1178
  ] }, key);
1215
1179
  }
1216
1180
  case "columns":
1217
- return /* @__PURE__ */ jsx12("div", { className: "rt-columns", children }, key);
1181
+ return /* @__PURE__ */ jsx11("div", { className: "rt-columns", children }, key);
1218
1182
  case "column":
1219
- return /* @__PURE__ */ jsx12("div", { className: "rt-column", children }, key);
1183
+ return /* @__PURE__ */ jsx11("div", { className: "rt-column", children }, key);
1220
1184
  case "table":
1221
- return /* @__PURE__ */ jsx12("div", { className: "rt-table-wrap", children: /* @__PURE__ */ jsx12("table", { className: "rt-table", children: /* @__PURE__ */ jsx12("tbody", { children }) }) }, key);
1185
+ return /* @__PURE__ */ jsx11("div", { className: "rt-table-wrap", children: /* @__PURE__ */ jsx11("table", { className: "rt-table", children: /* @__PURE__ */ jsx11("tbody", { children }) }) }, key);
1222
1186
  case "tableRow":
1223
- return /* @__PURE__ */ jsx12("tr", { children }, key);
1187
+ return /* @__PURE__ */ jsx11("tr", { children }, key);
1224
1188
  case "tableHeader": {
1225
1189
  const { colSpan, rowSpan } = tableCellSpans(node);
1226
- return /* @__PURE__ */ jsx12("th", { colSpan, rowSpan, children }, key);
1190
+ return /* @__PURE__ */ jsx11("th", { colSpan, rowSpan, children }, key);
1227
1191
  }
1228
1192
  case "tableCell": {
1229
1193
  const { colSpan, rowSpan } = tableCellSpans(node);
1230
- return /* @__PURE__ */ jsx12("td", { colSpan, rowSpan, children }, key);
1194
+ return /* @__PURE__ */ jsx11("td", { colSpan, rowSpan, children }, key);
1231
1195
  }
1232
1196
  case "embed":
1233
1197
  return renderEmbedNode(node, key);
1234
1198
  case "faq":
1235
- return /* @__PURE__ */ jsx12("div", { "data-roottale-cms": "faq", className: "rt-cms-faq", children }, key);
1199
+ return /* @__PURE__ */ jsx11("div", { "data-roottale-cms": "faq", className: "rt-cms-faq", children }, key);
1236
1200
  case "faqItem":
1237
- return /* @__PURE__ */ jsx12("details", { className: "rt-cms-faq-item", children }, key);
1201
+ return /* @__PURE__ */ jsx11("details", { className: "rt-cms-faq-item", children }, key);
1238
1202
  case "faqQuestion":
1239
- return /* @__PURE__ */ jsx12("summary", { className: "rt-cms-faq-q", children }, key);
1203
+ return /* @__PURE__ */ jsx11("summary", { className: "rt-cms-faq-q", children }, key);
1240
1204
  case "faqAnswer":
1241
- return /* @__PURE__ */ jsx12("div", { className: "rt-cms-faq-a", children }, key);
1205
+ return /* @__PURE__ */ jsx11("div", { className: "rt-cms-faq-a", children }, key);
1242
1206
  case "references": {
1243
1207
  const rawTitle = node.attrs?.title;
1244
1208
  const title = typeof rawTitle === "string" && rawTitle.trim() ? rawTitle.trim() : "\uACF5\uC2DD \uCD9C\uCC98";
1245
1209
  const titleId = `rt-cms-references-${key}`;
1246
- return /* @__PURE__ */ jsxs9(
1210
+ return /* @__PURE__ */ jsxs8(
1247
1211
  "section",
1248
1212
  {
1249
1213
  "data-roottale-cms": "references",
1250
1214
  className: "rt-cms-references",
1251
1215
  "aria-labelledby": titleId,
1252
1216
  children: [
1253
- /* @__PURE__ */ jsx12("h2", { id: titleId, className: "rt-cms-references__title", children: title }),
1217
+ /* @__PURE__ */ jsx11("h2", { id: titleId, className: "rt-cms-references__title", children: title }),
1254
1218
  children
1255
1219
  ]
1256
1220
  },
@@ -1272,14 +1236,14 @@ function renderTiptapNode(value, key, context) {
1272
1236
  return renderSection(node, key, context.section);
1273
1237
  }
1274
1238
  case "text":
1275
- return renderTextNode(node, key);
1239
+ return renderTextNode(node, key, context.internalLinks ?? null);
1276
1240
  default:
1277
- return children ? /* @__PURE__ */ jsx12("span", { children }, key) : null;
1241
+ return children ? /* @__PURE__ */ jsx11("span", { children }, key) : null;
1278
1242
  }
1279
1243
  }
1280
1244
 
1281
1245
  // src/server/render-tiptap.tsx
1282
- import { Fragment as Fragment5, jsx as jsx13 } from "react/jsx-runtime";
1246
+ import { Fragment as Fragment5, jsx as jsx12 } from "react/jsx-runtime";
1283
1247
  function isDevelopmentRuntime() {
1284
1248
  const processValue = Reflect.get(globalThis, "process");
1285
1249
  if (!isRecord(processValue)) return true;
@@ -1292,7 +1256,7 @@ function createSectionContextWithBlogListNetworkFetch(apiKey, baseUrl) {
1292
1256
  return {
1293
1257
  cms,
1294
1258
  isDev: isDevelopmentRuntime,
1295
- renderBlogList: (props) => /* @__PURE__ */ jsx13(RootTaleBlogList, { ...props })
1259
+ renderBlogList: (props) => /* @__PURE__ */ jsx12(RootTaleBlogList, { ...props })
1296
1260
  };
1297
1261
  }
1298
1262
  function contentNodes(doc) {
@@ -1302,7 +1266,8 @@ function RenderTiptap({
1302
1266
  doc,
1303
1267
  fields,
1304
1268
  apiKey,
1305
- baseUrl
1269
+ baseUrl,
1270
+ internalLinks
1306
1271
  }) {
1307
1272
  const content = contentNodes(doc);
1308
1273
  const hasTableOfContents = content.some(
@@ -1312,15 +1277,16 @@ function RenderTiptap({
1312
1277
  const renderDoc = hasTableOfContents && entries.length > 0 ? attachHeadingIds(doc, entries) : doc;
1313
1278
  const context = {
1314
1279
  fields: fields ?? null,
1315
- section: createSectionContextWithBlogListNetworkFetch(apiKey, baseUrl)
1280
+ section: createSectionContextWithBlogListNetworkFetch(apiKey, baseUrl),
1281
+ internalLinks: internalLinks ? internalContentLinkResolverFrom(internalLinks) : null
1316
1282
  };
1317
- return /* @__PURE__ */ jsx13(Fragment5, { children: contentNodes(renderDoc).map((value, index) => {
1283
+ return /* @__PURE__ */ jsx12(Fragment5, { children: contentNodes(renderDoc).map((value, index) => {
1318
1284
  const node = parseTiptapNode(value);
1319
1285
  if (node?.type !== "tableOfContents") {
1320
1286
  return renderTiptapNode(value, index, context);
1321
1287
  }
1322
1288
  const title = node.attrs?.title;
1323
- return /* @__PURE__ */ jsx13(
1289
+ return /* @__PURE__ */ jsx12(
1324
1290
  RootTaleTableOfContents,
1325
1291
  {
1326
1292
  entries,
@@ -1331,8 +1297,152 @@ function RenderTiptap({
1331
1297
  }) });
1332
1298
  }
1333
1299
 
1300
+ // src/server/site-patterns.tsx
1301
+ import { jsx as jsx13 } from "react/jsx-runtime";
1302
+ async function resolvePostPatternFailSoft(post, input, slot = POST_FOOTER_PATTERN_SLOT) {
1303
+ const key = post.patternSlots?.[slot] ?? null;
1304
+ if (!key) return null;
1305
+ try {
1306
+ const patterns = await fetchSitePatterns({
1307
+ apiKey: input.apiKey,
1308
+ baseUrl: input.baseUrl,
1309
+ tags: [SITE_PATTERNS_CACHE_TAG]
1310
+ });
1311
+ return selectSitePatternForSlot(post.patternSlots, patterns, slot);
1312
+ } catch {
1313
+ return null;
1314
+ }
1315
+ }
1316
+ function renderPostPattern(pattern, slot, input) {
1317
+ if (!pattern) return null;
1318
+ return /* @__PURE__ */ jsx13(
1319
+ "aside",
1320
+ {
1321
+ "data-roottale-cms": "post-pattern",
1322
+ "data-pattern-slot": slot,
1323
+ "data-pattern-key": pattern.key,
1324
+ className: "rt-cms-post-pattern",
1325
+ "aria-label": pattern.name,
1326
+ children: /* @__PURE__ */ jsx13(RenderTiptap, { doc: pattern.bodyJson, apiKey: input.apiKey, baseUrl: input.baseUrl })
1327
+ }
1328
+ );
1329
+ }
1330
+ async function RootTalePostPattern(props) {
1331
+ const slot = props.slot ?? POST_FOOTER_PATTERN_SLOT;
1332
+ const pattern = await resolvePostPatternFailSoft(props.post, props, slot);
1333
+ return renderPostPattern(pattern, slot, props);
1334
+ }
1335
+
1336
+ // src/server/blog-post-archive.tsx
1337
+ import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
1338
+ var FIELD_FACT_TYPES = /* @__PURE__ */ new Set([
1339
+ "text",
1340
+ "textarea",
1341
+ "email",
1342
+ "url",
1343
+ "select",
1344
+ "radio",
1345
+ "button_group",
1346
+ "number",
1347
+ "range"
1348
+ ]);
1349
+ function archiveGroupTitle(group) {
1350
+ if (group.collectionKey === "conditions") {
1351
+ return `\uC774 \uC99D\uC0C1\uACFC \uAD00\uB828\uB41C \uC9C8\uD658 ${group.items.length}`;
1352
+ }
1353
+ if (group.collectionKey === "treatments") {
1354
+ return `\uAD00\uB828 \uCE58\uB8CC\uBC95 ${group.items.length}`;
1355
+ }
1356
+ if (group.collectionKey === "symptoms") {
1357
+ return `\uAD00\uB828 \uC99D\uC0C1 ${group.items.length}`;
1358
+ }
1359
+ return `${group.label} ${group.items.length}`;
1360
+ }
1361
+ function defaultArchiveBasePath(collectionKey) {
1362
+ if (collectionKey === "symptoms") return "/symptoms";
1363
+ if (collectionKey === "conditions") return "/conditions";
1364
+ if (collectionKey === "treatments") return "/treatments";
1365
+ return "/blog";
1366
+ }
1367
+ function archivePostHref(collectionKey, slug, collections) {
1368
+ const collection = collections?.find((item) => item.key === collectionKey);
1369
+ const basePath = collection?.basePath || defaultArchiveBasePath(collectionKey);
1370
+ const trimmedBase = basePath.replace(/\/+$/, "");
1371
+ if (collection?.detail === false) return trimmedBase;
1372
+ return `${trimmedBase}/${slug}`;
1373
+ }
1374
+ function renderArchiveRelated(groups, collections) {
1375
+ const visibleGroups = groups.filter((group) => group.items.length > 0);
1376
+ if (visibleGroups.length === 0) return null;
1377
+ return /* @__PURE__ */ jsx14("aside", { className: "rt-archive-related", "aria-label": "\uC5F0\uAD00 \uC815\uBCF4", children: visibleGroups.map((group) => /* @__PURE__ */ jsxs9("section", { className: "rt-archive-related__group", children: [
1378
+ /* @__PURE__ */ jsx14("h2", { className: "rt-archive-related__title", children: archiveGroupTitle(group) }),
1379
+ /* @__PURE__ */ jsx14("ul", { className: "rt-archive-related__list", children: group.items.map((item) => /* @__PURE__ */ jsx14("li", { className: "rt-archive-related__item", children: /* @__PURE__ */ jsxs9(
1380
+ "a",
1381
+ {
1382
+ className: "rt-archive-related__link",
1383
+ href: archivePostHref(
1384
+ item.collectionKey,
1385
+ item.slug,
1386
+ collections
1387
+ ),
1388
+ children: [
1389
+ /* @__PURE__ */ jsx14("span", { className: "rt-archive-related__item-title", children: item.title }),
1390
+ item.excerpt ? /* @__PURE__ */ jsx14("span", { className: "rt-archive-related__item-excerpt", children: item.excerpt }) : null
1391
+ ]
1392
+ }
1393
+ ) }, item.id)) })
1394
+ ] }, group.collectionKey)) });
1395
+ }
1396
+ function fieldFactValue(raw, meta) {
1397
+ if (raw === null || raw === void 0) return null;
1398
+ if (typeof raw === "number") return raw.toLocaleString("ko-KR");
1399
+ if (typeof raw !== "string" || raw.trim() === "") return null;
1400
+ if (!meta.choices) return raw;
1401
+ const choice = meta.choices.find((item) => item.value === raw);
1402
+ return choice ? choice.label : raw;
1403
+ }
1404
+ function renderFieldFacts(post) {
1405
+ if (!post.fields) return null;
1406
+ const rows = post.fieldsMeta.filter((meta) => FIELD_FACT_TYPES.has(meta.type)).flatMap((meta) => {
1407
+ const value = fieldFactValue(post.fields?.[meta.name], meta);
1408
+ return value === null ? [] : [{ meta, value }];
1409
+ });
1410
+ if (rows.length === 0) return null;
1411
+ return /* @__PURE__ */ jsx14("aside", { className: "rt-archive-facts", "aria-label": "\uC0C1\uC138 \uC815\uBCF4", children: /* @__PURE__ */ jsx14("dl", { className: "rt-archive-facts__list", children: rows.map(({ meta, value }) => /* @__PURE__ */ jsxs9("div", { className: "rt-archive-facts__row", children: [
1412
+ /* @__PURE__ */ jsx14("dt", { className: "rt-archive-facts__label", children: meta.label }),
1413
+ /* @__PURE__ */ jsx14(
1414
+ "dd",
1415
+ {
1416
+ className: meta.type === "textarea" ? "rt-archive-facts__value rt-archive-facts__value--multiline" : "rt-archive-facts__value",
1417
+ children: value
1418
+ }
1419
+ )
1420
+ ] }, meta.name)) }) });
1421
+ }
1422
+
1334
1423
  // src/server/blog-post-body.tsx
1335
- import { Fragment as Fragment6, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
1424
+ import {
1425
+ internalContentLinkResolverFrom as internalContentLinkResolverFrom2,
1426
+ renderInternalContentLinksInHtml as renderInternalContentLinksInHtml2
1427
+ } from "@roottale/cms-core";
1428
+
1429
+ // src/server/post-body-source.ts
1430
+ import { sanitizeHtml as sanitizeHtml2 } from "@roottale/cms-core";
1431
+ function hasVisibleContent(html) {
1432
+ if (/<(?:img|hr|br|iframe|video|audio)\b/i.test(html)) return true;
1433
+ return html.replace(/<[^>]*>/g, "").replace(/&nbsp;/gi, " ").trim().length > 0;
1434
+ }
1435
+ function resolvePostBodySource(bodyHtml) {
1436
+ if (typeof bodyHtml !== "string") return { kind: "json" };
1437
+ const trimmed = bodyHtml.trim();
1438
+ if (trimmed.length === 0) return { kind: "json" };
1439
+ const sanitized = sanitizeHtml2(trimmed);
1440
+ if (!hasVisibleContent(sanitized)) return { kind: "json" };
1441
+ return { kind: "html", html: sanitized };
1442
+ }
1443
+
1444
+ // src/server/blog-post-body.tsx
1445
+ import { Fragment as Fragment6, jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
1336
1446
  function BlogPostBody(props) {
1337
1447
  const {
1338
1448
  doc,
@@ -1342,28 +1452,34 @@ function BlogPostBody(props) {
1342
1452
  toc,
1343
1453
  tocTitle,
1344
1454
  tocPosition,
1345
- bodyHtml
1455
+ bodyHtml,
1456
+ internalLinks = null
1346
1457
  } = props;
1347
- const tableOfContents = toc.length > 0 ? /* @__PURE__ */ jsx14(RootTaleTableOfContents, { entries: toc, title: tocTitle }) : null;
1458
+ const tableOfContents = toc.length > 0 ? /* @__PURE__ */ jsx15(RootTaleTableOfContents, { entries: toc, title: tocTitle }) : null;
1348
1459
  const source = resolvePostBodySource(bodyHtml);
1349
- const body = /* @__PURE__ */ jsx14("div", { "data-roottale-cms": "body", className: "rt-cms-body", children: source.kind === "html" ? (
1460
+ const sourceHtml = source.kind === "html" && internalLinks ? renderInternalContentLinksInHtml2(
1461
+ source.html,
1462
+ internalContentLinkResolverFrom2(internalLinks)
1463
+ ).html : source.kind === "html" ? source.html : "";
1464
+ const body = /* @__PURE__ */ jsx15("div", { "data-roottale-cms": "body", className: "rt-cms-body", children: source.kind === "html" ? (
1350
1465
  // source.html 은 resolvePostBodySource 가 공용 sanitizeHtml 로 정제한 값이다
1351
1466
  // (이 컴포넌트에서 한 번 더 정제하지 않는 이유 — sink 가 한 곳이어야 눈으로
1352
1467
  // 추적 가능하고, 이중 정제는 안전을 더 만들지 않으면서 비용만 든다).
1353
- /* @__PURE__ */ jsx14("div", { dangerouslySetInnerHTML: { __html: source.html } })
1354
- ) : /* @__PURE__ */ jsx14(
1468
+ /* @__PURE__ */ jsx15("div", { dangerouslySetInnerHTML: { __html: sourceHtml } })
1469
+ ) : /* @__PURE__ */ jsx15(
1355
1470
  RenderTiptap,
1356
1471
  {
1357
1472
  doc,
1358
1473
  fields,
1359
1474
  apiKey,
1360
- baseUrl
1475
+ baseUrl,
1476
+ internalLinks
1361
1477
  }
1362
1478
  ) });
1363
1479
  if (tocPosition === "sidebar" && tableOfContents) {
1364
1480
  return /* @__PURE__ */ jsxs10("div", { className: "rt-cms-layout rt-cms-layout--toc-side", children: [
1365
1481
  body,
1366
- /* @__PURE__ */ jsx14("aside", { className: "rt-cms-toc-side", children: tableOfContents })
1482
+ /* @__PURE__ */ jsx15("aside", { className: "rt-cms-toc-side", children: tableOfContents })
1367
1483
  ] });
1368
1484
  }
1369
1485
  return /* @__PURE__ */ jsxs10(Fragment6, { children: [
@@ -1380,7 +1496,7 @@ import {
1380
1496
  import {
1381
1497
  resolvePostCollection as resolvePostCollection2
1382
1498
  } from "@roottale/cms-core";
1383
- import { Fragment as Fragment7, jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
1499
+ import { Fragment as Fragment7, jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
1384
1500
  function absoluteUrl(siteUrl, path) {
1385
1501
  const normalizedPath = path.startsWith("/") ? path : `/${path}`;
1386
1502
  return `${trimSlash(siteUrl)}${normalizedPath}`;
@@ -1416,10 +1532,10 @@ function renderBlogPostBreadcrumb(input) {
1416
1532
  }))
1417
1533
  ) : null;
1418
1534
  return /* @__PURE__ */ jsxs11(Fragment7, { children: [
1419
- /* @__PURE__ */ jsx15("nav", { "aria-label": "\uBE0C\uB808\uB4DC\uD06C\uB7FC", className: "rt-cms-breadcrumb", children: /* @__PURE__ */ jsx15("ol", { children: crumbs.map(
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)
1535
+ /* @__PURE__ */ jsx16("nav", { "aria-label": "\uBE0C\uB808\uB4DC\uD06C\uB7FC", className: "rt-cms-breadcrumb", children: /* @__PURE__ */ jsx16("ol", { children: crumbs.map(
1536
+ (crumb, index) => index === crumbs.length - 1 ? /* @__PURE__ */ jsx16("li", { "aria-current": "page", children: crumb.label }, index) : /* @__PURE__ */ jsx16("li", { children: /* @__PURE__ */ jsx16("a", { href: crumb.href, children: crumb.label }) }, index)
1421
1537
  ) }) }),
1422
- jsonLd ? /* @__PURE__ */ jsx15(
1538
+ jsonLd ? /* @__PURE__ */ jsx16(
1423
1539
  "script",
1424
1540
  {
1425
1541
  type: "application/ld+json",
@@ -1429,12 +1545,51 @@ function renderBlogPostBreadcrumb(input) {
1429
1545
  ] });
1430
1546
  }
1431
1547
 
1548
+ // src/server/internal-links.ts
1549
+ import { internalContentPathIndex } from "@roottale/cms-core";
1550
+ import { fetchPosts as fetchPosts2 } from "@roottale/cms-client/server";
1551
+ async function fetchInternalContentPathIndex(options) {
1552
+ const entries = [];
1553
+ const maxPages = options.maxPages ?? 20;
1554
+ let cursor;
1555
+ for (let page = 0; page < maxPages; page += 1) {
1556
+ try {
1557
+ const result = await fetchPosts2({
1558
+ apiKey: options.apiKey,
1559
+ baseUrl: options.baseUrl,
1560
+ type: "post",
1561
+ limit: 100,
1562
+ cursor,
1563
+ ...options.locale ? { locale: options.locale } : {},
1564
+ ...options.revalidate !== void 0 ? { revalidate: options.revalidate } : {}
1565
+ });
1566
+ for (const post of result.items) {
1567
+ entries.push({ path: post.path, previousSlugs: post.previousSlugs });
1568
+ }
1569
+ if (!result.hasMore || !result.nextCursor) break;
1570
+ cursor = result.nextCursor;
1571
+ } catch {
1572
+ break;
1573
+ }
1574
+ }
1575
+ return internalContentPathIndex(entries);
1576
+ }
1577
+ function bodyHasInternalContentLinkTokens(doc, html) {
1578
+ if (html && html.includes("[[internal:")) return true;
1579
+ if (!doc) return false;
1580
+ try {
1581
+ return JSON.stringify(doc).includes("[[internal:");
1582
+ } catch {
1583
+ return false;
1584
+ }
1585
+ }
1586
+
1432
1587
  // src/server/blog-post-footer.tsx
1433
1588
  import {
1434
1589
  jsonLdJson as jsonLdJson2
1435
1590
  } from "@roottale/cms-client/server";
1436
1591
  import { extractFaqEntries, faqPageJsonLd } from "@roottale/cms-core";
1437
- import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
1592
+ import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
1438
1593
  function renderPostCta(cta) {
1439
1594
  if (!cta) return null;
1440
1595
  const href = cta.buttonHref.trim();
@@ -1442,9 +1597,9 @@ function renderPostCta(cta) {
1442
1597
  if (!isSafeHref2) return null;
1443
1598
  const isExternal = /^https?:/i.test(href);
1444
1599
  return /* @__PURE__ */ jsxs12("aside", { "data-roottale-cms": "post-cta", className: "rt-cms-post-cta", children: [
1445
- cta.title ? /* @__PURE__ */ jsx16("p", { className: "rt-cms-post-cta__title", children: cta.title }) : null,
1446
- cta.description ? /* @__PURE__ */ jsx16("p", { className: "rt-cms-post-cta__desc", children: cta.description }) : null,
1447
- /* @__PURE__ */ jsx16(
1600
+ cta.title ? /* @__PURE__ */ jsx17("p", { className: "rt-cms-post-cta__title", children: cta.title }) : null,
1601
+ cta.description ? /* @__PURE__ */ jsx17("p", { className: "rt-cms-post-cta__desc", children: cta.description }) : null,
1602
+ /* @__PURE__ */ jsx17(
1448
1603
  "a",
1449
1604
  {
1450
1605
  className: "rt-cms-post-cta__btn",
@@ -1459,9 +1614,9 @@ function renderPostCta(cta) {
1459
1614
  function renderRelatedPosts(posts, href, title) {
1460
1615
  if (posts.length === 0) return null;
1461
1616
  return /* @__PURE__ */ jsxs12("nav", { "data-roottale-cms": "related", className: "rt-cms-related", "aria-label": title, children: [
1462
- /* @__PURE__ */ jsx16("h2", { className: "rt-cms-related-title", children: title }),
1463
- /* @__PURE__ */ jsx16("ul", { className: "rt-cms-related-list", children: posts.map((post) => /* @__PURE__ */ jsx16("li", { className: "rt-cms-related-item", children: /* @__PURE__ */ jsxs12("a", { className: "rt-cms-related-link", href: href(post), children: [
1464
- post.featuredImageUrl ? /* @__PURE__ */ jsx16(
1617
+ /* @__PURE__ */ jsx17("h2", { className: "rt-cms-related-title", children: title }),
1618
+ /* @__PURE__ */ jsx17("ul", { className: "rt-cms-related-list", children: posts.map((post) => /* @__PURE__ */ jsx17("li", { className: "rt-cms-related-item", children: /* @__PURE__ */ jsxs12("a", { className: "rt-cms-related-link", href: href(post), children: [
1619
+ post.featuredImageUrl ? /* @__PURE__ */ jsx17(
1465
1620
  "img",
1466
1621
  {
1467
1622
  className: "rt-cms-related-thumb",
@@ -1470,14 +1625,14 @@ function renderRelatedPosts(posts, href, title) {
1470
1625
  loading: "lazy"
1471
1626
  }
1472
1627
  ) : null,
1473
- /* @__PURE__ */ jsx16("span", { className: "rt-cms-related-post-title", children: post.title })
1628
+ /* @__PURE__ */ jsx17("span", { className: "rt-cms-related-post-title", children: post.title })
1474
1629
  ] }) }, post.id)) })
1475
1630
  ] });
1476
1631
  }
1477
1632
  function renderFaqJsonLd(doc) {
1478
1633
  const jsonLd = faqPageJsonLd(extractFaqEntries(doc));
1479
1634
  if (!jsonLd) return null;
1480
- return /* @__PURE__ */ jsx16(
1635
+ return /* @__PURE__ */ jsx17(
1481
1636
  "script",
1482
1637
  {
1483
1638
  type: "application/ld+json",
@@ -1492,10 +1647,10 @@ function RootTaleFaqJsonLd({
1492
1647
  }
1493
1648
 
1494
1649
  // src/server/blog-post.tsx
1495
- import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
1650
+ import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
1496
1651
  async function fetchRelatedPostsFailSoft(input) {
1497
1652
  if (input.count <= 0) return [];
1498
- const candidates = await fetchPosts2({
1653
+ const candidates = await fetchPosts3({
1499
1654
  apiKey: input.apiKey,
1500
1655
  baseUrl: input.baseUrl,
1501
1656
  type: "post",
@@ -1567,6 +1722,16 @@ async function lookupPost(input) {
1567
1722
  });
1568
1723
  return post ? { kind: "post", post } : { kind: "missing" };
1569
1724
  }
1725
+ async function resolveInternalLinksSource(source, input) {
1726
+ if (source === false) return null;
1727
+ if (source !== "auto" && source !== void 0) return source;
1728
+ if (!input.hasTokens) return null;
1729
+ return fetchInternalContentPathIndex({
1730
+ apiKey: input.apiKey,
1731
+ baseUrl: input.baseUrl,
1732
+ locale: input.locale
1733
+ });
1734
+ }
1570
1735
  async function RootTaleBlogPost(props) {
1571
1736
  const {
1572
1737
  apiKey,
@@ -1585,7 +1750,8 @@ async function RootTaleBlogPost(props) {
1585
1750
  showFieldFacts = false,
1586
1751
  theme: themeProp,
1587
1752
  authorHref = defaultAuthorHref,
1588
- breadcrumb
1753
+ breadcrumb,
1754
+ internalLinks = "auto"
1589
1755
  } = props;
1590
1756
  const [lookup, themeStyle, blogSettings] = await Promise.all([
1591
1757
  lookupPost({ apiKey, baseUrl, slugOrId, previewToken, locale }),
@@ -1593,10 +1759,10 @@ async function RootTaleBlogPost(props) {
1593
1759
  fetchBlogSettingsFailSoft({ apiKey, baseUrl })
1594
1760
  ]);
1595
1761
  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") }) });
1762
+ return previewExpiredElement ?? /* @__PURE__ */ jsx18("div", { "data-roottale-cms": "post-preview-expired", style: themeStyle ?? void 0, children: /* @__PURE__ */ jsx18("p", { className: "rt-cms-empty", children: t(locale, "previewExpired") }) });
1597
1763
  }
1598
1764
  if (lookup.kind === "missing") {
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") }) });
1765
+ return notFoundElement ?? /* @__PURE__ */ jsx18("div", { "data-roottale-cms": "post-missing", style: themeStyle ?? void 0, children: /* @__PURE__ */ jsx18("p", { className: "rt-cms-empty", children: t(locale, "postNotFound") }) });
1600
1766
  }
1601
1767
  const post = lookup.post;
1602
1768
  const display = resolvePostDisplay2(post, blogSettings, {
@@ -1605,13 +1771,22 @@ async function RootTaleBlogPost(props) {
1605
1771
  });
1606
1772
  const toc = display.showTableOfContents ? extractToc(post.bodyJson) : [];
1607
1773
  const renderDoc = toc.length > 0 ? attachHeadingIds(post.bodyJson, toc) : post.bodyJson;
1608
- const related = await fetchRelatedPostsFailSoft({
1609
- post,
1610
- apiKey,
1611
- baseUrl,
1612
- count: relatedPostsCount,
1613
- locale
1614
- });
1774
+ const [related, internalLinkIndex, footerPattern] = await Promise.all([
1775
+ fetchRelatedPostsFailSoft({
1776
+ post,
1777
+ apiKey,
1778
+ baseUrl,
1779
+ count: relatedPostsCount,
1780
+ locale
1781
+ }),
1782
+ resolveInternalLinksSource(internalLinks, {
1783
+ apiKey,
1784
+ baseUrl,
1785
+ locale,
1786
+ hasTokens: bodyHasInternalContentLinkTokens(post.bodyJson, post.bodyHtml)
1787
+ }),
1788
+ resolvePostPatternFailSoft(post, { apiKey, baseUrl }, POST_FOOTER_PATTERN_SLOT2)
1789
+ ]);
1615
1790
  const relatedHref = props.postHref ?? (collections ? makeCollectionPostHref(collections) : defaultPostHref);
1616
1791
  const authorName = post.authorName;
1617
1792
  const authorSlug = post.authorSlug;
@@ -1645,7 +1820,7 @@ async function RootTaleBlogPost(props) {
1645
1820
  options: breadcrumb
1646
1821
  }),
1647
1822
  /* @__PURE__ */ jsxs13("header", { className: "rt-cms-post-header", children: [
1648
- categories.length > 0 ? /* @__PURE__ */ jsx17("nav", { className: "rt-cms-post-categories", "aria-label": "\uAE00 \uCE74\uD14C\uACE0\uB9AC", children: categories.map((category) => /* @__PURE__ */ jsx17(
1823
+ categories.length > 0 ? /* @__PURE__ */ jsx18("nav", { className: "rt-cms-post-categories", "aria-label": "\uAE00 \uCE74\uD14C\uACE0\uB9AC", children: categories.map((category) => /* @__PURE__ */ jsx18(
1649
1824
  "a",
1650
1825
  {
1651
1826
  className: "rt-cms-post-category",
@@ -1654,8 +1829,8 @@ async function RootTaleBlogPost(props) {
1654
1829
  },
1655
1830
  category.id
1656
1831
  )) }) : null,
1657
- showTitle ? /* @__PURE__ */ jsx17("h1", { className: "rt-cms-title", children: post.title }) : null,
1658
- post.excerpt ? /* @__PURE__ */ jsx17("p", { className: "rt-cms-excerpt", children: post.excerpt }) : null,
1832
+ showTitle ? /* @__PURE__ */ jsx18("h1", { className: "rt-cms-title", children: post.title }) : null,
1833
+ post.excerpt ? /* @__PURE__ */ jsx18("p", { className: "rt-cms-excerpt", children: post.excerpt }) : null,
1659
1834
  publishedDate || modifiedDate || showTopAuthor || views !== null || reviewedDate ? /* @__PURE__ */ jsxs13("p", { className: "rt-cms-meta", children: [
1660
1835
  publishedDate ? /* @__PURE__ */ jsxs13(
1661
1836
  "time",
@@ -1684,7 +1859,7 @@ async function RootTaleBlogPost(props) {
1684
1859
  showTopAuthor ? /* @__PURE__ */ jsxs13("span", { className: "rt-cms-meta-item", "data-rt-cms-meta": "author", children: [
1685
1860
  "\uC791\uC131\uC790",
1686
1861
  " ",
1687
- authorSlug ? /* @__PURE__ */ jsx17("a", { className: "rt-cms-author-link", href: authorHref(authorSlug), children: authorName }) : authorName
1862
+ authorSlug ? /* @__PURE__ */ jsx18("a", { className: "rt-cms-author-link", href: authorHref(authorSlug), children: authorName }) : authorName
1688
1863
  ] }) : null,
1689
1864
  editorialReview && reviewedDate ? /* @__PURE__ */ jsxs13("span", { className: "rt-cms-meta-item", "data-rt-cms-meta": "reviewed", children: [
1690
1865
  "\uB0B4\uC6A9 \uAC80\uD1A0 ",
@@ -1700,7 +1875,7 @@ async function RootTaleBlogPost(props) {
1700
1875
  ] }),
1701
1876
  renderArchiveRelated(post.archiveRelated, collections),
1702
1877
  showFieldFacts ? renderFieldFacts(post) : null,
1703
- /* @__PURE__ */ jsx17(
1878
+ /* @__PURE__ */ jsx18(
1704
1879
  BlogPostBody,
1705
1880
  {
1706
1881
  doc: renderDoc,
@@ -1710,11 +1885,13 @@ async function RootTaleBlogPost(props) {
1710
1885
  toc,
1711
1886
  tocTitle: display.tocTitle,
1712
1887
  tocPosition: display.tocPosition,
1713
- bodyHtml: post.bodyHtml
1888
+ bodyHtml: post.bodyHtml,
1889
+ internalLinks: internalLinkIndex
1714
1890
  }
1715
1891
  ),
1892
+ renderPostPattern(footerPattern, POST_FOOTER_PATTERN_SLOT2, { apiKey, baseUrl }),
1716
1893
  showAuthorCard && authorName ? /* @__PURE__ */ jsxs13("section", { className: "rt-cms-author-card", "aria-label": "\uC791\uC131\uC790", children: [
1717
- authorImageUrl ? /* @__PURE__ */ jsx17(
1894
+ authorImageUrl ? /* @__PURE__ */ jsx18(
1718
1895
  "img",
1719
1896
  {
1720
1897
  className: "rt-cms-author-card__image",
@@ -1723,11 +1900,11 @@ async function RootTaleBlogPost(props) {
1723
1900
  loading: "lazy",
1724
1901
  style: authorImageStyle
1725
1902
  }
1726
- ) : /* @__PURE__ */ jsx17("div", { className: "rt-cms-author-card__avatar", "aria-hidden": "true", children: authorInitials(authorName) }),
1903
+ ) : /* @__PURE__ */ jsx18("div", { className: "rt-cms-author-card__avatar", "aria-hidden": "true", children: authorInitials(authorName) }),
1727
1904
  /* @__PURE__ */ jsxs13("div", { className: "rt-cms-author-card__body", children: [
1728
- /* @__PURE__ */ jsx17("p", { className: "rt-cms-author-card__eyebrow", children: display.authorCardEyebrow }),
1729
- /* @__PURE__ */ jsx17("p", { className: "rt-cms-author-card__name", children: authorSlug ? /* @__PURE__ */ jsx17("a", { className: "rt-cms-author-link", href: authorHref(authorSlug), children: authorName }) : authorName }),
1730
- authorBio ? /* @__PURE__ */ jsx17("p", { className: "rt-cms-author-card__description", children: authorBio }) : null
1905
+ /* @__PURE__ */ jsx18("p", { className: "rt-cms-author-card__eyebrow", children: display.authorCardEyebrow }),
1906
+ /* @__PURE__ */ jsx18("p", { className: "rt-cms-author-card__name", children: authorSlug ? /* @__PURE__ */ jsx18("a", { className: "rt-cms-author-link", href: authorHref(authorSlug), children: authorName }) : authorName }),
1907
+ authorBio ? /* @__PURE__ */ jsx18("p", { className: "rt-cms-author-card__description", children: authorBio }) : null
1731
1908
  ] })
1732
1909
  ] }) : null,
1733
1910
  renderRelatedPosts(related, relatedHref, relatedPostsTitle),
@@ -1739,7 +1916,7 @@ async function RootTaleBlogPost(props) {
1739
1916
  }
1740
1917
 
1741
1918
  // src/server/preview-notice.tsx
1742
- import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
1919
+ import { jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
1743
1920
  function formatExpiry(locale, iso) {
1744
1921
  const date = new Date(iso);
1745
1922
  if (Number.isNaN(date.getTime())) return null;
@@ -1756,14 +1933,14 @@ function RootTalePreviewNotice({
1756
1933
  }) {
1757
1934
  const expiry = expiresAt ? formatExpiry(locale, expiresAt) : null;
1758
1935
  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
1936
+ /* @__PURE__ */ jsx19("span", { children: message ?? t(locale, "previewNotice") }),
1937
+ expiry ? /* @__PURE__ */ jsx19("span", { className: "rt-cms-preview-notice__expiry", children: expiry }) : null
1761
1938
  ] });
1762
1939
  }
1763
1940
 
1764
1941
  // src/server/page.tsx
1765
1942
  import { fetchPost as fetchPost2 } from "@roottale/cms-client/server";
1766
- import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
1943
+ import { jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
1767
1944
  async function RootTalePage(props) {
1768
1945
  const {
1769
1946
  apiKey,
@@ -1780,7 +1957,7 @@ async function RootTalePage(props) {
1780
1957
  resolveThemeStyleFailSoft({ themeProp, apiKey, baseUrl })
1781
1958
  ]);
1782
1959
  if (!post) {
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") }) });
1960
+ return notFoundElement ?? /* @__PURE__ */ jsx20("div", { "data-roottale-cms": "page-missing", style: themeStyle ?? void 0, children: /* @__PURE__ */ jsx20("p", { className: "rt-cms-empty", children: t(locale, "pageNotFound") }) });
1784
1961
  }
1785
1962
  const pageBodySource = resolvePostBodySource(post.bodyHtml);
1786
1963
  const bodyJson = pageBodySource.kind === "json" && prepareBody ? await prepareBody(post.bodyJson) : post.bodyJson;
@@ -1791,8 +1968,8 @@ async function RootTalePage(props) {
1791
1968
  className: "rt-cms-article",
1792
1969
  style: themeStyle ?? void 0,
1793
1970
  children: [
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(
1971
+ showTitle ? /* @__PURE__ */ jsx20("header", { className: "rt-cms-post-header", children: /* @__PURE__ */ jsx20("h1", { className: "rt-cms-title", children: post.title }) }) : null,
1972
+ /* @__PURE__ */ jsx20("div", { "data-roottale-cms": "body", className: "rt-cms-body", children: pageBodySource.kind === "html" ? /* @__PURE__ */ jsx20("div", { dangerouslySetInnerHTML: { __html: pageBodySource.html } }) : /* @__PURE__ */ jsx20(
1796
1973
  RenderTiptap,
1797
1974
  {
1798
1975
  doc: bodyJson,
@@ -1808,7 +1985,7 @@ async function RootTalePage(props) {
1808
1985
 
1809
1986
  // src/server/exposure-slot.tsx
1810
1987
  import { fetchExposure } from "@roottale/cms-client/server";
1811
- import { jsx as jsx20 } from "react/jsx-runtime";
1988
+ import { jsx as jsx21 } from "react/jsx-runtime";
1812
1989
  async function RootTaleExposureSlot(props) {
1813
1990
  const campaign = await fetchExposure({
1814
1991
  apiKey: props.apiKey,
@@ -1819,7 +1996,7 @@ async function RootTaleExposureSlot(props) {
1819
1996
  revalidate: props.revalidate
1820
1997
  });
1821
1998
  if (!campaign || !props.allowedVariants.includes(campaign.content.variant)) return null;
1822
- return /* @__PURE__ */ jsx20(RootTaleExposure, { campaign, allowedVariants: props.allowedVariants });
1999
+ return /* @__PURE__ */ jsx21(RootTaleExposure, { campaign, allowedVariants: props.allowedVariants });
1823
2000
  }
1824
2001
  export {
1825
2002
  RenderTiptap,
@@ -1832,16 +2009,20 @@ export {
1832
2009
  RootTaleLeadForm,
1833
2010
  RootTalePage,
1834
2011
  RootTalePostCard,
2012
+ RootTalePostPattern,
1835
2013
  RootTalePreviewNotice,
1836
2014
  RootTaleTableOfContents,
1837
2015
  RootTaleThemeProvider,
1838
2016
  attachHeadingIds,
2017
+ bodyHasInternalContentLinkTokens,
1839
2018
  collectCategories,
1840
2019
  collectCategoriesWithCounts,
1841
2020
  defaultCategoryHref,
1842
2021
  extractToc,
2022
+ fetchInternalContentPathIndex,
1843
2023
  filterPostsByCategory,
1844
2024
  headingToId,
2025
+ makeCategoryHref,
1845
2026
  renderBlock,
1846
2027
  renderBlocks,
1847
2028
  renderBoundBlocks,