@roottale/cms-core 0.68.0 → 0.70.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,30 +1959,32 @@ 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
1963
- import { z as z4 } from "zod";
1987
+ import { z as z6 } from "zod";
1964
1988
 
1965
1989
  // src/metadata-text.ts
1966
1990
  import { z as z3 } from "zod";
@@ -1991,13 +2015,13 @@ var MetadataTextRuleSchema = z3.object({
1991
2015
  fallbackBySlug: z3.record(z3.string().max(200), fallbackText).optional()
1992
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");
1993
2017
  function cleanMetadataText(value, rule) {
1994
- let text2 = value.trim();
1995
- if (rule?.stripLeadingLabel === "question") text2 = text2.replace(/^\s*Q[..:]?\s*/iu, "");
1996
- if (rule?.stripLeadingLabel === "answer") text2 = text2.replace(/^\s*A[..:]?\s*/iu, "");
1997
- if (rule?.replaceLineBreakTokens) text2 = text2.replace(/\[\[br(?::[^\]]*)?\]\]/giu, " ");
1998
- if (rule?.removeEmDashes) text2 = text2.replaceAll("\u2014", "");
1999
- if (rule?.normalizeWhitespace) text2 = text2.replace(/\s+/gu, " ");
2000
- return text2.trim();
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();
2001
2025
  }
2002
2026
  function metadataBodyParagraphs(value) {
2003
2027
  const paragraphs = [];
@@ -2015,37 +2039,37 @@ function metadataBodyParagraphs(value) {
2015
2039
  if (!node || typeof node !== "object") return;
2016
2040
  const item = node;
2017
2041
  if (item.type === "paragraph") {
2018
- const text2 = textParts(item.content).join(" ").replace(/\s+/gu, " ").trim();
2019
- if (text2) paragraphs.push(text2);
2042
+ const text3 = textParts(item.content).join(" ").replace(/\s+/gu, " ").trim();
2043
+ if (text3) paragraphs.push(text3);
2020
2044
  } else walk(item.content);
2021
2045
  };
2022
2046
  walk(value);
2023
2047
  return paragraphs;
2024
2048
  }
2025
2049
  function formatMetadataText(value, rule, input, name) {
2026
- let text2 = rule?.source === "fallback" ? "" : cleanMetadataText(value ?? "", rule);
2050
+ let text3 = rule?.source === "fallback" ? "" : cleanMetadataText(value ?? "", rule);
2027
2051
  if (rule?.source === "template") {
2028
- text2 = cleanMetadataText((rule.template ?? "").replace(templateToken, (token) => {
2052
+ text3 = cleanMetadataText((rule.template ?? "").replace(templateToken, (token) => {
2029
2053
  if (token === "{{title}}") return name;
2030
- const field = token.slice(8, -2);
2031
- const value2 = Object.hasOwn(input.fields ?? {}, field) ? input.fields?.[field] : void 0;
2054
+ const field3 = token.slice(8, -2);
2055
+ const value2 = Object.hasOwn(input.fields ?? {}, field3) ? input.fields?.[field3] : void 0;
2032
2056
  return typeof value2 === "string" ? value2.trim() : "";
2033
2057
  }), rule);
2034
2058
  }
2035
- if (!text2 && rule?.bodyFallback) {
2059
+ if (!text3 && rule?.bodyFallback) {
2036
2060
  const paragraphs = input.bodyParagraphs ?? [];
2037
2061
  const preferred = rule.bodyFallback.preferContaining;
2038
- text2 = (preferred ? paragraphs.find((paragraph) => paragraph.includes(preferred)) : void 0) ?? paragraphs[0] ?? "";
2039
- if (rule.bodyFallback.stripLeadingLabel === "conclusion") text2 = text2.replace(/^(먼저\s*)?결론부터\s*(말씀드리면|말씀드릴게요)[.!?]?\s*/u, "");
2040
- text2 = cleanMetadataText(text2, rule);
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);
2041
2065
  }
2042
- if (!text2) {
2066
+ if (!text3) {
2043
2067
  const slugFallback = input.slug && Object.hasOwn(rule?.fallbackBySlug ?? {}, input.slug) ? rule?.fallbackBySlug?.[input.slug] : void 0;
2044
2068
  const fallback = input.parentId ? rule?.fallbackWithParent ?? rule?.fallback : slugFallback ?? rule?.fallbackWithoutParent ?? rule?.fallback;
2045
- text2 = cleanMetadataText((fallback ?? "").replaceAll("%s", () => name), rule);
2069
+ text3 = cleanMetadataText((fallback ?? "").replaceAll("%s", () => name), rule);
2046
2070
  }
2047
- if (!text2) return void 0;
2048
- const units = rule?.lengthUnit === "code-points" ? [...text2] : text2.split("");
2071
+ if (!text3) return void 0;
2072
+ const units = rule?.lengthUnit === "code-points" ? [...text3] : text3.split("");
2049
2073
  if (rule?.maxLength && units.length > rule.maxLength) {
2050
2074
  const suffix = rule.ellipsis ?? "";
2051
2075
  const suffixLength = rule.lengthUnit === "code-points" ? [...suffix].length : suffix.length;
@@ -2054,11 +2078,294 @@ function formatMetadataText(value, rule, input, name) {
2054
2078
  if (rule.wordBoundaryMinLength && boundary >= rule.wordBoundaryMinLength) head = head.slice(0, boundary);
2055
2079
  return suffix ? head.trimEnd() + suffix : head;
2056
2080
  }
2057
- return text2;
2081
+ return text3;
2082
+ }
2083
+
2084
+ // src/content-policy-schema.ts
2085
+ import { z as z4 } from "zod";
2086
+ var ContentPolicyFieldNameSchema = z4.string().min(1).max(64).regex(/^[A-Za-z_][A-Za-z0-9_]*$/).refine((name) => name !== "prototype" && !Object.hasOwn(Object.prototype, name));
2087
+ var ContentPolicyScalarSchema = z4.union([
2088
+ z4.string().max(2048),
2089
+ z4.number().finite(),
2090
+ z4.boolean(),
2091
+ z4.null()
2092
+ ]);
2093
+ var contentPolicyEquality = z4.object({
2094
+ field: ContentPolicyFieldNameSchema,
2095
+ equals: ContentPolicyScalarSchema
2096
+ }).strict();
2097
+ var ContentPolicyWhenSchema = z4.union([
2098
+ contentPolicyEquality,
2099
+ z4.object({
2100
+ all: z4.array(contentPolicyEquality).min(1).max(4).optional(),
2101
+ unlessAll: z4.array(contentPolicyEquality).min(1).max(4).optional()
2102
+ }).strict().refine(
2103
+ (when2) => when2.all !== void 0 || when2.unlessAll !== void 0,
2104
+ "\uCF58\uD150\uCE20 \uC870\uAC74\uC5D0\uB294 all \uB610\uB294 unlessAll \uADF8\uB8F9\uC774 \uD544\uC694\uD569\uB2C8\uB2E4"
2105
+ )
2106
+ ]);
2107
+ var ContentPolicyInternalPathSchema = z4.string().min(1).max(2048).refine((path) => {
2108
+ if (!path.startsWith("/") || path.startsWith("//") || /[\\?#\u0000-\u0020\u007f]/u.test(path)) return false;
2109
+ try {
2110
+ return path.split("/").every((part, index) => {
2111
+ if (index === 0) return true;
2112
+ const decoded2 = decodeURIComponent(part);
2113
+ return decoded2 !== "." && decoded2 !== ".." && !/[\\/\u0000-\u0020\u007f]/u.test(decoded2);
2114
+ });
2115
+ } catch {
2116
+ return false;
2117
+ }
2118
+ }, "\uCF58\uD150\uCE20 \uACBD\uB85C\uB294 \uC548\uC804\uD55C \uC0AC\uC774\uD2B8 \uB0B4\uBD80 \uACBD\uB85C\uC5EC\uC57C \uD569\uB2C8\uB2E4");
2119
+ var field = { field: ContentPolicyFieldNameSchema };
2120
+ var scalarChecks = [
2121
+ z4.object({ ...field, op: z4.literal("equals"), value: ContentPolicyScalarSchema }).strict(),
2122
+ z4.object({ ...field, op: z4.literal("notEquals"), value: ContentPolicyScalarSchema }).strict(),
2123
+ z4.object({ ...field, op: z4.literal("oneOf"), values: z4.array(ContentPolicyScalarSchema).min(1).max(16), allowEmpty: z4.boolean().optional() }).strict(),
2124
+ z4.object({ ...field, op: z4.literal("text") }).strict(),
2125
+ z4.object({ ...field, op: z4.literal("date") }).strict(),
2126
+ z4.object({ ...field, op: z4.literal("safeHttps") }).strict(),
2127
+ z4.object({ ...field, op: z4.literal("allowedPath"), paths: z4.array(ContentPolicyInternalPathSchema).min(1).max(512) }).strict()
2128
+ ];
2129
+ var ContentPolicyScalarCheckSchema = z4.discriminatedUnion("op", scalarChecks);
2130
+ var when = { when: ContentPolicyWhenSchema.optional() };
2131
+ var ContentPolicyCheckSchema = z4.discriminatedUnion("op", [
2132
+ scalarChecks[0].extend(when),
2133
+ scalarChecks[1].extend(when),
2134
+ scalarChecks[2].extend(when),
2135
+ scalarChecks[3].extend(when),
2136
+ scalarChecks[4].extend(when),
2137
+ scalarChecks[5].extend(when),
2138
+ scalarChecks[6].extend(when),
2139
+ z4.object({ ...field, ...when, op: z4.literal("some"), checks: z4.array(ContentPolicyScalarCheckSchema).min(1).max(8) }).strict(),
2140
+ z4.object({
2141
+ ...when,
2142
+ op: z4.literal("bodyContent"),
2143
+ allowExcerpt: z4.boolean().optional(),
2144
+ allowFeaturedImage: z4.boolean().optional(),
2145
+ additionalUrl: z4.object({ ...field, ...when }).strict().optional()
2146
+ }).strict(),
2147
+ z4.object({ ...when, op: z4.literal("bodyText") }).strict()
2148
+ ]);
2149
+ var contentPolicy = z4.object({
2150
+ visibility: z4.array(ContentPolicyCheckSchema).max(24),
2151
+ indexing: z4.array(ContentPolicyCheckSchema).max(24),
2152
+ pathSegmentFormat: z4.literal("lower-kebab").optional()
2153
+ }).strict();
2154
+ var ContentPolicySchema = contentPolicy.superRefine((policy, ctx) => {
2155
+ const refs = collectContentPolicyFieldRefs(policy);
2156
+ if (refs.length > 64) ctx.addIssue({ code: z4.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" });
2157
+ for (const ref of refs) {
2158
+ if ((ref.children?.length ?? 0) > 8) ctx.addIssue({ code: z4.ZodIssueCode.custom, message: `\uBC18\uBCF5 \uD544\uB4DC ${ref.name}\uC758 \uC790\uC2DD \uCC38\uC870\uB294 8\uAC1C\uAE4C\uC9C0 \uD5C8\uC6A9\uD569\uB2C8\uB2E4` });
2159
+ }
2160
+ });
2161
+ function collectContentPolicyFieldRefs(policy) {
2162
+ const refs = /* @__PURE__ */ new Map();
2163
+ const add = (name, children = []) => {
2164
+ const names = refs.get(name) ?? /* @__PURE__ */ new Set();
2165
+ for (const child of children) names.add(child);
2166
+ refs.set(name, names);
2167
+ };
2168
+ const addWhen = (when2) => {
2169
+ if (!when2) return;
2170
+ if ("field" in when2) add(when2.field);
2171
+ else for (const condition of [...when2.all ?? [], ...when2.unlessAll ?? []]) add(condition.field);
2172
+ };
2173
+ for (const check of [...policy.visibility, ...policy.indexing]) {
2174
+ addWhen(check.when);
2175
+ if ("field" in check) add(check.field, check.op === "some" ? check.checks.map((child) => child.field) : []);
2176
+ if (check.op === "bodyContent" && check.additionalUrl) {
2177
+ add(check.additionalUrl.field);
2178
+ addWhen(check.additionalUrl.when);
2179
+ }
2180
+ }
2181
+ return [...refs].map(([name, children]) => ({ name, ...children.size ? { children: [...children] } : {} }));
2182
+ }
2183
+
2184
+ // src/content-policy-condition.ts
2185
+ function matchesContentPolicyWhen(when2, fields) {
2186
+ if (!when2) return true;
2187
+ const matches = ({ field: field3, equals }) => {
2188
+ const value = Object.hasOwn(fields, field3) ? fields[field3] : void 0;
2189
+ return value === equals;
2190
+ };
2191
+ if ("field" in when2) return matches(when2);
2192
+ return (when2.all?.every(matches) ?? true) && !(when2.unlessAll?.every(matches) ?? false);
2193
+ }
2194
+
2195
+ // src/post-body-source.ts
2196
+ function resolvePostBodySource(bodyHtml) {
2197
+ if (typeof bodyHtml !== "string") return { kind: "json" };
2198
+ const trimmed = bodyHtml.trim();
2199
+ if (trimmed.length === 0) return { kind: "json" };
2200
+ const sanitized = sanitizeHtml(trimmed);
2201
+ const visible = /<(?:img|hr|br|iframe|video|audio)\b/i.test(sanitized) || sanitized.replace(/<[^>]*>/g, "").replace(/&nbsp;/gi, " ").trim().length > 0;
2202
+ return visible ? { kind: "html", html: sanitized } : { kind: "json" };
2203
+ }
2204
+
2205
+ // src/content-policy.ts
2206
+ var text2 = (value) => typeof value === "string" ? value.trim() : "";
2207
+ var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2208
+ function fieldValue(fields, field3) {
2209
+ return Object.hasOwn(fields, field3) ? fields[field3] : void 0;
2210
+ }
2211
+ function contentPolicySafeHttpsUrl(value) {
2212
+ if (typeof value !== "string" || /[\\\u0000-\u0020\u007f]/u.test(value)) return null;
2213
+ try {
2214
+ const url = new URL(value);
2215
+ return url.protocol === "https:" && !url.username && !url.password ? url.href : null;
2216
+ } catch {
2217
+ return null;
2218
+ }
2219
+ }
2220
+ function dateValue(value) {
2221
+ if (!value || typeof value !== "string" && typeof value !== "number") return null;
2222
+ if (!Number.isFinite(Date.parse(String(value)))) return null;
2223
+ const date = new Date(value).getTime();
2224
+ return Number.isFinite(date) ? date : null;
2225
+ }
2226
+ function contentPolicyBodyText(bodyJson) {
2227
+ const root = record(bodyJson);
2228
+ if (root?.type !== "doc" || !Array.isArray(root.content)) return "";
2229
+ const pending = [{ node: root, depth: 0 }];
2230
+ const output = [];
2231
+ let count = 0;
2232
+ while (pending.length) {
2233
+ const entry = pending.pop();
2234
+ if (++count > 1e5 || entry.depth > 128) throw new Error("Content policy body exceeds traversal limits");
2235
+ const node = record(entry.node);
2236
+ if (!node) continue;
2237
+ if (node.type === "text") {
2238
+ const value = text2(node.text);
2239
+ if (value) output.push(value);
2240
+ } else if (Array.isArray(node.content)) {
2241
+ for (let index = node.content.length - 1; index >= 0; index--) {
2242
+ pending.push({ node: node.content[index], depth: entry.depth + 1 });
2243
+ }
2244
+ }
2245
+ }
2246
+ return output.join(" ");
2247
+ }
2248
+ function matchesContentScalarCheck(check, fields) {
2249
+ const value = fieldValue(fields, check.field);
2250
+ switch (check.op) {
2251
+ case "equals":
2252
+ return value === check.value;
2253
+ case "notEquals":
2254
+ return value !== check.value;
2255
+ case "oneOf":
2256
+ return check.allowEmpty === true && !value || check.values.some((candidate) => candidate === value);
2257
+ case "text":
2258
+ return text2(value).length > 0;
2259
+ case "date":
2260
+ return dateValue(value) !== null;
2261
+ case "safeHttps":
2262
+ return contentPolicySafeHttpsUrl(value) !== null;
2263
+ case "allowedPath":
2264
+ return check.paths.includes(text2(value));
2265
+ }
2266
+ }
2267
+ function bodyFacts(post) {
2268
+ const jsonText = contentPolicyBodyText(post.bodyJson);
2269
+ const source = resolvePostBodySource(post.bodyHtml);
2270
+ const html = source.kind === "html" ? source.html : "";
2271
+ return { jsonText, html, text: source.kind === "html" ? html.replace(/<[^>]*>/g, "").replace(/&nbsp;/gi, " ").trim() : jsonText };
2272
+ }
2273
+ function matchesCheck(check, post, fields, getBody) {
2274
+ if (!matchesContentPolicyWhen(check.when, fields)) return true;
2275
+ if (check.op === "some") {
2276
+ const value = fieldValue(fields, check.field);
2277
+ return Array.isArray(value) && value.some((item) => {
2278
+ const child = record(item);
2279
+ return child !== null && check.checks.every((scalar) => matchesContentScalarCheck(scalar, child));
2280
+ });
2281
+ }
2282
+ if (check.op === "bodyText") return Boolean(getBody().text);
2283
+ if (check.op === "bodyContent") {
2284
+ const body = getBody();
2285
+ return Boolean(body.jsonText || body.html || check.allowExcerpt && text2(post.excerpt) || check.allowFeaturedImage && contentPolicySafeHttpsUrl(post.featuredImageUrl) || check.additionalUrl && matchesContentPolicyWhen(check.additionalUrl.when, fields) && contentPolicySafeHttpsUrl(fieldValue(fields, check.additionalUrl.field)));
2286
+ }
2287
+ return matchesContentScalarCheck(check, fields);
2288
+ }
2289
+ function validPath(policy, post, options) {
2290
+ const path = text2(post.path).replace(/\/+$/, "");
2291
+ if (!path || !ContentPolicyInternalPathSchema.safeParse(path).success) return false;
2292
+ const segments = path.slice(1).split("/");
2293
+ if (segments.some((segment) => !segment)) return false;
2294
+ if (policy.pathSegmentFormat === "lower-kebab" && segments.some((segment) => !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(segment))) return false;
2295
+ if (decodeURIComponent(segments.at(-1)) !== post.slug) return false;
2296
+ if (options.expectedPath !== void 0) {
2297
+ return options.expectedPath !== null && path === options.expectedPath.replace(/\/+$/, "");
2298
+ }
2299
+ return true;
2300
+ }
2301
+ function evaluateContentPolicy(policy, post, options = {}) {
2302
+ const visibility = [];
2303
+ if (post.status !== "published") visibility.push("content:not-published");
2304
+ if (!text2(post.id) || !text2(post.title) || !text2(post.slug)) visibility.push("content:invalid-identity");
2305
+ const publishedAt = dateValue(post.publishedAt);
2306
+ if (publishedAt === null) visibility.push("content:invalid-published-at");
2307
+ else if (publishedAt > (options.now ?? Date.now())) visibility.push("content:future-publication");
2308
+ if (!validPath(policy, post, options)) visibility.push("content:invalid-path");
2309
+ const fields = post.fields ?? {};
2310
+ let body;
2311
+ const getBody = () => body ??= bodyFacts(post);
2312
+ const failures = (checks, stage) => checks.flatMap((check) => matchesCheck(check, post, fields, getBody) ? [] : [`content:${stage}:${check.op}${"field" in check ? `:${check.field}` : ""}`]);
2313
+ if (visibility.length === 0) visibility.push(...failures(policy.visibility, "visibility"));
2314
+ const visible = visibility.length === 0;
2315
+ const indexing = visible ? failures(policy.indexing, "indexing") : [];
2316
+ const forced = !visible || indexing.length > 0 || options.robots?.forceNoindex === true;
2317
+ const inheritedReasons = options.robots?.forceNoindex ? options.robots.noindexReasons?.length ? options.robots.noindexReasons : ["content:forced-noindex"] : [];
2318
+ const noindexReasons = [.../* @__PURE__ */ new Set([...visibility, ...indexing, ...inheritedReasons])].slice(0, 64);
2319
+ const manualNoindex = readMetadataValues(post.metaJson?.seo).noindex === true;
2320
+ return {
2321
+ visible,
2322
+ indexable: visible && !forced && !manualNoindex,
2323
+ robots: {
2324
+ ...options.robots,
2325
+ ...forced ? { forceNoindex: true } : {},
2326
+ ...noindexReasons.length ? { noindexReasons } : {}
2327
+ },
2328
+ reasons: [...noindexReasons, ...manualNoindex ? ["content:manual-noindex"] : []]
2329
+ };
2330
+ }
2331
+
2332
+ // src/metadata-video-thumbnail.ts
2333
+ import { z as z5 } from "zod";
2334
+ var MetadataVideoThumbnailRuleSchema = z5.object({
2335
+ source: z5.literal("youtube"),
2336
+ field: ContentPolicyFieldNameSchema,
2337
+ when: ContentPolicyWhenSchema.optional()
2338
+ }).strict();
2339
+ function metadataVideoThumbnail(rule, fields) {
2340
+ if (!rule || !matchesContentPolicyWhen(rule.when, fields ?? {})) return void 0;
2341
+ const value = fields && Object.hasOwn(fields, rule.field) ? fields[rule.field] : void 0;
2342
+ const safe = contentPolicySafeHttpsUrl(value);
2343
+ if (!safe) return void 0;
2344
+ const url = new URL(safe);
2345
+ const id = url.hostname === "youtu.be" ? url.pathname.slice(1) : ["www.youtube.com", "youtube.com", "m.youtube.com"].includes(url.hostname) ? url.pathname === "/watch" ? url.searchParams.get("v") : url.pathname.match(/^\/(?:shorts|embed)\/([^/]+)$/)?.[1] : null;
2346
+ return /^[a-zA-Z0-9_-]{11}$/.test(id ?? "") ? `https://i.ytimg.com/vi/${id}/hqdefault.jpg` : void 0;
2347
+ }
2348
+
2349
+ // src/metadata-default-fields.ts
2350
+ function collectMetadataDefaultFieldRefs(rule) {
2351
+ const fields = /* @__PURE__ */ new Set();
2352
+ const video = rule?.videoThumbnail;
2353
+ if (video) {
2354
+ fields.add(video.field);
2355
+ if (video.when) {
2356
+ if ("field" in video.when) fields.add(video.when.field);
2357
+ else for (const condition of [...video.when.all ?? [], ...video.when.unlessAll ?? []]) fields.add(condition.field);
2358
+ }
2359
+ }
2360
+ for (const text3 of [rule?.title, rule?.description]) {
2361
+ if (text3?.source !== "template") continue;
2362
+ for (const match of (text3.template ?? "").matchAll(/\{\{field:([a-zA-Z][a-zA-Z0-9_-]{0,159})\}\}/gu)) fields.add(match[1]);
2363
+ }
2364
+ return [...fields].map((name) => ({ name }));
2058
2365
  }
2059
2366
 
2060
2367
  // src/metadata-defaults.ts
2061
- var imageOrigin = z4.string().max(2048).refine((value) => {
2368
+ var imageOrigin = z6.string().max(2048).refine((value) => {
2062
2369
  try {
2063
2370
  const url = new URL(value);
2064
2371
  return url.protocol === "https:" && url.origin === value;
@@ -2066,23 +2373,34 @@ var imageOrigin = z4.string().max(2048).refine((value) => {
2066
2373
  return false;
2067
2374
  }
2068
2375
  }, "\uC774\uBBF8\uC9C0 \uD5C8\uC6A9 \uBAA9\uB85D\uC5D0\uB294 \uACBD\uB85C \uC5C6\uB294 HTTPS origin\uB9CC \uC120\uC5B8\uD569\uB2C8\uB2E4");
2069
- var MetadataDefaultRulesSchema = z4.object({
2376
+ var MetadataDefaultRulesSchema = z6.object({
2070
2377
  title: MetadataTextRuleSchema.optional(),
2071
- titleTemplateMaxLength: z4.number().int().min(1).max(1e3).optional(),
2378
+ titleTemplateMaxLength: z6.number().int().min(1).max(1e3).optional(),
2072
2379
  description: MetadataTextRuleSchema.optional(),
2073
2380
  /** Code-owned defaults precede site defaults but always follow manual overrides. */
2074
2381
  defaults: MetadataValuesSchema.optional(),
2075
2382
  robots: MetadataRobotsPolicySchema.optional(),
2076
- useFeaturedImage: z4.boolean().optional(),
2077
- useFirstBodyImage: z4.boolean().optional(),
2078
- useTermImage: z4.boolean().optional(),
2079
- imageAltSource: z4.enum(["content", "title"]).optional(),
2080
- legacyImageSource: z4.literal("legacy-thumbnail").optional(),
2081
- imageAllowedOrigins: z4.array(imageOrigin).min(1).max(32).optional(),
2082
- legacyImageAllowedOrigins: z4.array(imageOrigin).min(1).max(32).optional(),
2083
- normalizeCloudflareImages: z4.boolean().optional(),
2383
+ useFeaturedImage: z6.boolean().optional(),
2384
+ videoThumbnail: MetadataVideoThumbnailRuleSchema.optional(),
2385
+ useFirstBodyImage: z6.boolean().optional(),
2386
+ useTermImage: z6.boolean().optional(),
2387
+ imageAltSource: z6.enum(["content", "title"]).optional(),
2388
+ legacyImageSource: z6.literal("legacy-thumbnail").optional(),
2389
+ imageAllowedOrigins: z6.array(imageOrigin).min(1).max(32).optional(),
2390
+ legacyImageAllowedOrigins: z6.array(imageOrigin).min(1).max(32).optional(),
2391
+ normalizeCloudflareImages: z6.boolean().optional(),
2084
2392
  fallback: MetadataValuesSchema.optional()
2085
- }).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");
2393
+ }).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").superRefine((rule, context) => {
2394
+ if (!rule.videoThumbnail) return;
2395
+ const refs = collectMetadataDefaultFieldRefs(rule);
2396
+ if (refs.length > 64) context.addIssue({ code: z6.ZodIssueCode.custom, message: "\uC601\uC0C1 \uC790\uB3D9\uAC12\uC740 \uACF5\uAC1C \uD544\uB4DC 64\uAC1C\uAE4C\uC9C0 \uCC38\uC870\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4" });
2397
+ for (const { name } of refs) {
2398
+ if (!ContentPolicyFieldNameSchema.safeParse(name).success) context.addIssue({
2399
+ code: z6.ZodIssueCode.custom,
2400
+ message: "\uC601\uC0C1 \uC790\uB3D9\uAC12\uC758 \uD14D\uC2A4\uD2B8\xB7\uC870\uAC74\uC740 \uC548\uC804\uD55C \uACF5\uAC1C \uD544\uB4DC \uC774\uB984\uC744 \uC0AC\uC6A9\uD569\uB2C8\uB2E4"
2401
+ });
2402
+ }
2403
+ });
2086
2404
  function metadataContentImageUrl(value, normalizeCloudflareImages = false) {
2087
2405
  if (!normalizeCloudflareImages) return value;
2088
2406
  try {
@@ -2129,8 +2447,9 @@ function formatMetadataDefaults(input, declaration) {
2129
2447
  const finalTitle = templatedTitle && rule?.titleTemplateMaxLength && templatedTitle.length > rule.titleTemplateMaxLength ? title : templatedTitle;
2130
2448
  const description = formatMetadataText(input.description, rule?.description, input, name);
2131
2449
  const bodyImage = input.firstBodyImage && (input.firstBodyImage.startsWith("https://") || /^\/(?!\/)/u.test(input.firstBodyImage)) ? input.firstBodyImage : void 0;
2132
- const featuredImage = (input.kind === "terms" ? rule?.useTermImage : rule?.useFeaturedImage) === false ? void 0 : input.featuredImage;
2133
- const candidateImage = featuredImage || (rule?.useFirstBodyImage ? bodyImage : void 0) || legacyImageUrl(input.legacyImage, rule);
2450
+ const rawFeaturedImage = (input.kind === "terms" ? rule?.useTermImage : rule?.useFeaturedImage) === false ? void 0 : input.featuredImage;
2451
+ const featuredImage = rule?.videoThumbnail ? contentPolicySafeHttpsUrl(rawFeaturedImage) : rawFeaturedImage;
2452
+ const candidateImage = featuredImage || (input.kind === "posts" ? metadataVideoThumbnail(rule?.videoThumbnail, input.fields) : void 0) || (rule?.useFirstBodyImage ? bodyImage : void 0) || legacyImageUrl(input.legacyImage, rule);
2134
2453
  const ogImage = candidateImage && contentCandidateAllowed(candidateImage, input, rule) ? readMetadataValues({ ogImage: metadataContentImageUrl(candidateImage, rule?.normalizeCloudflareImages) }).ogImage : void 0;
2135
2454
  return {
2136
2455
  ...readMetadataValues(rule?.defaults),
@@ -2140,6 +2459,85 @@ function formatMetadataDefaults(input, declaration) {
2140
2459
  };
2141
2460
  }
2142
2461
 
2462
+ // src/content-query.ts
2463
+ import { z as z7 } from "zod";
2464
+ var field2 = { field: ContentPolicyFieldNameSchema };
2465
+ var ContentQueryFilterSchema = z7.discriminatedUnion("op", [
2466
+ z7.object({ ...field2, op: z7.literal("equals"), value: ContentPolicyScalarSchema }).strict(),
2467
+ z7.object({ ...field2, op: z7.literal("notEquals"), value: ContentPolicyScalarSchema }).strict(),
2468
+ z7.object({ ...field2, op: z7.literal("oneOf"), values: z7.array(ContentPolicyScalarSchema).min(1).max(16), allowEmpty: z7.boolean().optional() }).strict()
2469
+ ]);
2470
+ var ContentQuerySchema = z7.object({
2471
+ modelKeys: z7.array(z7.string().min(1).max(100).regex(/^[a-z][a-z0-9_-]*$/)).min(1).max(16),
2472
+ filters: z7.array(ContentQueryFilterSchema).max(8).optional(),
2473
+ scope: z7.enum(["currentTerm", "currentPath"]).optional(),
2474
+ pinnedField: ContentPolicyFieldNameSchema.optional(),
2475
+ pageSize: z7.number().int().min(1).max(100),
2476
+ requireItems: z7.boolean().optional()
2477
+ }).strict();
2478
+ function collectContentQueryFieldRefs(query) {
2479
+ return [.../* @__PURE__ */ new Set([...(query.filters ?? []).map((filter) => filter.field), ...query.pinnedField ? [query.pinnedField] : []])].map((name) => ({ name }));
2480
+ }
2481
+ function matchesScope(query, post, options) {
2482
+ if (query.scope === "currentTerm") return post.terms?.some((term) => term.id === options.termId) === true;
2483
+ if (query.scope === "currentPath") {
2484
+ const path = options.path.replace(/\/+$/, "");
2485
+ return post.path?.trim().startsWith(`${path}/`) === true;
2486
+ }
2487
+ return true;
2488
+ }
2489
+ function evaluateContentQuery(query, candidates, options) {
2490
+ if (!options.complete) throw new Error("Content query input is incomplete");
2491
+ const page = options.page ?? 1;
2492
+ if (!Number.isSafeInteger(page) || page < 1) throw new Error("Content query page must be a positive safe integer");
2493
+ if (query.scope === "currentTerm" && !options.termId) throw new Error("Content query requires the current term id");
2494
+ if (query.scope === "currentPath" && (!options.path || !ContentPolicyInternalPathSchema.safeParse(options.path).success)) throw new Error("Content query requires a safe current path");
2495
+ const visible = candidates.filter(({ policy }) => policy.visible);
2496
+ const pathCounts = /* @__PURE__ */ new Map();
2497
+ const canonical = (post) => post.path?.trim().replace(/\/+$/, "") ?? null;
2498
+ for (const { post } of visible) pathCounts.set(canonical(post), (pathCounts.get(canonical(post)) ?? 0) + 1);
2499
+ const seen = /* @__PURE__ */ new Set();
2500
+ const selected = visible.filter(({ post }) => {
2501
+ if (pathCounts.get(canonical(post)) !== 1 || seen.has(post.id)) return false;
2502
+ seen.add(post.id);
2503
+ return query.modelKeys.includes(post.modelKey ?? post.collectionKey ?? "") && matchesScope(query, post, options) && (query.filters ?? []).every((filter) => matchesContentScalarCheck(filter, post.fields ?? {}));
2504
+ }).sort((left, right) => {
2505
+ if (query.pinnedField) {
2506
+ const delta = Number(right.post.fields?.[query.pinnedField] === true) - Number(left.post.fields?.[query.pinnedField] === true);
2507
+ if (delta) return delta;
2508
+ }
2509
+ return Date.parse(right.post.publishedAt) - Date.parse(left.post.publishedAt) || left.post.id.localeCompare(right.post.id);
2510
+ });
2511
+ const pageCount = Math.max(1, Math.ceil(selected.length / query.pageSize));
2512
+ const current = selected.slice((page - 1) * query.pageSize, page * query.pageSize);
2513
+ const pageVisible = page <= pageCount && (!query.requireItems || selected.length > 0);
2514
+ const hasIndexable = current.some(({ policy }) => policy.indexable);
2515
+ const forced = !hasIndexable || options.robots?.forceNoindex === true;
2516
+ const reasons = [.../* @__PURE__ */ new Set([
2517
+ ...page > pageCount ? ["content:invalid-page"] : query.requireItems && !selected.length ? ["content:empty-required-list"] : !current.length ? ["content:empty-page"] : !hasIndexable ? ["content:no-indexable-items"] : [],
2518
+ ...options.robots?.forceNoindex ? options.robots.noindexReasons?.length ? options.robots.noindexReasons : ["content:forced-noindex"] : []
2519
+ ])].slice(0, 64);
2520
+ return {
2521
+ items: current.map(({ post }) => post),
2522
+ total: selected.length,
2523
+ page,
2524
+ pageCount,
2525
+ visible: pageVisible,
2526
+ indexable: pageVisible && !forced,
2527
+ robots: { ...options.robots, ...forced ? { forceNoindex: true } : {}, ...reasons.length ? { noindexReasons: reasons } : {} },
2528
+ reasons
2529
+ };
2530
+ }
2531
+ function selectContentQuery(query, posts, policies, options) {
2532
+ if (!options.complete) throw new Error("Content query input is incomplete");
2533
+ const candidates = posts.filter((post) => query.modelKeys.includes(post.modelKey ?? post.collectionKey ?? "")).map((post) => {
2534
+ const modelKey = post.modelKey ?? post.collectionKey ?? "";
2535
+ if (!Object.hasOwn(policies, modelKey)) throw new Error(`Content query has no policy for model ${modelKey}`);
2536
+ return { post, policy: evaluateContentPolicy(policies[modelKey], post, { now: options.now, ...options.postOptions(post) }) };
2537
+ });
2538
+ return evaluateContentQuery(query, candidates, options);
2539
+ }
2540
+
2143
2541
  // src/redirect-paths.ts
2144
2542
  var CONTROL_OR_BACKSLASH = /[\\\u0000-\u001f\u007f]/u;
2145
2543
  var PROTECTED_PREFIX = /^\/(?:api|_next|_vercel|auth|admin|login|logout|revalidate|preview|assets|static|images|fonts|\.well-known)(?:\/|$)/i;
@@ -2206,6 +2604,15 @@ export {
2206
2604
  CMS_IMAGE_WIDTHS,
2207
2605
  CMS_LINE_HEIGHTS,
2208
2606
  CMS_PARAGRAPH_SPACINGS,
2607
+ ContentPolicyCheckSchema,
2608
+ ContentPolicyFieldNameSchema,
2609
+ ContentPolicyInternalPathSchema,
2610
+ ContentPolicyScalarCheckSchema,
2611
+ ContentPolicyScalarSchema,
2612
+ ContentPolicySchema,
2613
+ ContentPolicyWhenSchema,
2614
+ ContentQueryFilterSchema,
2615
+ ContentQuerySchema,
2209
2616
  DEFAULT_SITE_PATTERN_PRESENTATION,
2210
2617
  EMPTY_SITE_PATTERN_PLACEMENTS,
2211
2618
  FIELD_BINDING_SOURCE,
@@ -2243,10 +2650,17 @@ export {
2243
2650
  cmsImageWidth,
2244
2651
  cmsLineHeight,
2245
2652
  cmsParagraphSpacing,
2653
+ collectContentPolicyFieldRefs,
2654
+ collectContentQueryFieldRefs,
2655
+ collectMetadataDefaultFieldRefs,
2656
+ contentPolicyBodyText,
2657
+ contentPolicySafeHttpsUrl,
2246
2658
  createFieldBindingResolver,
2247
2659
  emptyPatternSlots,
2248
2660
  ensureImportedHtmlDocumentRoots,
2249
2661
  ensureStableIds,
2662
+ evaluateContentPolicy,
2663
+ evaluateContentQuery,
2250
2664
  extractFaqEntries,
2251
2665
  faqPageJsonLd,
2252
2666
  findInternalContentLinkTokens,
@@ -2263,14 +2677,17 @@ export {
2263
2677
  isAllowedMapEmbedSrc,
2264
2678
  isInCollectionMode,
2265
2679
  isProtectedRedirectPath,
2680
+ isReservedCategoryTreeSlug,
2266
2681
  isSectionContentPropPath,
2267
2682
  isSitePatternSlotKey,
2268
2683
  isValidBlock,
2269
2684
  isValidSitePatternKey,
2270
2685
  issuePreviewToken,
2686
+ matchesContentScalarCheck,
2271
2687
  mergeMetadataPatch,
2272
2688
  metadataBodyParagraphs,
2273
2689
  metadataContentImageUrl,
2690
+ metadataVideoThumbnail,
2274
2691
  normalizeEmbed,
2275
2692
  normalizeHexColor,
2276
2693
  normalizeImageAlt,
@@ -2289,11 +2706,13 @@ export {
2289
2706
  renderInternalContentLinksInText,
2290
2707
  resolveCategoryPath,
2291
2708
  resolveCategoryTreeChain,
2709
+ resolveCategoryTreeEntryCategory,
2292
2710
  resolveCategoryTreeEntryPath,
2293
2711
  resolveCategoryTreeHubPaths,
2294
2712
  resolveCategoryTreeSelection,
2295
2713
  resolveMetadata,
2296
2714
  resolvePatternSlots,
2715
+ resolvePostBodySource,
2297
2716
  resolvePostCategorySlug,
2298
2717
  resolvePostCollection,
2299
2718
  resolvePostPath,
@@ -2303,6 +2722,7 @@ export {
2303
2722
  sanitizeImportedCss,
2304
2723
  sanitizeImportedHtml,
2305
2724
  scopeImportedCss,
2725
+ selectContentQuery,
2306
2726
  serializeBlock,
2307
2727
  setSitePatternPlacementRule,
2308
2728
  sitePatternButtonColor,