@roottale/cms-core 0.67.1 → 0.69.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/index.js CHANGED
@@ -225,6 +225,9 @@ function validateCollections(collections) {
225
225
  }
226
226
 
227
227
  // src/category-tree.ts
228
+ function isReservedCategoryTreeSlug(presentation, slug) {
229
+ return slug === presentation.categoryFallbackSlug || presentation.reservedCategorySlugs?.includes(slug) === true;
230
+ }
228
231
  function basePathPrefix(basePath) {
229
232
  if (!basePath.trim()) return null;
230
233
  return basePath.replace(/\/+$/, "");
@@ -253,6 +256,7 @@ function hasChildCategory(termId, terms) {
253
256
  function resolveCategoryTreeChain(presentation, termId, terms) {
254
257
  const chain = categoryTreeAncestry(presentation.categoryDepth, termId, terms);
255
258
  if (!chain) return null;
259
+ if (chain.some((term) => isReservedCategoryTreeSlug(presentation, term.slug))) return null;
256
260
  if (presentation.entryCategory === "leaf") return chain;
257
261
  if (hasChildCategory(termId, terms)) return null;
258
262
  return chain.length === presentation.categoryDepth ? chain : null;
@@ -261,6 +265,33 @@ function resolveCategoryTreeSelection(presentation, termId, terms) {
261
265
  if (hasChildCategory(termId, terms)) return null;
262
266
  return resolveCategoryTreeChain(presentation, termId, terms);
263
267
  }
268
+ function resolveCategoryTreeEntryCategory(presentation, entry, terms) {
269
+ const termsById = new Map(terms.map((term) => [term.id, term]));
270
+ const first = presentation.categorySelection === "first";
271
+ const categoryIds = [...new Set(entry.terms.filter((term) => term.taxonomy === "category" && (first || termsById.has(term.id))).map((term) => term.id))];
272
+ if (first) {
273
+ if (termsById.size !== terms.length) return null;
274
+ if (categoryIds.length === 0) {
275
+ const slug = presentation.categoryFallbackSlug;
276
+ if (presentation.categoryDepth !== 1 || !slug || presentation.reservedCategorySlugs?.includes(slug) || terms.some((term) => term.slug === slug)) return null;
277
+ return { kind: "fallback", slug };
278
+ }
279
+ const candidates = [];
280
+ for (const id of categoryIds) {
281
+ const category2 = termsById.get(id);
282
+ if (!category2 || !Number.isInteger(category2.termOrder ?? 0)) return null;
283
+ const chain2 = resolveCategoryTreeChain(presentation, id, terms);
284
+ if (!chain2) return null;
285
+ candidates.push({ category: category2, chain: chain2 });
286
+ }
287
+ candidates.sort((a, b) => (a.category.termOrder ?? 0) - (b.category.termOrder ?? 0) || (a.category.id < b.category.id ? -1 : a.category.id > b.category.id ? 1 : 0));
288
+ return { kind: "category", ...candidates[0] };
289
+ }
290
+ if (categoryIds.length !== 1) return null;
291
+ const category = termsById.get(categoryIds[0]);
292
+ const chain = resolveCategoryTreeChain(presentation, category.id, terms);
293
+ return chain ? { kind: "category", category, chain } : null;
294
+ }
264
295
  function resolveCategoryTreeHubPaths(presentation, termId, terms) {
265
296
  const chain = resolveCategoryTreeChain(presentation, termId, terms);
266
297
  const prefix = basePathPrefix(presentation.basePath);
@@ -275,20 +306,11 @@ function resolveCategoryTreeHubPaths(presentation, termId, terms) {
275
306
  }
276
307
  function resolveCategoryTreeEntryPath(presentation, entry, terms) {
277
308
  if (!entry.slug) return null;
278
- const knownIds = new Set(terms.map((term) => term.id));
279
- const categoryIds = [
280
- ...new Set(
281
- entry.terms.filter((term) => term.taxonomy === "category" && knownIds.has(term.id)).map((term) => term.id)
282
- )
283
- ];
284
- if (categoryIds.length !== 1) return null;
285
- const hubs = resolveCategoryTreeHubPaths(
286
- presentation,
287
- categoryIds[0],
288
- terms
289
- );
290
- const leafPath = hubs?.at(-1);
291
- return leafPath ? `${leafPath}/${encodeURIComponent(entry.slug)}` : null;
309
+ const selected = resolveCategoryTreeEntryCategory(presentation, entry, terms);
310
+ const prefix = basePathPrefix(presentation.basePath);
311
+ if (!selected || prefix === null) return null;
312
+ const slugs = selected.kind === "fallback" ? [selected.slug] : selected.chain.map((term) => term.slug);
313
+ return `${prefix}/${[...slugs, entry.slug].map(encodeURIComponent).join("/")}`;
292
314
  }
293
315
 
294
316
  // src/public-path.ts
@@ -973,10 +995,10 @@ function scopeImportedCss(css, scope, allowedAssetUrls = []) {
973
995
 
974
996
  // src/internal-content-links.ts
975
997
  var INTERNAL_CONTENT_LINK_TOKEN_SOURCE = String.raw`\[\[internal:([a-z0-9가-힣]+(?:[.-][a-z0-9가-힣]+)*)\|([^\]\n|]+)\]\]`;
976
- function findInternalContentLinkTokens(text2) {
998
+ function findInternalContentLinkTokens(text3) {
977
999
  const pattern = new RegExp(INTERNAL_CONTENT_LINK_TOKEN_SOURCE, "giu");
978
1000
  const tokens = [];
979
- for (const match of text2.matchAll(pattern)) {
1001
+ for (const match of text3.matchAll(pattern)) {
980
1002
  const [raw, key, label] = match;
981
1003
  if (!key || !label || match.index === void 0) continue;
982
1004
  tokens.push({
@@ -1035,19 +1057,19 @@ function escapeHtmlText(value) {
1035
1057
  var INTERNAL_CONTENT_LINK_CLASS = "rt-internal-link";
1036
1058
  var INTERNAL_CONTENT_LINK_PENDING_CLASS = "rt-internal-link--pending";
1037
1059
  var INTERNAL_CONTENT_LINK_PENDING_ATTR = "data-rt-internal-link-pending";
1038
- function renderInternalContentLinksInText(text2, resolve) {
1039
- const tokens = findInternalContentLinkTokens(text2);
1040
- if (tokens.length === 0) return { html: text2, hasTokens: false };
1060
+ function renderInternalContentLinksInText(text3, resolve) {
1061
+ const tokens = findInternalContentLinkTokens(text3);
1062
+ if (tokens.length === 0) return { html: text3, hasTokens: false };
1041
1063
  let cursor = 0;
1042
1064
  let html = "";
1043
1065
  for (const token of tokens) {
1044
- html += text2.slice(cursor, token.index);
1066
+ html += text3.slice(cursor, token.index);
1045
1067
  const path = resolve(token.key);
1046
1068
  const label = escapeHtmlText(token.label);
1047
1069
  html += isSafeInternalPath(path) ? `<a href="${escapeHtmlText(path)}" class="${INTERNAL_CONTENT_LINK_CLASS}">${label}</a>` : `<span class="${INTERNAL_CONTENT_LINK_CLASS} ${INTERNAL_CONTENT_LINK_PENDING_CLASS}" ${INTERNAL_CONTENT_LINK_PENDING_ATTR}="${escapeHtmlText(token.key)}">${label}</span>`;
1048
1070
  cursor = token.index + token.raw.length;
1049
1071
  }
1050
- html += text2.slice(cursor);
1072
+ html += text3.slice(cursor);
1051
1073
  return { html, hasTokens: true };
1052
1074
  }
1053
1075
  var INTERNAL_LINK_BLOCKED_HTML_TAGS = /* @__PURE__ */ new Set([
@@ -1081,12 +1103,12 @@ function renderInternalContentLinksInHtml(html, resolve) {
1081
1103
  let cursor = 0;
1082
1104
  let out = "";
1083
1105
  let hasTokens = false;
1084
- const emitText = (text2) => {
1085
- if (blockedDepth > 0 || !text2) {
1086
- out += text2;
1106
+ const emitText = (text3) => {
1107
+ if (blockedDepth > 0 || !text3) {
1108
+ out += text3;
1087
1109
  return;
1088
1110
  }
1089
- const rendered = renderInternalContentLinksInText(text2, resolve);
1111
+ const rendered = renderInternalContentLinksInText(text3, resolve);
1090
1112
  hasTokens ||= rendered.hasTokens;
1091
1113
  out += rendered.html;
1092
1114
  };
@@ -1806,13 +1828,13 @@ function findSectionLockViolations(beforeDoc, afterDoc) {
1806
1828
  ...Object.keys(next.attrs)
1807
1829
  ]);
1808
1830
  fields.delete("props");
1809
- for (const field of fields) {
1810
- if (!valueEquals(section.attrs[field], next.attrs[field])) {
1831
+ for (const field3 of fields) {
1832
+ if (!valueEquals(section.attrs[field3], next.attrs[field3])) {
1811
1833
  violations.push({
1812
- code: field === "lock" ? "locked_section_lock_changed" : "locked_section_structure_changed",
1834
+ code: field3 === "lock" ? "locked_section_lock_changed" : "locked_section_structure_changed",
1813
1835
  sectionId: section.id,
1814
1836
  sectionType: section.sectionType,
1815
- field
1837
+ field: field3
1816
1838
  });
1817
1839
  }
1818
1840
  }
@@ -1870,11 +1892,11 @@ function imageUrl(value, base) {
1870
1892
  function resolveMetadata(input) {
1871
1893
  const layers = [input.overrides, input.defaults, input.siteDefaults, input.fallback];
1872
1894
  const result = {};
1873
- for (const field of ["title", "description", "ogImageAlt"]) {
1895
+ for (const field3 of ["title", "description", "ogImageAlt"]) {
1874
1896
  for (const layer of layers) {
1875
- const value = text(layer?.[field]);
1897
+ const value = text(layer?.[field3]);
1876
1898
  if (value !== void 0) {
1877
- result[field] = value;
1899
+ result[field3] = value;
1878
1900
  break;
1879
1901
  }
1880
1902
  }
@@ -1886,11 +1908,11 @@ function resolveMetadata(input) {
1886
1908
  break;
1887
1909
  }
1888
1910
  }
1889
- for (const field of ["noindex", "nofollow"]) {
1911
+ for (const field3 of ["noindex", "nofollow"]) {
1890
1912
  for (const layer of layers) {
1891
- const value = layer?.[field];
1913
+ const value = layer?.[field3];
1892
1914
  if (typeof value === "boolean") {
1893
- result[field] = value;
1915
+ result[field3] = value;
1894
1916
  break;
1895
1917
  }
1896
1918
  }
@@ -1937,87 +1959,154 @@ var MetadataPatchSchema = z2.object({
1937
1959
  }).strict();
1938
1960
  var MetadataRobotsPolicySchema = z2.object({
1939
1961
  forceNoindex: z2.boolean().optional(),
1940
- forceNofollow: z2.boolean().optional()
1962
+ forceNofollow: z2.boolean().optional(),
1963
+ /** Stable policy reason codes. They explain an enforced exclusion, never relax it. */
1964
+ noindexReasons: z2.array(z2.string().trim().min(1).max(200)).max(64).optional()
1941
1965
  }).strict();
1942
1966
  function readMetadataValues(input) {
1943
1967
  if (!input || typeof input !== "object" || Array.isArray(input)) return {};
1944
- const record = input;
1968
+ const record2 = input;
1945
1969
  const output = {};
1946
- for (const field of METADATA_FIELDS) {
1947
- const parsed = metadataFields[field].safeParse(record[field]);
1948
- if (parsed.success) output[field] = parsed.data;
1970
+ for (const field3 of METADATA_FIELDS) {
1971
+ const parsed = metadataFields[field3].safeParse(record2[field3]);
1972
+ if (parsed.success) output[field3] = parsed.data;
1949
1973
  }
1950
1974
  return MetadataValuesSchema.parse(output);
1951
1975
  }
1952
1976
  function mergeMetadataPatch(current, input) {
1953
1977
  const patch = MetadataPatchSchema.parse(input);
1954
1978
  const next = { ...current };
1955
- for (const field of METADATA_FIELDS) {
1956
- if (patch[field] === null) delete next[field];
1957
- else if (patch[field] !== void 0) next[field] = patch[field];
1979
+ for (const field3 of METADATA_FIELDS) {
1980
+ if (patch[field3] === null) delete next[field3];
1981
+ else if (patch[field3] !== void 0) next[field3] = patch[field3];
1958
1982
  }
1959
1983
  return next;
1960
1984
  }
1961
1985
 
1962
1986
  // src/metadata-defaults.ts
1987
+ import { z as z4 } from "zod";
1988
+
1989
+ // src/metadata-text.ts
1963
1990
  import { z as z3 } from "zod";
1964
1991
  var fallbackText = z3.string().max(2e3);
1965
- var imageOrigin = z3.string().max(2048).refine((value) => {
1966
- try {
1967
- const url = new URL(value);
1968
- return url.protocol === "https:" && url.origin === value;
1969
- } catch {
1970
- return false;
1971
- }
1972
- }, "\uC774\uBBF8\uC9C0 \uD5C8\uC6A9 \uBAA9\uB85D\uC5D0\uB294 \uACBD\uB85C \uC5C6\uB294 HTTPS origin\uB9CC \uC120\uC5B8\uD569\uB2C8\uB2E4");
1992
+ var templateToken = /\{\{(?:title|field:[a-zA-Z][a-zA-Z0-9_-]{0,159})\}\}/gu;
1993
+ var template = fallbackText.refine(
1994
+ (value) => !/[{}]/u.test(value.replace(templateToken, "")),
1995
+ "\uC790\uB3D9 \uD14D\uC2A4\uD2B8\uC5D0\uB294 {{title}} \uB610\uB294 {{field:\uD544\uB4DC\uC774\uB984}} \uD1A0\uD070\uB9CC \uC0AC\uC6A9\uD569\uB2C8\uB2E4"
1996
+ );
1973
1997
  var MetadataTextRuleSchema = z3.object({
1974
- source: z3.enum(["content", "fallback"]).optional(),
1998
+ source: z3.enum(["content", "fallback", "template"]).optional(),
1999
+ template: template.optional(),
1975
2000
  stripLeadingLabel: z3.enum(["question", "answer"]).optional(),
1976
2001
  replaceLineBreakTokens: z3.boolean().optional(),
1977
2002
  removeEmDashes: z3.boolean().optional(),
1978
2003
  normalizeWhitespace: z3.boolean().optional(),
1979
2004
  maxLength: z3.number().int().min(1).max(2e3).optional(),
2005
+ lengthUnit: z3.enum(["code-units", "code-points"]).optional(),
2006
+ wordBoundaryMinLength: z3.number().int().min(1).max(2e3).optional(),
1980
2007
  ellipsis: z3.string().max(10).optional(),
2008
+ bodyFallback: z3.object({
2009
+ preferContaining: z3.string().trim().min(1).max(100).optional(),
2010
+ stripLeadingLabel: z3.literal("conclusion").optional()
2011
+ }).strict().optional(),
1981
2012
  fallback: fallbackText.optional(),
1982
2013
  fallbackWithParent: fallbackText.optional(),
1983
2014
  fallbackWithoutParent: fallbackText.optional(),
1984
2015
  fallbackBySlug: z3.record(z3.string().max(200), fallbackText).optional()
1985
- }).strict().refine((rule) => !rule.ellipsis || rule.maxLength !== void 0 && rule.ellipsis.length < rule.maxLength, "\uB9D0\uC904\uC784\uD45C\uC5D0\uB294 \uB354 \uAE34 \uCD5C\uB300 \uAE38\uC774\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4");
1986
- var MetadataDefaultRulesSchema = z3.object({
1987
- title: MetadataTextRuleSchema.optional(),
1988
- description: MetadataTextRuleSchema.optional(),
1989
- useFirstBodyImage: z3.boolean().optional(),
1990
- useTermImage: z3.boolean().optional(),
1991
- imageAltSource: z3.enum(["content", "title"]).optional(),
1992
- legacyImageSource: z3.literal("legacy-thumbnail").optional(),
1993
- imageAllowedOrigins: z3.array(imageOrigin).min(1).max(32).optional(),
1994
- legacyImageAllowedOrigins: z3.array(imageOrigin).min(1).max(32).optional(),
1995
- normalizeCloudflareImages: z3.boolean().optional(),
1996
- fallback: MetadataValuesSchema.optional()
1997
- }).strict().refine((rule) => !rule.legacyImageSource || Boolean(rule.legacyImageAllowedOrigins?.length), "\uC774\uC804 \uC774\uBBF8\uC9C0 \uC6D0\uBCF8\uC5D0\uB294 \uD5C8\uC6A9 origin \uC120\uC5B8\uC774 \uD544\uC694\uD569\uB2C8\uB2E4");
1998
- function cleanText(value, rule) {
1999
- let text2 = value.trim();
2000
- if (rule?.stripLeadingLabel === "question") text2 = text2.replace(/^\s*Q[..:]?\s*/iu, "");
2001
- if (rule?.stripLeadingLabel === "answer") text2 = text2.replace(/^\s*A[..:]?\s*/iu, "");
2002
- if (rule?.replaceLineBreakTokens) text2 = text2.replace(/\[\[br(?::[^\]]*)?\]\]/giu, " ");
2003
- if (rule?.removeEmDashes) text2 = text2.replaceAll("\u2014", "");
2004
- if (rule?.normalizeWhitespace) text2 = text2.replace(/\s+/gu, " ");
2005
- return text2.trim();
2006
- }
2007
- function formatText(value, rule, input, name) {
2008
- let text2 = rule?.source === "fallback" ? "" : cleanText(value ?? "", rule);
2009
- if (!text2) {
2010
- const fallback = input.parentId ? rule?.fallbackWithParent ?? rule?.fallback : (input.slug ? rule?.fallbackBySlug?.[input.slug] : void 0) ?? rule?.fallbackWithoutParent ?? rule?.fallback;
2011
- text2 = cleanText((fallback ?? "").replaceAll("%s", () => name), rule);
2012
- }
2013
- if (!text2) return void 0;
2014
- if (rule?.maxLength && text2.length > rule.maxLength) {
2016
+ }).strict().refine((rule) => !rule.ellipsis || rule.maxLength !== void 0 && rule.ellipsis.length < rule.maxLength, "\uB9D0\uC904\uC784\uD45C\uC5D0\uB294 \uB354 \uAE34 \uCD5C\uB300 \uAE38\uC774\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4").refine((rule) => rule.source === "template" ? rule.template !== void 0 : rule.template === void 0, "\uD14D\uC2A4\uD2B8 template\uC740 source: template\uACFC \uD568\uAED8 \uC120\uC5B8\uD569\uB2C8\uB2E4").refine((rule) => !rule.wordBoundaryMinLength || Boolean(rule.maxLength && rule.wordBoundaryMinLength < rule.maxLength), "\uB2E8\uC5B4 \uACBD\uACC4\uC5D0\uB294 \uB354 \uAE34 \uCD5C\uB300 \uAE38\uC774\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4");
2017
+ function cleanMetadataText(value, rule) {
2018
+ let text3 = value.trim();
2019
+ if (rule?.stripLeadingLabel === "question") text3 = text3.replace(/^\s*Q[..:]?\s*/iu, "");
2020
+ if (rule?.stripLeadingLabel === "answer") text3 = text3.replace(/^\s*A[..:]?\s*/iu, "");
2021
+ if (rule?.replaceLineBreakTokens) text3 = text3.replace(/\[\[br(?::[^\]]*)?\]\]/giu, " ");
2022
+ if (rule?.removeEmDashes) text3 = text3.replaceAll("\u2014", "");
2023
+ if (rule?.normalizeWhitespace) text3 = text3.replace(/\s+/gu, " ");
2024
+ return text3.trim();
2025
+ }
2026
+ function metadataBodyParagraphs(value) {
2027
+ const paragraphs = [];
2028
+ const textParts = (node) => {
2029
+ if (Array.isArray(node)) return node.flatMap(textParts);
2030
+ if (!node || typeof node !== "object") return [];
2031
+ const item = node;
2032
+ return [...typeof item.text === "string" ? [item.text] : [], ...textParts(item.content)];
2033
+ };
2034
+ const walk = (node) => {
2035
+ if (Array.isArray(node)) {
2036
+ node.forEach(walk);
2037
+ return;
2038
+ }
2039
+ if (!node || typeof node !== "object") return;
2040
+ const item = node;
2041
+ if (item.type === "paragraph") {
2042
+ const text3 = textParts(item.content).join(" ").replace(/\s+/gu, " ").trim();
2043
+ if (text3) paragraphs.push(text3);
2044
+ } else walk(item.content);
2045
+ };
2046
+ walk(value);
2047
+ return paragraphs;
2048
+ }
2049
+ function formatMetadataText(value, rule, input, name) {
2050
+ let text3 = rule?.source === "fallback" ? "" : cleanMetadataText(value ?? "", rule);
2051
+ if (rule?.source === "template") {
2052
+ text3 = cleanMetadataText((rule.template ?? "").replace(templateToken, (token) => {
2053
+ if (token === "{{title}}") return name;
2054
+ const field3 = token.slice(8, -2);
2055
+ const value2 = Object.hasOwn(input.fields ?? {}, field3) ? input.fields?.[field3] : void 0;
2056
+ return typeof value2 === "string" ? value2.trim() : "";
2057
+ }), rule);
2058
+ }
2059
+ if (!text3 && rule?.bodyFallback) {
2060
+ const paragraphs = input.bodyParagraphs ?? [];
2061
+ const preferred = rule.bodyFallback.preferContaining;
2062
+ text3 = (preferred ? paragraphs.find((paragraph) => paragraph.includes(preferred)) : void 0) ?? paragraphs[0] ?? "";
2063
+ if (rule.bodyFallback.stripLeadingLabel === "conclusion") text3 = text3.replace(/^(먼저\s*)?결론부터\s*(말씀드리면|말씀드릴게요)[.!?]?\s*/u, "");
2064
+ text3 = cleanMetadataText(text3, rule);
2065
+ }
2066
+ if (!text3) {
2067
+ const slugFallback = input.slug && Object.hasOwn(rule?.fallbackBySlug ?? {}, input.slug) ? rule?.fallbackBySlug?.[input.slug] : void 0;
2068
+ const fallback = input.parentId ? rule?.fallbackWithParent ?? rule?.fallback : slugFallback ?? rule?.fallbackWithoutParent ?? rule?.fallback;
2069
+ text3 = cleanMetadataText((fallback ?? "").replaceAll("%s", () => name), rule);
2070
+ }
2071
+ if (!text3) return void 0;
2072
+ const units = rule?.lengthUnit === "code-points" ? [...text3] : text3.split("");
2073
+ if (rule?.maxLength && units.length > rule.maxLength) {
2015
2074
  const suffix = rule.ellipsis ?? "";
2016
- const head = text2.slice(0, rule.maxLength - suffix.length);
2075
+ const suffixLength = rule.lengthUnit === "code-points" ? [...suffix].length : suffix.length;
2076
+ let head = units.slice(0, rule.maxLength - suffixLength).join("");
2077
+ const boundary = head.lastIndexOf(" ");
2078
+ if (rule.wordBoundaryMinLength && boundary >= rule.wordBoundaryMinLength) head = head.slice(0, boundary);
2017
2079
  return suffix ? head.trimEnd() + suffix : head;
2018
2080
  }
2019
- return text2;
2081
+ return text3;
2020
2082
  }
2083
+
2084
+ // src/metadata-defaults.ts
2085
+ var imageOrigin = z4.string().max(2048).refine((value) => {
2086
+ try {
2087
+ const url = new URL(value);
2088
+ return url.protocol === "https:" && url.origin === value;
2089
+ } catch {
2090
+ return false;
2091
+ }
2092
+ }, "\uC774\uBBF8\uC9C0 \uD5C8\uC6A9 \uBAA9\uB85D\uC5D0\uB294 \uACBD\uB85C \uC5C6\uB294 HTTPS origin\uB9CC \uC120\uC5B8\uD569\uB2C8\uB2E4");
2093
+ var MetadataDefaultRulesSchema = z4.object({
2094
+ title: MetadataTextRuleSchema.optional(),
2095
+ titleTemplateMaxLength: z4.number().int().min(1).max(1e3).optional(),
2096
+ description: MetadataTextRuleSchema.optional(),
2097
+ /** Code-owned defaults precede site defaults but always follow manual overrides. */
2098
+ defaults: MetadataValuesSchema.optional(),
2099
+ robots: MetadataRobotsPolicySchema.optional(),
2100
+ useFeaturedImage: z4.boolean().optional(),
2101
+ useFirstBodyImage: z4.boolean().optional(),
2102
+ useTermImage: z4.boolean().optional(),
2103
+ imageAltSource: z4.enum(["content", "title"]).optional(),
2104
+ legacyImageSource: z4.literal("legacy-thumbnail").optional(),
2105
+ imageAllowedOrigins: z4.array(imageOrigin).min(1).max(32).optional(),
2106
+ legacyImageAllowedOrigins: z4.array(imageOrigin).min(1).max(32).optional(),
2107
+ normalizeCloudflareImages: z4.boolean().optional(),
2108
+ fallback: MetadataValuesSchema.optional()
2109
+ }).strict().refine((rule) => !rule.legacyImageSource || Boolean(rule.legacyImageAllowedOrigins?.length), "\uC774\uC804 \uC774\uBBF8\uC9C0 \uC6D0\uBCF8\uC5D0\uB294 \uD5C8\uC6A9 origin \uC120\uC5B8\uC774 \uD544\uC694\uD569\uB2C8\uB2E4");
2021
2110
  function metadataContentImageUrl(value, normalizeCloudflareImages = false) {
2022
2111
  if (!normalizeCloudflareImages) return value;
2023
2112
  try {
@@ -2057,20 +2146,328 @@ function contentCandidateAllowed(value, input, rule) {
2057
2146
  function formatMetadataDefaults(input, declaration) {
2058
2147
  const dynamic = declaration[input.kind];
2059
2148
  const rule = dynamic?.defaultRules?.[input.modelKey];
2060
- const name = cleanText(input.title, rule?.title);
2061
- const title = formatText(input.title, rule?.title, input, name);
2062
- const template = dynamic?.titleTemplates?.[input.modelKey];
2063
- const description = formatText(input.description, rule?.description, input, name);
2149
+ const name = cleanMetadataText(input.title, rule?.title);
2150
+ const title = formatMetadataText(input.title, rule?.title, input, name);
2151
+ const template2 = dynamic?.titleTemplates?.[input.modelKey];
2152
+ const templatedTitle = title && template2 ? template2.replace("%s", () => title) : title;
2153
+ const finalTitle = templatedTitle && rule?.titleTemplateMaxLength && templatedTitle.length > rule.titleTemplateMaxLength ? title : templatedTitle;
2154
+ const description = formatMetadataText(input.description, rule?.description, input, name);
2064
2155
  const bodyImage = input.firstBodyImage && (input.firstBodyImage.startsWith("https://") || /^\/(?!\/)/u.test(input.firstBodyImage)) ? input.firstBodyImage : void 0;
2065
- const candidateImage = input.kind === "terms" && rule?.useTermImage === false ? void 0 : input.featuredImage || (rule?.useFirstBodyImage ? bodyImage : void 0) || legacyImageUrl(input.legacyImage, rule);
2156
+ const featuredImage = (input.kind === "terms" ? rule?.useTermImage : rule?.useFeaturedImage) === false ? void 0 : input.featuredImage;
2157
+ const candidateImage = featuredImage || (rule?.useFirstBodyImage ? bodyImage : void 0) || legacyImageUrl(input.legacyImage, rule);
2066
2158
  const ogImage = candidateImage && contentCandidateAllowed(candidateImage, input, rule) ? readMetadataValues({ ogImage: metadataContentImageUrl(candidateImage, rule?.normalizeCloudflareImages) }).ogImage : void 0;
2067
2159
  return {
2068
- ...title ? { title: template ? template.replace("%s", () => title) : title } : {},
2160
+ ...readMetadataValues(rule?.defaults),
2161
+ ...finalTitle ? { title: finalTitle } : {},
2069
2162
  ...description ? { description } : {},
2070
2163
  ...ogImage ? { ogImage } : {}
2071
2164
  };
2072
2165
  }
2073
2166
 
2167
+ // src/post-body-source.ts
2168
+ function resolvePostBodySource(bodyHtml) {
2169
+ if (typeof bodyHtml !== "string") return { kind: "json" };
2170
+ const trimmed = bodyHtml.trim();
2171
+ if (trimmed.length === 0) return { kind: "json" };
2172
+ const sanitized = sanitizeHtml(trimmed);
2173
+ const visible = /<(?:img|hr|br|iframe|video|audio)\b/i.test(sanitized) || sanitized.replace(/<[^>]*>/g, "").replace(/&nbsp;/gi, " ").trim().length > 0;
2174
+ return visible ? { kind: "html", html: sanitized } : { kind: "json" };
2175
+ }
2176
+
2177
+ // src/content-policy-schema.ts
2178
+ import { z as z5 } from "zod";
2179
+ var ContentPolicyFieldNameSchema = z5.string().min(1).max(64).regex(/^[A-Za-z_][A-Za-z0-9_]*$/).refine((name) => name !== "prototype" && !Object.hasOwn(Object.prototype, name));
2180
+ var ContentPolicyScalarSchema = z5.union([
2181
+ z5.string().max(2048),
2182
+ z5.number().finite(),
2183
+ z5.boolean(),
2184
+ z5.null()
2185
+ ]);
2186
+ var ContentPolicyWhenSchema = z5.object({
2187
+ field: ContentPolicyFieldNameSchema,
2188
+ equals: ContentPolicyScalarSchema
2189
+ }).strict();
2190
+ var ContentPolicyInternalPathSchema = z5.string().min(1).max(2048).refine((path) => {
2191
+ if (!path.startsWith("/") || path.startsWith("//") || /[\\?#\u0000-\u0020\u007f]/u.test(path)) return false;
2192
+ try {
2193
+ return path.split("/").every((part, index) => {
2194
+ if (index === 0) return true;
2195
+ const decoded2 = decodeURIComponent(part);
2196
+ return decoded2 !== "." && decoded2 !== ".." && !/[\\/\u0000-\u0020\u007f]/u.test(decoded2);
2197
+ });
2198
+ } catch {
2199
+ return false;
2200
+ }
2201
+ }, "\uCF58\uD150\uCE20 \uACBD\uB85C\uB294 \uC548\uC804\uD55C \uC0AC\uC774\uD2B8 \uB0B4\uBD80 \uACBD\uB85C\uC5EC\uC57C \uD569\uB2C8\uB2E4");
2202
+ var field = { field: ContentPolicyFieldNameSchema };
2203
+ var scalarChecks = [
2204
+ z5.object({ ...field, op: z5.literal("equals"), value: ContentPolicyScalarSchema }).strict(),
2205
+ z5.object({ ...field, op: z5.literal("notEquals"), value: ContentPolicyScalarSchema }).strict(),
2206
+ z5.object({ ...field, op: z5.literal("oneOf"), values: z5.array(ContentPolicyScalarSchema).min(1).max(16), allowEmpty: z5.boolean().optional() }).strict(),
2207
+ z5.object({ ...field, op: z5.literal("text") }).strict(),
2208
+ z5.object({ ...field, op: z5.literal("date") }).strict(),
2209
+ z5.object({ ...field, op: z5.literal("safeHttps") }).strict(),
2210
+ z5.object({ ...field, op: z5.literal("allowedPath"), paths: z5.array(ContentPolicyInternalPathSchema).min(1).max(512) }).strict()
2211
+ ];
2212
+ var ContentPolicyScalarCheckSchema = z5.discriminatedUnion("op", scalarChecks);
2213
+ var when = { when: ContentPolicyWhenSchema.optional() };
2214
+ var ContentPolicyCheckSchema = z5.discriminatedUnion("op", [
2215
+ scalarChecks[0].extend(when),
2216
+ scalarChecks[1].extend(when),
2217
+ scalarChecks[2].extend(when),
2218
+ scalarChecks[3].extend(when),
2219
+ scalarChecks[4].extend(when),
2220
+ scalarChecks[5].extend(when),
2221
+ scalarChecks[6].extend(when),
2222
+ z5.object({ ...field, ...when, op: z5.literal("some"), checks: z5.array(ContentPolicyScalarCheckSchema).min(1).max(8) }).strict(),
2223
+ z5.object({
2224
+ ...when,
2225
+ op: z5.literal("bodyContent"),
2226
+ allowExcerpt: z5.boolean().optional(),
2227
+ allowFeaturedImage: z5.boolean().optional(),
2228
+ additionalUrl: z5.object({ ...field, ...when }).strict().optional()
2229
+ }).strict(),
2230
+ z5.object({ ...when, op: z5.literal("bodyText") }).strict()
2231
+ ]);
2232
+ var contentPolicy = z5.object({
2233
+ visibility: z5.array(ContentPolicyCheckSchema).max(24),
2234
+ indexing: z5.array(ContentPolicyCheckSchema).max(24),
2235
+ pathSegmentFormat: z5.literal("lower-kebab").optional()
2236
+ }).strict();
2237
+ var ContentPolicySchema = contentPolicy.superRefine((policy, ctx) => {
2238
+ const refs = collectContentPolicyFieldRefs(policy);
2239
+ if (refs.length > 64) ctx.addIssue({ code: z5.ZodIssueCode.custom, message: "\uCF58\uD150\uCE20 \uC815\uCC45\uC740 \uACF5\uAC1C \uD544\uB4DC 64\uAC1C\uAE4C\uC9C0 \uCC38\uC870\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4" });
2240
+ for (const ref of refs) {
2241
+ if ((ref.children?.length ?? 0) > 8) ctx.addIssue({ code: z5.ZodIssueCode.custom, message: `\uBC18\uBCF5 \uD544\uB4DC ${ref.name}\uC758 \uC790\uC2DD \uCC38\uC870\uB294 8\uAC1C\uAE4C\uC9C0 \uD5C8\uC6A9\uD569\uB2C8\uB2E4` });
2242
+ }
2243
+ });
2244
+ function collectContentPolicyFieldRefs(policy) {
2245
+ const refs = /* @__PURE__ */ new Map();
2246
+ const add = (name, children = []) => {
2247
+ const names = refs.get(name) ?? /* @__PURE__ */ new Set();
2248
+ for (const child of children) names.add(child);
2249
+ refs.set(name, names);
2250
+ };
2251
+ for (const check of [...policy.visibility, ...policy.indexing]) {
2252
+ if (check.when) add(check.when.field);
2253
+ if ("field" in check) add(check.field, check.op === "some" ? check.checks.map((child) => child.field) : []);
2254
+ if (check.op === "bodyContent" && check.additionalUrl) {
2255
+ add(check.additionalUrl.field);
2256
+ if (check.additionalUrl.when) add(check.additionalUrl.when.field);
2257
+ }
2258
+ }
2259
+ return [...refs].map(([name, children]) => ({ name, ...children.size ? { children: [...children] } : {} }));
2260
+ }
2261
+
2262
+ // src/content-policy.ts
2263
+ var text2 = (value) => typeof value === "string" ? value.trim() : "";
2264
+ var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2265
+ function fieldValue(fields, field3) {
2266
+ return Object.hasOwn(fields, field3) ? fields[field3] : void 0;
2267
+ }
2268
+ function contentPolicySafeHttpsUrl(value) {
2269
+ if (typeof value !== "string" || /[\\\u0000-\u0020\u007f]/u.test(value)) return null;
2270
+ try {
2271
+ const url = new URL(value);
2272
+ return url.protocol === "https:" && !url.username && !url.password ? url.href : null;
2273
+ } catch {
2274
+ return null;
2275
+ }
2276
+ }
2277
+ function dateValue(value) {
2278
+ if (!value || typeof value !== "string" && typeof value !== "number") return null;
2279
+ if (!Number.isFinite(Date.parse(String(value)))) return null;
2280
+ const date = new Date(value).getTime();
2281
+ return Number.isFinite(date) ? date : null;
2282
+ }
2283
+ function contentPolicyBodyText(bodyJson) {
2284
+ const root = record(bodyJson);
2285
+ if (root?.type !== "doc" || !Array.isArray(root.content)) return "";
2286
+ const pending = [{ node: root, depth: 0 }];
2287
+ const output = [];
2288
+ let count = 0;
2289
+ while (pending.length) {
2290
+ const entry = pending.pop();
2291
+ if (++count > 1e5 || entry.depth > 128) throw new Error("Content policy body exceeds traversal limits");
2292
+ const node = record(entry.node);
2293
+ if (!node) continue;
2294
+ if (node.type === "text") {
2295
+ const value = text2(node.text);
2296
+ if (value) output.push(value);
2297
+ } else if (Array.isArray(node.content)) {
2298
+ for (let index = node.content.length - 1; index >= 0; index--) {
2299
+ pending.push({ node: node.content[index], depth: entry.depth + 1 });
2300
+ }
2301
+ }
2302
+ }
2303
+ return output.join(" ");
2304
+ }
2305
+ function matchesContentScalarCheck(check, fields) {
2306
+ const value = fieldValue(fields, check.field);
2307
+ switch (check.op) {
2308
+ case "equals":
2309
+ return value === check.value;
2310
+ case "notEquals":
2311
+ return value !== check.value;
2312
+ case "oneOf":
2313
+ return check.allowEmpty === true && !value || check.values.some((candidate) => candidate === value);
2314
+ case "text":
2315
+ return text2(value).length > 0;
2316
+ case "date":
2317
+ return dateValue(value) !== null;
2318
+ case "safeHttps":
2319
+ return contentPolicySafeHttpsUrl(value) !== null;
2320
+ case "allowedPath":
2321
+ return check.paths.includes(text2(value));
2322
+ }
2323
+ }
2324
+ function matchesWhen(when2, fields) {
2325
+ return !when2 || fieldValue(fields, when2.field) === when2.equals;
2326
+ }
2327
+ function bodyFacts(post) {
2328
+ const jsonText = contentPolicyBodyText(post.bodyJson);
2329
+ const source = resolvePostBodySource(post.bodyHtml);
2330
+ const html = source.kind === "html" ? source.html : "";
2331
+ return { jsonText, html, text: source.kind === "html" ? html.replace(/<[^>]*>/g, "").replace(/&nbsp;/gi, " ").trim() : jsonText };
2332
+ }
2333
+ function matchesCheck(check, post, fields, getBody) {
2334
+ if (!matchesWhen(check.when, fields)) return true;
2335
+ if (check.op === "some") {
2336
+ const value = fieldValue(fields, check.field);
2337
+ return Array.isArray(value) && value.some((item) => {
2338
+ const child = record(item);
2339
+ return child !== null && check.checks.every((scalar) => matchesContentScalarCheck(scalar, child));
2340
+ });
2341
+ }
2342
+ if (check.op === "bodyText") return Boolean(getBody().text);
2343
+ if (check.op === "bodyContent") {
2344
+ const body = getBody();
2345
+ return Boolean(body.jsonText || body.html || check.allowExcerpt && text2(post.excerpt) || check.allowFeaturedImage && contentPolicySafeHttpsUrl(post.featuredImageUrl) || check.additionalUrl && matchesWhen(check.additionalUrl.when, fields) && contentPolicySafeHttpsUrl(fieldValue(fields, check.additionalUrl.field)));
2346
+ }
2347
+ return matchesContentScalarCheck(check, fields);
2348
+ }
2349
+ function validPath(policy, post, options) {
2350
+ const path = text2(post.path).replace(/\/+$/, "");
2351
+ if (!path || !ContentPolicyInternalPathSchema.safeParse(path).success) return false;
2352
+ const segments = path.slice(1).split("/");
2353
+ if (segments.some((segment) => !segment)) return false;
2354
+ if (policy.pathSegmentFormat === "lower-kebab" && segments.some((segment) => !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(segment))) return false;
2355
+ if (decodeURIComponent(segments.at(-1)) !== post.slug) return false;
2356
+ if (options.expectedPath !== void 0) {
2357
+ return options.expectedPath !== null && path === options.expectedPath.replace(/\/+$/, "");
2358
+ }
2359
+ return true;
2360
+ }
2361
+ function evaluateContentPolicy(policy, post, options = {}) {
2362
+ const visibility = [];
2363
+ if (post.status !== "published") visibility.push("content:not-published");
2364
+ if (!text2(post.id) || !text2(post.title) || !text2(post.slug)) visibility.push("content:invalid-identity");
2365
+ const publishedAt = dateValue(post.publishedAt);
2366
+ if (publishedAt === null) visibility.push("content:invalid-published-at");
2367
+ else if (publishedAt > (options.now ?? Date.now())) visibility.push("content:future-publication");
2368
+ if (!validPath(policy, post, options)) visibility.push("content:invalid-path");
2369
+ const fields = post.fields ?? {};
2370
+ let body;
2371
+ const getBody = () => body ??= bodyFacts(post);
2372
+ const failures = (checks, stage) => checks.flatMap((check) => matchesCheck(check, post, fields, getBody) ? [] : [`content:${stage}:${check.op}${"field" in check ? `:${check.field}` : ""}`]);
2373
+ if (visibility.length === 0) visibility.push(...failures(policy.visibility, "visibility"));
2374
+ const visible = visibility.length === 0;
2375
+ const indexing = visible ? failures(policy.indexing, "indexing") : [];
2376
+ const forced = !visible || indexing.length > 0 || options.robots?.forceNoindex === true;
2377
+ const inheritedReasons = options.robots?.forceNoindex ? options.robots.noindexReasons?.length ? options.robots.noindexReasons : ["content:forced-noindex"] : [];
2378
+ const noindexReasons = [.../* @__PURE__ */ new Set([...visibility, ...indexing, ...inheritedReasons])].slice(0, 64);
2379
+ const manualNoindex = readMetadataValues(post.metaJson?.seo).noindex === true;
2380
+ return {
2381
+ visible,
2382
+ indexable: visible && !forced && !manualNoindex,
2383
+ robots: {
2384
+ ...options.robots,
2385
+ ...forced ? { forceNoindex: true } : {},
2386
+ ...noindexReasons.length ? { noindexReasons } : {}
2387
+ },
2388
+ reasons: [...noindexReasons, ...manualNoindex ? ["content:manual-noindex"] : []]
2389
+ };
2390
+ }
2391
+
2392
+ // src/content-query.ts
2393
+ import { z as z6 } from "zod";
2394
+ var field2 = { field: ContentPolicyFieldNameSchema };
2395
+ var ContentQueryFilterSchema = z6.discriminatedUnion("op", [
2396
+ z6.object({ ...field2, op: z6.literal("equals"), value: ContentPolicyScalarSchema }).strict(),
2397
+ z6.object({ ...field2, op: z6.literal("notEquals"), value: ContentPolicyScalarSchema }).strict(),
2398
+ z6.object({ ...field2, op: z6.literal("oneOf"), values: z6.array(ContentPolicyScalarSchema).min(1).max(16), allowEmpty: z6.boolean().optional() }).strict()
2399
+ ]);
2400
+ var ContentQuerySchema = z6.object({
2401
+ modelKeys: z6.array(z6.string().min(1).max(100).regex(/^[a-z][a-z0-9_-]*$/)).min(1).max(16),
2402
+ filters: z6.array(ContentQueryFilterSchema).max(8).optional(),
2403
+ scope: z6.enum(["currentTerm", "currentPath"]).optional(),
2404
+ pinnedField: ContentPolicyFieldNameSchema.optional(),
2405
+ pageSize: z6.number().int().min(1).max(100),
2406
+ requireItems: z6.boolean().optional()
2407
+ }).strict();
2408
+ function collectContentQueryFieldRefs(query) {
2409
+ return [.../* @__PURE__ */ new Set([...(query.filters ?? []).map((filter) => filter.field), ...query.pinnedField ? [query.pinnedField] : []])].map((name) => ({ name }));
2410
+ }
2411
+ function matchesScope(query, post, options) {
2412
+ if (query.scope === "currentTerm") return post.terms?.some((term) => term.id === options.termId) === true;
2413
+ if (query.scope === "currentPath") {
2414
+ const path = options.path.replace(/\/+$/, "");
2415
+ return post.path?.trim().startsWith(`${path}/`) === true;
2416
+ }
2417
+ return true;
2418
+ }
2419
+ function evaluateContentQuery(query, candidates, options) {
2420
+ if (!options.complete) throw new Error("Content query input is incomplete");
2421
+ const page = options.page ?? 1;
2422
+ if (!Number.isSafeInteger(page) || page < 1) throw new Error("Content query page must be a positive safe integer");
2423
+ if (query.scope === "currentTerm" && !options.termId) throw new Error("Content query requires the current term id");
2424
+ if (query.scope === "currentPath" && (!options.path || !ContentPolicyInternalPathSchema.safeParse(options.path).success)) throw new Error("Content query requires a safe current path");
2425
+ const visible = candidates.filter(({ policy }) => policy.visible);
2426
+ const pathCounts = /* @__PURE__ */ new Map();
2427
+ const canonical = (post) => post.path?.trim().replace(/\/+$/, "") ?? null;
2428
+ for (const { post } of visible) pathCounts.set(canonical(post), (pathCounts.get(canonical(post)) ?? 0) + 1);
2429
+ const seen = /* @__PURE__ */ new Set();
2430
+ const selected = visible.filter(({ post }) => {
2431
+ if (pathCounts.get(canonical(post)) !== 1 || seen.has(post.id)) return false;
2432
+ seen.add(post.id);
2433
+ return query.modelKeys.includes(post.modelKey ?? post.collectionKey ?? "") && matchesScope(query, post, options) && (query.filters ?? []).every((filter) => matchesContentScalarCheck(filter, post.fields ?? {}));
2434
+ }).sort((left, right) => {
2435
+ if (query.pinnedField) {
2436
+ const delta = Number(right.post.fields?.[query.pinnedField] === true) - Number(left.post.fields?.[query.pinnedField] === true);
2437
+ if (delta) return delta;
2438
+ }
2439
+ return Date.parse(right.post.publishedAt) - Date.parse(left.post.publishedAt) || left.post.id.localeCompare(right.post.id);
2440
+ });
2441
+ const pageCount = Math.max(1, Math.ceil(selected.length / query.pageSize));
2442
+ const current = selected.slice((page - 1) * query.pageSize, page * query.pageSize);
2443
+ const pageVisible = page <= pageCount && (!query.requireItems || selected.length > 0);
2444
+ const hasIndexable = current.some(({ policy }) => policy.indexable);
2445
+ const forced = !hasIndexable || options.robots?.forceNoindex === true;
2446
+ const reasons = [.../* @__PURE__ */ new Set([
2447
+ ...page > pageCount ? ["content:invalid-page"] : query.requireItems && !selected.length ? ["content:empty-required-list"] : !current.length ? ["content:empty-page"] : !hasIndexable ? ["content:no-indexable-items"] : [],
2448
+ ...options.robots?.forceNoindex ? options.robots.noindexReasons?.length ? options.robots.noindexReasons : ["content:forced-noindex"] : []
2449
+ ])].slice(0, 64);
2450
+ return {
2451
+ items: current.map(({ post }) => post),
2452
+ total: selected.length,
2453
+ page,
2454
+ pageCount,
2455
+ visible: pageVisible,
2456
+ indexable: pageVisible && !forced,
2457
+ robots: { ...options.robots, ...forced ? { forceNoindex: true } : {}, ...reasons.length ? { noindexReasons: reasons } : {} },
2458
+ reasons
2459
+ };
2460
+ }
2461
+ function selectContentQuery(query, posts, policies, options) {
2462
+ if (!options.complete) throw new Error("Content query input is incomplete");
2463
+ const candidates = posts.filter((post) => query.modelKeys.includes(post.modelKey ?? post.collectionKey ?? "")).map((post) => {
2464
+ const modelKey = post.modelKey ?? post.collectionKey ?? "";
2465
+ if (!Object.hasOwn(policies, modelKey)) throw new Error(`Content query has no policy for model ${modelKey}`);
2466
+ return { post, policy: evaluateContentPolicy(policies[modelKey], post, { now: options.now, ...options.postOptions(post) }) };
2467
+ });
2468
+ return evaluateContentQuery(query, candidates, options);
2469
+ }
2470
+
2074
2471
  // src/redirect-paths.ts
2075
2472
  var CONTROL_OR_BACKSLASH = /[\\\u0000-\u001f\u007f]/u;
2076
2473
  var PROTECTED_PREFIX = /^\/(?:api|_next|_vercel|auth|admin|login|logout|revalidate|preview|assets|static|images|fonts|\.well-known)(?:\/|$)/i;
@@ -2137,6 +2534,15 @@ export {
2137
2534
  CMS_IMAGE_WIDTHS,
2138
2535
  CMS_LINE_HEIGHTS,
2139
2536
  CMS_PARAGRAPH_SPACINGS,
2537
+ ContentPolicyCheckSchema,
2538
+ ContentPolicyFieldNameSchema,
2539
+ ContentPolicyInternalPathSchema,
2540
+ ContentPolicyScalarCheckSchema,
2541
+ ContentPolicyScalarSchema,
2542
+ ContentPolicySchema,
2543
+ ContentPolicyWhenSchema,
2544
+ ContentQueryFilterSchema,
2545
+ ContentQuerySchema,
2140
2546
  DEFAULT_SITE_PATTERN_PRESENTATION,
2141
2547
  EMPTY_SITE_PATTERN_PLACEMENTS,
2142
2548
  FIELD_BINDING_SOURCE,
@@ -2174,10 +2580,16 @@ export {
2174
2580
  cmsImageWidth,
2175
2581
  cmsLineHeight,
2176
2582
  cmsParagraphSpacing,
2583
+ collectContentPolicyFieldRefs,
2584
+ collectContentQueryFieldRefs,
2585
+ contentPolicyBodyText,
2586
+ contentPolicySafeHttpsUrl,
2177
2587
  createFieldBindingResolver,
2178
2588
  emptyPatternSlots,
2179
2589
  ensureImportedHtmlDocumentRoots,
2180
2590
  ensureStableIds,
2591
+ evaluateContentPolicy,
2592
+ evaluateContentQuery,
2181
2593
  extractFaqEntries,
2182
2594
  faqPageJsonLd,
2183
2595
  findInternalContentLinkTokens,
@@ -2194,12 +2606,15 @@ export {
2194
2606
  isAllowedMapEmbedSrc,
2195
2607
  isInCollectionMode,
2196
2608
  isProtectedRedirectPath,
2609
+ isReservedCategoryTreeSlug,
2197
2610
  isSectionContentPropPath,
2198
2611
  isSitePatternSlotKey,
2199
2612
  isValidBlock,
2200
2613
  isValidSitePatternKey,
2201
2614
  issuePreviewToken,
2615
+ matchesContentScalarCheck,
2202
2616
  mergeMetadataPatch,
2617
+ metadataBodyParagraphs,
2203
2618
  metadataContentImageUrl,
2204
2619
  normalizeEmbed,
2205
2620
  normalizeHexColor,
@@ -2219,11 +2634,13 @@ export {
2219
2634
  renderInternalContentLinksInText,
2220
2635
  resolveCategoryPath,
2221
2636
  resolveCategoryTreeChain,
2637
+ resolveCategoryTreeEntryCategory,
2222
2638
  resolveCategoryTreeEntryPath,
2223
2639
  resolveCategoryTreeHubPaths,
2224
2640
  resolveCategoryTreeSelection,
2225
2641
  resolveMetadata,
2226
2642
  resolvePatternSlots,
2643
+ resolvePostBodySource,
2227
2644
  resolvePostCategorySlug,
2228
2645
  resolvePostCollection,
2229
2646
  resolvePostPath,
@@ -2233,6 +2650,7 @@ export {
2233
2650
  sanitizeImportedCss,
2234
2651
  sanitizeImportedHtml,
2235
2652
  scopeImportedCss,
2653
+ selectContentQuery,
2236
2654
  serializeBlock,
2237
2655
  setSitePatternPlacementRule,
2238
2656
  sitePatternButtonColor,