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