@roottale/cms-renderer-next 0.56.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
@@ -857,115 +857,27 @@ async function RootTaleBlogCategories(props) {
857
857
 
858
858
  // src/server/blog-post.tsx
859
859
  import {
860
+ POST_FOOTER_PATTERN_SLOT as POST_FOOTER_PATTERN_SLOT2,
860
861
  fetchPost,
861
862
  fetchPostPreview,
862
- fetchPosts as fetchPosts2,
863
+ fetchPosts as fetchPosts3,
863
864
  isPreviewExpiredError,
864
865
  resolvePostDisplay as resolvePostDisplay2,
865
866
  selectRelatedPosts
866
867
  } from "@roottale/cms-client/server";
867
868
 
868
- // src/server/blog-post-archive.tsx
869
- import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
870
- var FIELD_FACT_TYPES = /* @__PURE__ */ new Set([
871
- "text",
872
- "textarea",
873
- "email",
874
- "url",
875
- "select",
876
- "radio",
877
- "button_group",
878
- "number",
879
- "range"
880
- ]);
881
- function archiveGroupTitle(group) {
882
- if (group.collectionKey === "conditions") {
883
- return `\uC774 \uC99D\uC0C1\uACFC \uAD00\uB828\uB41C \uC9C8\uD658 ${group.items.length}`;
884
- }
885
- if (group.collectionKey === "treatments") {
886
- return `\uAD00\uB828 \uCE58\uB8CC\uBC95 ${group.items.length}`;
887
- }
888
- if (group.collectionKey === "symptoms") {
889
- return `\uAD00\uB828 \uC99D\uC0C1 ${group.items.length}`;
890
- }
891
- return `${group.label} ${group.items.length}`;
892
- }
893
- function defaultArchiveBasePath(collectionKey) {
894
- if (collectionKey === "symptoms") return "/symptoms";
895
- if (collectionKey === "conditions") return "/conditions";
896
- if (collectionKey === "treatments") return "/treatments";
897
- return "/blog";
898
- }
899
- function archivePostHref(collectionKey, slug, collections) {
900
- const collection = collections?.find((item) => item.key === collectionKey);
901
- const basePath = collection?.basePath || defaultArchiveBasePath(collectionKey);
902
- const trimmedBase = basePath.replace(/\/+$/, "");
903
- if (collection?.detail === false) return trimmedBase;
904
- return `${trimmedBase}/${slug}`;
905
- }
906
- function renderArchiveRelated(groups, collections) {
907
- const visibleGroups = groups.filter((group) => group.items.length > 0);
908
- if (visibleGroups.length === 0) return null;
909
- 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: [
910
- /* @__PURE__ */ jsx9("h2", { className: "rt-archive-related__title", children: archiveGroupTitle(group) }),
911
- /* @__PURE__ */ jsx9("ul", { className: "rt-archive-related__list", children: group.items.map((item) => /* @__PURE__ */ jsx9("li", { className: "rt-archive-related__item", children: /* @__PURE__ */ jsxs8(
912
- "a",
913
- {
914
- className: "rt-archive-related__link",
915
- href: archivePostHref(
916
- item.collectionKey,
917
- item.slug,
918
- collections
919
- ),
920
- children: [
921
- /* @__PURE__ */ jsx9("span", { className: "rt-archive-related__item-title", children: item.title }),
922
- item.excerpt ? /* @__PURE__ */ jsx9("span", { className: "rt-archive-related__item-excerpt", children: item.excerpt }) : null
923
- ]
924
- }
925
- ) }, item.id)) })
926
- ] }, group.collectionKey)) });
927
- }
928
- function fieldFactValue(raw, meta) {
929
- if (raw === null || raw === void 0) return null;
930
- if (typeof raw === "number") return raw.toLocaleString("ko-KR");
931
- if (typeof raw !== "string" || raw.trim() === "") return null;
932
- if (!meta.choices) return raw;
933
- const choice = meta.choices.find((item) => item.value === raw);
934
- return choice ? choice.label : raw;
935
- }
936
- function renderFieldFacts(post) {
937
- if (!post.fields) return null;
938
- const rows = post.fieldsMeta.filter((meta) => FIELD_FACT_TYPES.has(meta.type)).flatMap((meta) => {
939
- const value = fieldFactValue(post.fields?.[meta.name], meta);
940
- return value === null ? [] : [{ meta, value }];
941
- });
942
- if (rows.length === 0) return null;
943
- 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: [
944
- /* @__PURE__ */ jsx9("dt", { className: "rt-archive-facts__label", children: meta.label }),
945
- /* @__PURE__ */ jsx9(
946
- "dd",
947
- {
948
- className: meta.type === "textarea" ? "rt-archive-facts__value rt-archive-facts__value--multiline" : "rt-archive-facts__value",
949
- children: value
950
- }
951
- )
952
- ] }, meta.name)) }) });
953
- }
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";
954
876
 
955
- // src/server/post-body-source.ts
956
- import { sanitizeHtml as sanitizeHtml2 } from "@roottale/cms-core";
957
- function hasVisibleContent(html) {
958
- if (/<(?:img|hr|br|iframe|video|audio)\b/i.test(html)) return true;
959
- return html.replace(/<[^>]*>/g, "").replace(/&nbsp;/gi, " ").trim().length > 0;
960
- }
961
- function resolvePostBodySource(bodyHtml) {
962
- if (typeof bodyHtml !== "string") return { kind: "json" };
963
- const trimmed = bodyHtml.trim();
964
- if (trimmed.length === 0) return { kind: "json" };
965
- const sanitized = sanitizeHtml2(trimmed);
966
- if (!hasVisibleContent(sanitized)) return { kind: "json" };
967
- return { kind: "html", html: sanitized };
968
- }
877
+ // src/server/render-tiptap.tsx
878
+ import {
879
+ internalContentLinkResolverFrom
880
+ } from "@roottale/cms-core";
969
881
 
970
882
  // src/server/tiptap-node-renderer.tsx
971
883
  import { Fragment as Fragment4 } from "react";
@@ -975,12 +887,13 @@ import {
975
887
  boundFieldValue,
976
888
  ensureImportedHtmlDocumentRoots,
977
889
  normalizeImageAlt,
890
+ renderInternalContentLinksInHtml,
978
891
  scopeImportedCss
979
892
  } from "@roottale/cms-core";
980
893
 
981
894
  // src/server/tiptap-embed-renderer.tsx
982
895
  import { isAllowedEmbedSrc } from "@roottale/cms-core";
983
- import { jsx as jsx10 } from "react/jsx-runtime";
896
+ import { jsx as jsx9 } from "react/jsx-runtime";
984
897
  function isSafeFallbackHref(value) {
985
898
  if (typeof value !== "string") return false;
986
899
  const href = value.trim();
@@ -994,13 +907,13 @@ function renderEmbedNode(node, key) {
994
907
  const src = node.attrs?.src;
995
908
  if (isAllowedEmbedSrc(src)) {
996
909
  const provider = typeof node.attrs?.provider === "string" ? node.attrs.provider : void 0;
997
- return /* @__PURE__ */ jsx10(
910
+ return /* @__PURE__ */ jsx9(
998
911
  "div",
999
912
  {
1000
913
  className: "rt-embed",
1001
914
  "data-provider": provider,
1002
915
  style: { aspectRatio: "16 / 9" },
1003
- children: /* @__PURE__ */ jsx10(
916
+ children: /* @__PURE__ */ jsx9(
1004
917
  "iframe",
1005
918
  {
1006
919
  src,
@@ -1018,9 +931,17 @@ function renderEmbedNode(node, key) {
1018
931
  }
1019
932
  const originalUrl = node.attrs?.originalUrl;
1020
933
  if (!isSafeFallbackHref(originalUrl)) return null;
1021
- 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);
1022
935
  }
1023
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
+
1024
945
  // src/server/tiptap-types.ts
1025
946
  function isRecord(value) {
1026
947
  return typeof value === "object" && value !== null;
@@ -1044,7 +965,7 @@ function parseTiptapMark(value) {
1044
965
  }
1045
966
 
1046
967
  // src/server/tiptap-inline-renderer.tsx
1047
- import { Fragment as Fragment3, jsx as jsx11 } from "react/jsx-runtime";
968
+ import { Fragment as Fragment3, jsx as jsx10 } from "react/jsx-runtime";
1048
969
  function isSafeHref(value) {
1049
970
  if (typeof value !== "string") return false;
1050
971
  const href = value.trim();
@@ -1057,52 +978,86 @@ function isSafeHref(value) {
1057
978
  function applyMark(mark, child) {
1058
979
  switch (mark.type) {
1059
980
  case "bold":
1060
- return /* @__PURE__ */ jsx11("strong", { children: child });
981
+ return /* @__PURE__ */ jsx10("strong", { children: child });
1061
982
  case "italic":
1062
- return /* @__PURE__ */ jsx11("em", { children: child });
983
+ return /* @__PURE__ */ jsx10("em", { children: child });
1063
984
  case "underline":
1064
- return /* @__PURE__ */ jsx11("u", { children: child });
985
+ return /* @__PURE__ */ jsx10("u", { children: child });
1065
986
  case "strike":
1066
- return /* @__PURE__ */ jsx11("s", { children: child });
987
+ return /* @__PURE__ */ jsx10("s", { children: child });
1067
988
  case "code":
1068
- return /* @__PURE__ */ jsx11("code", { children: child });
989
+ return /* @__PURE__ */ jsx10("code", { children: child });
1069
990
  case "highlight": {
1070
991
  const color = mark.attrs?.color;
1071
992
  const style = typeof color === "string" && color.length > 0 ? { background: color } : void 0;
1072
- return /* @__PURE__ */ jsx11("mark", { style, children: child });
993
+ return /* @__PURE__ */ jsx10("mark", { style, children: child });
1073
994
  }
1074
995
  case "textStyle": {
1075
996
  const color = mark.attrs?.color;
1076
997
  if (typeof color !== "string" || color.length === 0) {
1077
- return /* @__PURE__ */ jsx11(Fragment3, { children: child });
998
+ return /* @__PURE__ */ jsx10(Fragment3, { children: child });
1078
999
  }
1079
- return /* @__PURE__ */ jsx11("span", { style: { color }, children: child });
1000
+ return /* @__PURE__ */ jsx10("span", { style: { color }, children: child });
1080
1001
  }
1081
1002
  case "link": {
1082
1003
  const rawHref = mark.attrs?.href;
1083
1004
  const href = isSafeHref(rawHref) ? rawHref : "#";
1084
- 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 });
1085
1006
  }
1086
1007
  default:
1087
- return /* @__PURE__ */ jsx11("span", { children: child });
1008
+ return /* @__PURE__ */ jsx10("span", { children: child });
1088
1009
  }
1089
1010
  }
1090
- 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) {
1091
1039
  const text = node.text ?? "";
1092
1040
  const marks = (node.marks ?? []).flatMap((value) => {
1093
1041
  const mark = parseTiptapMark(value);
1094
1042
  return mark ? [mark] : [];
1095
1043
  });
1096
- if (marks.length === 0) return text;
1097
- 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 });
1098
1050
  for (const mark of marks) {
1099
1051
  element = applyMark(mark, element);
1100
1052
  }
1101
- return /* @__PURE__ */ jsx11("span", { children: element }, key);
1053
+ return /* @__PURE__ */ jsx10("span", { children: element }, key);
1102
1054
  }
1103
1055
 
1104
1056
  // src/server/tiptap-node-renderer.tsx
1105
- 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
+ }
1106
1061
  function boundContentText(node, fields) {
1107
1062
  return bindingTextValue(
1108
1063
  boundFieldValue(node.attrs?.bindings, "content", fields)
@@ -1135,7 +1090,7 @@ function renderTiptapNode(value, key, context) {
1135
1090
  switch (node.type) {
1136
1091
  case "paragraph": {
1137
1092
  const bound = boundContentText(node, context.fields);
1138
- return /* @__PURE__ */ jsx12("p", { style: alignStyle(node), children: bound ?? children }, key);
1093
+ return /* @__PURE__ */ jsx11("p", { style: alignStyle(node), children: bound ?? children }, key);
1139
1094
  }
1140
1095
  case "heading": {
1141
1096
  const rawLevel = Number(node.attrs?.level ?? 2);
@@ -1146,27 +1101,29 @@ function renderTiptapNode(value, key, context) {
1146
1101
  const style = alignStyle(node);
1147
1102
  const inner = boundContentText(node, context.fields) ?? children;
1148
1103
  if (level === 2) {
1149
- return /* @__PURE__ */ jsx12("h2", { id, style, children: inner }, key);
1104
+ return /* @__PURE__ */ jsx11("h2", { id, style, children: inner }, key);
1150
1105
  }
1151
1106
  if (level === 3) {
1152
- return /* @__PURE__ */ jsx12("h3", { id, style, children: inner }, key);
1107
+ return /* @__PURE__ */ jsx11("h3", { id, style, children: inner }, key);
1153
1108
  }
1154
- return /* @__PURE__ */ jsx12("h4", { id, style, children: inner }, key);
1109
+ return /* @__PURE__ */ jsx11("h4", { id, style, children: inner }, key);
1155
1110
  }
1156
1111
  case "bulletList":
1157
- return /* @__PURE__ */ jsx12("ul", { children }, key);
1112
+ return /* @__PURE__ */ jsx11("ul", { children }, key);
1158
1113
  case "orderedList":
1159
- return /* @__PURE__ */ jsx12("ol", { children }, key);
1114
+ return /* @__PURE__ */ jsx11("ol", { children }, key);
1160
1115
  case "listItem":
1161
- return /* @__PURE__ */ jsx12("li", { children }, key);
1116
+ return /* @__PURE__ */ jsx11("li", { children }, key);
1162
1117
  case "blockquote":
1163
- return /* @__PURE__ */ jsx12("blockquote", { children }, key);
1118
+ return /* @__PURE__ */ jsx11("blockquote", { children }, key);
1164
1119
  case "codeBlock":
1165
- 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);
1166
1123
  case "horizontalRule":
1167
- return /* @__PURE__ */ jsx12("hr", {}, key);
1124
+ return /* @__PURE__ */ jsx11("hr", {}, key);
1168
1125
  case "hardBreak":
1169
- return /* @__PURE__ */ jsx12("br", {}, key);
1126
+ return /* @__PURE__ */ jsx11("br", {}, key);
1170
1127
  case "image": {
1171
1128
  const boundSrc = bindingSrcValue(
1172
1129
  boundFieldValue(node.attrs?.bindings, "src", context.fields)
@@ -1177,7 +1134,7 @@ function renderTiptapNode(value, key, context) {
1177
1134
  const title = typeof node.attrs?.title === "string" ? node.attrs.title : void 0;
1178
1135
  const width = typeof node.attrs?.width === "number" && node.attrs.width > 0 ? node.attrs.width : void 0;
1179
1136
  const height = typeof node.attrs?.height === "number" && node.attrs.height > 0 ? node.attrs.height : void 0;
1180
- return /* @__PURE__ */ jsx12(
1137
+ return /* @__PURE__ */ jsx11(
1181
1138
  "img",
1182
1139
  {
1183
1140
  src,
@@ -1202,59 +1159,62 @@ function renderTiptapNode(value, key, context) {
1202
1159
  scope,
1203
1160
  assetUrls
1204
1161
  );
1205
- return /* @__PURE__ */ jsxs9(Fragment4, { children: [
1206
- scopedCss ? /* @__PURE__ */ jsx12("style", { "data-rt-imported-style": scope, children: scopedCss }) : null,
1207
- /* @__PURE__ */ jsx12(
1162
+ return /* @__PURE__ */ jsxs8(Fragment4, { children: [
1163
+ scopedCss ? /* @__PURE__ */ jsx11("style", { "data-rt-imported-style": scope, children: scopedCss }) : null,
1164
+ /* @__PURE__ */ jsx11(
1208
1165
  "div",
1209
1166
  {
1210
1167
  className: "rt-imported-html",
1211
1168
  "data-rt-import-scope": scopedCss ? scope : void 0,
1212
1169
  style: { position: "relative", isolation: "isolate" },
1213
1170
  dangerouslySetInnerHTML: {
1214
- __html: ensureImportedHtmlDocumentRoots(rawHtml, assetUrls)
1171
+ __html: withInternalLinksInHtml(
1172
+ ensureImportedHtmlDocumentRoots(rawHtml, assetUrls),
1173
+ context.internalLinks ?? null
1174
+ )
1215
1175
  }
1216
1176
  }
1217
1177
  )
1218
1178
  ] }, key);
1219
1179
  }
1220
1180
  case "columns":
1221
- return /* @__PURE__ */ jsx12("div", { className: "rt-columns", children }, key);
1181
+ return /* @__PURE__ */ jsx11("div", { className: "rt-columns", children }, key);
1222
1182
  case "column":
1223
- return /* @__PURE__ */ jsx12("div", { className: "rt-column", children }, key);
1183
+ return /* @__PURE__ */ jsx11("div", { className: "rt-column", children }, key);
1224
1184
  case "table":
1225
- 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);
1226
1186
  case "tableRow":
1227
- return /* @__PURE__ */ jsx12("tr", { children }, key);
1187
+ return /* @__PURE__ */ jsx11("tr", { children }, key);
1228
1188
  case "tableHeader": {
1229
1189
  const { colSpan, rowSpan } = tableCellSpans(node);
1230
- return /* @__PURE__ */ jsx12("th", { colSpan, rowSpan, children }, key);
1190
+ return /* @__PURE__ */ jsx11("th", { colSpan, rowSpan, children }, key);
1231
1191
  }
1232
1192
  case "tableCell": {
1233
1193
  const { colSpan, rowSpan } = tableCellSpans(node);
1234
- return /* @__PURE__ */ jsx12("td", { colSpan, rowSpan, children }, key);
1194
+ return /* @__PURE__ */ jsx11("td", { colSpan, rowSpan, children }, key);
1235
1195
  }
1236
1196
  case "embed":
1237
1197
  return renderEmbedNode(node, key);
1238
1198
  case "faq":
1239
- 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);
1240
1200
  case "faqItem":
1241
- return /* @__PURE__ */ jsx12("details", { className: "rt-cms-faq-item", children }, key);
1201
+ return /* @__PURE__ */ jsx11("details", { className: "rt-cms-faq-item", children }, key);
1242
1202
  case "faqQuestion":
1243
- return /* @__PURE__ */ jsx12("summary", { className: "rt-cms-faq-q", children }, key);
1203
+ return /* @__PURE__ */ jsx11("summary", { className: "rt-cms-faq-q", children }, key);
1244
1204
  case "faqAnswer":
1245
- return /* @__PURE__ */ jsx12("div", { className: "rt-cms-faq-a", children }, key);
1205
+ return /* @__PURE__ */ jsx11("div", { className: "rt-cms-faq-a", children }, key);
1246
1206
  case "references": {
1247
1207
  const rawTitle = node.attrs?.title;
1248
1208
  const title = typeof rawTitle === "string" && rawTitle.trim() ? rawTitle.trim() : "\uACF5\uC2DD \uCD9C\uCC98";
1249
1209
  const titleId = `rt-cms-references-${key}`;
1250
- return /* @__PURE__ */ jsxs9(
1210
+ return /* @__PURE__ */ jsxs8(
1251
1211
  "section",
1252
1212
  {
1253
1213
  "data-roottale-cms": "references",
1254
1214
  className: "rt-cms-references",
1255
1215
  "aria-labelledby": titleId,
1256
1216
  children: [
1257
- /* @__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 }),
1258
1218
  children
1259
1219
  ]
1260
1220
  },
@@ -1276,14 +1236,14 @@ function renderTiptapNode(value, key, context) {
1276
1236
  return renderSection(node, key, context.section);
1277
1237
  }
1278
1238
  case "text":
1279
- return renderTextNode(node, key);
1239
+ return renderTextNode(node, key, context.internalLinks ?? null);
1280
1240
  default:
1281
- return children ? /* @__PURE__ */ jsx12("span", { children }, key) : null;
1241
+ return children ? /* @__PURE__ */ jsx11("span", { children }, key) : null;
1282
1242
  }
1283
1243
  }
1284
1244
 
1285
1245
  // src/server/render-tiptap.tsx
1286
- import { Fragment as Fragment5, jsx as jsx13 } from "react/jsx-runtime";
1246
+ import { Fragment as Fragment5, jsx as jsx12 } from "react/jsx-runtime";
1287
1247
  function isDevelopmentRuntime() {
1288
1248
  const processValue = Reflect.get(globalThis, "process");
1289
1249
  if (!isRecord(processValue)) return true;
@@ -1296,7 +1256,7 @@ function createSectionContextWithBlogListNetworkFetch(apiKey, baseUrl) {
1296
1256
  return {
1297
1257
  cms,
1298
1258
  isDev: isDevelopmentRuntime,
1299
- renderBlogList: (props) => /* @__PURE__ */ jsx13(RootTaleBlogList, { ...props })
1259
+ renderBlogList: (props) => /* @__PURE__ */ jsx12(RootTaleBlogList, { ...props })
1300
1260
  };
1301
1261
  }
1302
1262
  function contentNodes(doc) {
@@ -1306,7 +1266,8 @@ function RenderTiptap({
1306
1266
  doc,
1307
1267
  fields,
1308
1268
  apiKey,
1309
- baseUrl
1269
+ baseUrl,
1270
+ internalLinks
1310
1271
  }) {
1311
1272
  const content = contentNodes(doc);
1312
1273
  const hasTableOfContents = content.some(
@@ -1316,15 +1277,16 @@ function RenderTiptap({
1316
1277
  const renderDoc = hasTableOfContents && entries.length > 0 ? attachHeadingIds(doc, entries) : doc;
1317
1278
  const context = {
1318
1279
  fields: fields ?? null,
1319
- section: createSectionContextWithBlogListNetworkFetch(apiKey, baseUrl)
1280
+ section: createSectionContextWithBlogListNetworkFetch(apiKey, baseUrl),
1281
+ internalLinks: internalLinks ? internalContentLinkResolverFrom(internalLinks) : null
1320
1282
  };
1321
- return /* @__PURE__ */ jsx13(Fragment5, { children: contentNodes(renderDoc).map((value, index) => {
1283
+ return /* @__PURE__ */ jsx12(Fragment5, { children: contentNodes(renderDoc).map((value, index) => {
1322
1284
  const node = parseTiptapNode(value);
1323
1285
  if (node?.type !== "tableOfContents") {
1324
1286
  return renderTiptapNode(value, index, context);
1325
1287
  }
1326
1288
  const title = node.attrs?.title;
1327
- return /* @__PURE__ */ jsx13(
1289
+ return /* @__PURE__ */ jsx12(
1328
1290
  RootTaleTableOfContents,
1329
1291
  {
1330
1292
  entries,
@@ -1335,8 +1297,152 @@ function RenderTiptap({
1335
1297
  }) });
1336
1298
  }
1337
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
+
1338
1423
  // src/server/blog-post-body.tsx
1339
- 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";
1340
1446
  function BlogPostBody(props) {
1341
1447
  const {
1342
1448
  doc,
@@ -1346,28 +1452,34 @@ function BlogPostBody(props) {
1346
1452
  toc,
1347
1453
  tocTitle,
1348
1454
  tocPosition,
1349
- bodyHtml
1455
+ bodyHtml,
1456
+ internalLinks = null
1350
1457
  } = props;
1351
- 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;
1352
1459
  const source = resolvePostBodySource(bodyHtml);
1353
- 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" ? (
1354
1465
  // source.html 은 resolvePostBodySource 가 공용 sanitizeHtml 로 정제한 값이다
1355
1466
  // (이 컴포넌트에서 한 번 더 정제하지 않는 이유 — sink 가 한 곳이어야 눈으로
1356
1467
  // 추적 가능하고, 이중 정제는 안전을 더 만들지 않으면서 비용만 든다).
1357
- /* @__PURE__ */ jsx14("div", { dangerouslySetInnerHTML: { __html: source.html } })
1358
- ) : /* @__PURE__ */ jsx14(
1468
+ /* @__PURE__ */ jsx15("div", { dangerouslySetInnerHTML: { __html: sourceHtml } })
1469
+ ) : /* @__PURE__ */ jsx15(
1359
1470
  RenderTiptap,
1360
1471
  {
1361
1472
  doc,
1362
1473
  fields,
1363
1474
  apiKey,
1364
- baseUrl
1475
+ baseUrl,
1476
+ internalLinks
1365
1477
  }
1366
1478
  ) });
1367
1479
  if (tocPosition === "sidebar" && tableOfContents) {
1368
1480
  return /* @__PURE__ */ jsxs10("div", { className: "rt-cms-layout rt-cms-layout--toc-side", children: [
1369
1481
  body,
1370
- /* @__PURE__ */ jsx14("aside", { className: "rt-cms-toc-side", children: tableOfContents })
1482
+ /* @__PURE__ */ jsx15("aside", { className: "rt-cms-toc-side", children: tableOfContents })
1371
1483
  ] });
1372
1484
  }
1373
1485
  return /* @__PURE__ */ jsxs10(Fragment6, { children: [
@@ -1384,7 +1496,7 @@ import {
1384
1496
  import {
1385
1497
  resolvePostCollection as resolvePostCollection2
1386
1498
  } from "@roottale/cms-core";
1387
- 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";
1388
1500
  function absoluteUrl(siteUrl, path) {
1389
1501
  const normalizedPath = path.startsWith("/") ? path : `/${path}`;
1390
1502
  return `${trimSlash(siteUrl)}${normalizedPath}`;
@@ -1420,10 +1532,10 @@ function renderBlogPostBreadcrumb(input) {
1420
1532
  }))
1421
1533
  ) : null;
1422
1534
  return /* @__PURE__ */ jsxs11(Fragment7, { children: [
1423
- /* @__PURE__ */ jsx15("nav", { "aria-label": "\uBE0C\uB808\uB4DC\uD06C\uB7FC", className: "rt-cms-breadcrumb", children: /* @__PURE__ */ jsx15("ol", { children: crumbs.map(
1424
- (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)
1425
1537
  ) }) }),
1426
- jsonLd ? /* @__PURE__ */ jsx15(
1538
+ jsonLd ? /* @__PURE__ */ jsx16(
1427
1539
  "script",
1428
1540
  {
1429
1541
  type: "application/ld+json",
@@ -1433,12 +1545,51 @@ function renderBlogPostBreadcrumb(input) {
1433
1545
  ] });
1434
1546
  }
1435
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
+
1436
1587
  // src/server/blog-post-footer.tsx
1437
1588
  import {
1438
1589
  jsonLdJson as jsonLdJson2
1439
1590
  } from "@roottale/cms-client/server";
1440
1591
  import { extractFaqEntries, faqPageJsonLd } from "@roottale/cms-core";
1441
- import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
1592
+ import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
1442
1593
  function renderPostCta(cta) {
1443
1594
  if (!cta) return null;
1444
1595
  const href = cta.buttonHref.trim();
@@ -1446,9 +1597,9 @@ function renderPostCta(cta) {
1446
1597
  if (!isSafeHref2) return null;
1447
1598
  const isExternal = /^https?:/i.test(href);
1448
1599
  return /* @__PURE__ */ jsxs12("aside", { "data-roottale-cms": "post-cta", className: "rt-cms-post-cta", children: [
1449
- cta.title ? /* @__PURE__ */ jsx16("p", { className: "rt-cms-post-cta__title", children: cta.title }) : null,
1450
- cta.description ? /* @__PURE__ */ jsx16("p", { className: "rt-cms-post-cta__desc", children: cta.description }) : null,
1451
- /* @__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(
1452
1603
  "a",
1453
1604
  {
1454
1605
  className: "rt-cms-post-cta__btn",
@@ -1463,9 +1614,9 @@ function renderPostCta(cta) {
1463
1614
  function renderRelatedPosts(posts, href, title) {
1464
1615
  if (posts.length === 0) return null;
1465
1616
  return /* @__PURE__ */ jsxs12("nav", { "data-roottale-cms": "related", className: "rt-cms-related", "aria-label": title, children: [
1466
- /* @__PURE__ */ jsx16("h2", { className: "rt-cms-related-title", children: title }),
1467
- /* @__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: [
1468
- 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(
1469
1620
  "img",
1470
1621
  {
1471
1622
  className: "rt-cms-related-thumb",
@@ -1474,14 +1625,14 @@ function renderRelatedPosts(posts, href, title) {
1474
1625
  loading: "lazy"
1475
1626
  }
1476
1627
  ) : null,
1477
- /* @__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 })
1478
1629
  ] }) }, post.id)) })
1479
1630
  ] });
1480
1631
  }
1481
1632
  function renderFaqJsonLd(doc) {
1482
1633
  const jsonLd = faqPageJsonLd(extractFaqEntries(doc));
1483
1634
  if (!jsonLd) return null;
1484
- return /* @__PURE__ */ jsx16(
1635
+ return /* @__PURE__ */ jsx17(
1485
1636
  "script",
1486
1637
  {
1487
1638
  type: "application/ld+json",
@@ -1496,10 +1647,10 @@ function RootTaleFaqJsonLd({
1496
1647
  }
1497
1648
 
1498
1649
  // src/server/blog-post.tsx
1499
- import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
1650
+ import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
1500
1651
  async function fetchRelatedPostsFailSoft(input) {
1501
1652
  if (input.count <= 0) return [];
1502
- const candidates = await fetchPosts2({
1653
+ const candidates = await fetchPosts3({
1503
1654
  apiKey: input.apiKey,
1504
1655
  baseUrl: input.baseUrl,
1505
1656
  type: "post",
@@ -1571,6 +1722,16 @@ async function lookupPost(input) {
1571
1722
  });
1572
1723
  return post ? { kind: "post", post } : { kind: "missing" };
1573
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
+ }
1574
1735
  async function RootTaleBlogPost(props) {
1575
1736
  const {
1576
1737
  apiKey,
@@ -1589,7 +1750,8 @@ async function RootTaleBlogPost(props) {
1589
1750
  showFieldFacts = false,
1590
1751
  theme: themeProp,
1591
1752
  authorHref = defaultAuthorHref,
1592
- breadcrumb
1753
+ breadcrumb,
1754
+ internalLinks = "auto"
1593
1755
  } = props;
1594
1756
  const [lookup, themeStyle, blogSettings] = await Promise.all([
1595
1757
  lookupPost({ apiKey, baseUrl, slugOrId, previewToken, locale }),
@@ -1597,10 +1759,10 @@ async function RootTaleBlogPost(props) {
1597
1759
  fetchBlogSettingsFailSoft({ apiKey, baseUrl })
1598
1760
  ]);
1599
1761
  if (lookup.kind === "preview-expired") {
1600
- 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") }) });
1601
1763
  }
1602
1764
  if (lookup.kind === "missing") {
1603
- 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") }) });
1604
1766
  }
1605
1767
  const post = lookup.post;
1606
1768
  const display = resolvePostDisplay2(post, blogSettings, {
@@ -1609,13 +1771,22 @@ async function RootTaleBlogPost(props) {
1609
1771
  });
1610
1772
  const toc = display.showTableOfContents ? extractToc(post.bodyJson) : [];
1611
1773
  const renderDoc = toc.length > 0 ? attachHeadingIds(post.bodyJson, toc) : post.bodyJson;
1612
- const related = await fetchRelatedPostsFailSoft({
1613
- post,
1614
- apiKey,
1615
- baseUrl,
1616
- count: relatedPostsCount,
1617
- locale
1618
- });
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
+ ]);
1619
1790
  const relatedHref = props.postHref ?? (collections ? makeCollectionPostHref(collections) : defaultPostHref);
1620
1791
  const authorName = post.authorName;
1621
1792
  const authorSlug = post.authorSlug;
@@ -1649,7 +1820,7 @@ async function RootTaleBlogPost(props) {
1649
1820
  options: breadcrumb
1650
1821
  }),
1651
1822
  /* @__PURE__ */ jsxs13("header", { className: "rt-cms-post-header", children: [
1652
- 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(
1653
1824
  "a",
1654
1825
  {
1655
1826
  className: "rt-cms-post-category",
@@ -1658,8 +1829,8 @@ async function RootTaleBlogPost(props) {
1658
1829
  },
1659
1830
  category.id
1660
1831
  )) }) : null,
1661
- showTitle ? /* @__PURE__ */ jsx17("h1", { className: "rt-cms-title", children: post.title }) : null,
1662
- 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,
1663
1834
  publishedDate || modifiedDate || showTopAuthor || views !== null || reviewedDate ? /* @__PURE__ */ jsxs13("p", { className: "rt-cms-meta", children: [
1664
1835
  publishedDate ? /* @__PURE__ */ jsxs13(
1665
1836
  "time",
@@ -1688,7 +1859,7 @@ async function RootTaleBlogPost(props) {
1688
1859
  showTopAuthor ? /* @__PURE__ */ jsxs13("span", { className: "rt-cms-meta-item", "data-rt-cms-meta": "author", children: [
1689
1860
  "\uC791\uC131\uC790",
1690
1861
  " ",
1691
- 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
1692
1863
  ] }) : null,
1693
1864
  editorialReview && reviewedDate ? /* @__PURE__ */ jsxs13("span", { className: "rt-cms-meta-item", "data-rt-cms-meta": "reviewed", children: [
1694
1865
  "\uB0B4\uC6A9 \uAC80\uD1A0 ",
@@ -1704,7 +1875,7 @@ async function RootTaleBlogPost(props) {
1704
1875
  ] }),
1705
1876
  renderArchiveRelated(post.archiveRelated, collections),
1706
1877
  showFieldFacts ? renderFieldFacts(post) : null,
1707
- /* @__PURE__ */ jsx17(
1878
+ /* @__PURE__ */ jsx18(
1708
1879
  BlogPostBody,
1709
1880
  {
1710
1881
  doc: renderDoc,
@@ -1714,11 +1885,13 @@ async function RootTaleBlogPost(props) {
1714
1885
  toc,
1715
1886
  tocTitle: display.tocTitle,
1716
1887
  tocPosition: display.tocPosition,
1717
- bodyHtml: post.bodyHtml
1888
+ bodyHtml: post.bodyHtml,
1889
+ internalLinks: internalLinkIndex
1718
1890
  }
1719
1891
  ),
1892
+ renderPostPattern(footerPattern, POST_FOOTER_PATTERN_SLOT2, { apiKey, baseUrl }),
1720
1893
  showAuthorCard && authorName ? /* @__PURE__ */ jsxs13("section", { className: "rt-cms-author-card", "aria-label": "\uC791\uC131\uC790", children: [
1721
- authorImageUrl ? /* @__PURE__ */ jsx17(
1894
+ authorImageUrl ? /* @__PURE__ */ jsx18(
1722
1895
  "img",
1723
1896
  {
1724
1897
  className: "rt-cms-author-card__image",
@@ -1727,11 +1900,11 @@ async function RootTaleBlogPost(props) {
1727
1900
  loading: "lazy",
1728
1901
  style: authorImageStyle
1729
1902
  }
1730
- ) : /* @__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) }),
1731
1904
  /* @__PURE__ */ jsxs13("div", { className: "rt-cms-author-card__body", children: [
1732
- /* @__PURE__ */ jsx17("p", { className: "rt-cms-author-card__eyebrow", children: display.authorCardEyebrow }),
1733
- /* @__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 }),
1734
- 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
1735
1908
  ] })
1736
1909
  ] }) : null,
1737
1910
  renderRelatedPosts(related, relatedHref, relatedPostsTitle),
@@ -1743,7 +1916,7 @@ async function RootTaleBlogPost(props) {
1743
1916
  }
1744
1917
 
1745
1918
  // src/server/preview-notice.tsx
1746
- import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
1919
+ import { jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
1747
1920
  function formatExpiry(locale, iso) {
1748
1921
  const date = new Date(iso);
1749
1922
  if (Number.isNaN(date.getTime())) return null;
@@ -1760,14 +1933,14 @@ function RootTalePreviewNotice({
1760
1933
  }) {
1761
1934
  const expiry = expiresAt ? formatExpiry(locale, expiresAt) : null;
1762
1935
  return /* @__PURE__ */ jsxs14("div", { "data-roottale-cms": "preview-notice", className: "rt-cms-preview-notice", role: "status", children: [
1763
- /* @__PURE__ */ jsx18("span", { children: message ?? t(locale, "previewNotice") }),
1764
- 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
1765
1938
  ] });
1766
1939
  }
1767
1940
 
1768
1941
  // src/server/page.tsx
1769
1942
  import { fetchPost as fetchPost2 } from "@roottale/cms-client/server";
1770
- import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
1943
+ import { jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
1771
1944
  async function RootTalePage(props) {
1772
1945
  const {
1773
1946
  apiKey,
@@ -1784,7 +1957,7 @@ async function RootTalePage(props) {
1784
1957
  resolveThemeStyleFailSoft({ themeProp, apiKey, baseUrl })
1785
1958
  ]);
1786
1959
  if (!post) {
1787
- 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") }) });
1788
1961
  }
1789
1962
  const pageBodySource = resolvePostBodySource(post.bodyHtml);
1790
1963
  const bodyJson = pageBodySource.kind === "json" && prepareBody ? await prepareBody(post.bodyJson) : post.bodyJson;
@@ -1795,8 +1968,8 @@ async function RootTalePage(props) {
1795
1968
  className: "rt-cms-article",
1796
1969
  style: themeStyle ?? void 0,
1797
1970
  children: [
1798
- showTitle ? /* @__PURE__ */ jsx19("header", { className: "rt-cms-post-header", children: /* @__PURE__ */ jsx19("h1", { className: "rt-cms-title", children: post.title }) }) : null,
1799
- /* @__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(
1800
1973
  RenderTiptap,
1801
1974
  {
1802
1975
  doc: bodyJson,
@@ -1812,7 +1985,7 @@ async function RootTalePage(props) {
1812
1985
 
1813
1986
  // src/server/exposure-slot.tsx
1814
1987
  import { fetchExposure } from "@roottale/cms-client/server";
1815
- import { jsx as jsx20 } from "react/jsx-runtime";
1988
+ import { jsx as jsx21 } from "react/jsx-runtime";
1816
1989
  async function RootTaleExposureSlot(props) {
1817
1990
  const campaign = await fetchExposure({
1818
1991
  apiKey: props.apiKey,
@@ -1823,7 +1996,7 @@ async function RootTaleExposureSlot(props) {
1823
1996
  revalidate: props.revalidate
1824
1997
  });
1825
1998
  if (!campaign || !props.allowedVariants.includes(campaign.content.variant)) return null;
1826
- return /* @__PURE__ */ jsx20(RootTaleExposure, { campaign, allowedVariants: props.allowedVariants });
1999
+ return /* @__PURE__ */ jsx21(RootTaleExposure, { campaign, allowedVariants: props.allowedVariants });
1827
2000
  }
1828
2001
  export {
1829
2002
  RenderTiptap,
@@ -1836,14 +2009,17 @@ export {
1836
2009
  RootTaleLeadForm,
1837
2010
  RootTalePage,
1838
2011
  RootTalePostCard,
2012
+ RootTalePostPattern,
1839
2013
  RootTalePreviewNotice,
1840
2014
  RootTaleTableOfContents,
1841
2015
  RootTaleThemeProvider,
1842
2016
  attachHeadingIds,
2017
+ bodyHasInternalContentLinkTokens,
1843
2018
  collectCategories,
1844
2019
  collectCategoriesWithCounts,
1845
2020
  defaultCategoryHref,
1846
2021
  extractToc,
2022
+ fetchInternalContentPathIndex,
1847
2023
  filterPostsByCategory,
1848
2024
  headingToId,
1849
2025
  makeCategoryHref,