@roottale/cms-core 0.65.1 → 0.67.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,39 +225,51 @@ function validateCollections(collections) {
225
225
  }
226
226
 
227
227
  // src/category-tree.ts
228
- function normalizeBasePath(basePath) {
228
+ function basePathPrefix(basePath) {
229
+ if (!basePath.trim()) return null;
229
230
  return basePath.replace(/\/+$/, "");
230
231
  }
231
- function resolveCategoryTreeChain(presentation, leafId, terms) {
232
- if (!Number.isInteger(presentation.categoryDepth) || presentation.categoryDepth < 1) {
233
- return null;
234
- }
232
+ function categoryTreeAncestry(categoryDepth, termId, terms) {
233
+ if (!Number.isInteger(categoryDepth) || categoryDepth < 1) return null;
235
234
  const termsById = new Map(terms.map((term) => [term.id, term]));
236
235
  if (termsById.size !== terms.length) return null;
237
- if (terms.some((term) => term.parentId === leafId)) return null;
238
236
  const reversed = [];
239
237
  const visited = /* @__PURE__ */ new Set();
240
- let currentId = leafId;
238
+ let currentId = termId;
241
239
  while (currentId) {
242
240
  if (visited.has(currentId)) return null;
243
241
  visited.add(currentId);
244
242
  const current = termsById.get(currentId);
245
243
  if (!current || !current.slug) return null;
246
244
  reversed.push(current);
245
+ if (reversed.length > categoryDepth) return null;
247
246
  currentId = current.parentId ?? null;
248
247
  }
249
- if (reversed.length !== presentation.categoryDepth) return null;
250
248
  return reversed.reverse();
251
249
  }
250
+ function hasChildCategory(termId, terms) {
251
+ return terms.some((term) => term.parentId === termId);
252
+ }
253
+ function resolveCategoryTreeChain(presentation, termId, terms) {
254
+ const chain = categoryTreeAncestry(presentation.categoryDepth, termId, terms);
255
+ if (!chain) return null;
256
+ if (presentation.entryCategory === "leaf") return chain;
257
+ if (hasChildCategory(termId, terms)) return null;
258
+ return chain.length === presentation.categoryDepth ? chain : null;
259
+ }
260
+ function resolveCategoryTreeSelection(presentation, termId, terms) {
261
+ if (hasChildCategory(termId, terms)) return null;
262
+ return resolveCategoryTreeChain(presentation, termId, terms);
263
+ }
252
264
  function resolveCategoryTreeHubPaths(presentation, termId, terms) {
253
265
  const chain = resolveCategoryTreeChain(presentation, termId, terms);
254
- const basePath = normalizeBasePath(presentation.basePath);
255
- if (!chain || !basePath) return null;
256
- const paths = [basePath];
266
+ const prefix = basePathPrefix(presentation.basePath);
267
+ if (!chain || prefix === null) return null;
268
+ const paths = [prefix || "/"];
257
269
  const segments = [];
258
270
  for (const term of chain) {
259
271
  segments.push(encodeURIComponent(term.slug));
260
- paths.push(`${basePath}/${segments.join("/")}`);
272
+ paths.push(`${prefix}/${segments.join("/")}`);
261
273
  }
262
274
  return paths;
263
275
  }
@@ -961,10 +973,10 @@ function scopeImportedCss(css, scope, allowedAssetUrls = []) {
961
973
 
962
974
  // src/internal-content-links.ts
963
975
  var INTERNAL_CONTENT_LINK_TOKEN_SOURCE = String.raw`\[\[internal:([a-z0-9가-힣]+(?:[.-][a-z0-9가-힣]+)*)\|([^\]\n|]+)\]\]`;
964
- function findInternalContentLinkTokens(text) {
976
+ function findInternalContentLinkTokens(text2) {
965
977
  const pattern = new RegExp(INTERNAL_CONTENT_LINK_TOKEN_SOURCE, "giu");
966
978
  const tokens = [];
967
- for (const match of text.matchAll(pattern)) {
979
+ for (const match of text2.matchAll(pattern)) {
968
980
  const [raw, key, label] = match;
969
981
  if (!key || !label || match.index === void 0) continue;
970
982
  tokens.push({
@@ -1023,19 +1035,19 @@ function escapeHtmlText(value) {
1023
1035
  var INTERNAL_CONTENT_LINK_CLASS = "rt-internal-link";
1024
1036
  var INTERNAL_CONTENT_LINK_PENDING_CLASS = "rt-internal-link--pending";
1025
1037
  var INTERNAL_CONTENT_LINK_PENDING_ATTR = "data-rt-internal-link-pending";
1026
- function renderInternalContentLinksInText(text, resolve) {
1027
- const tokens = findInternalContentLinkTokens(text);
1028
- if (tokens.length === 0) return { html: text, hasTokens: false };
1038
+ function renderInternalContentLinksInText(text2, resolve) {
1039
+ const tokens = findInternalContentLinkTokens(text2);
1040
+ if (tokens.length === 0) return { html: text2, hasTokens: false };
1029
1041
  let cursor = 0;
1030
1042
  let html = "";
1031
1043
  for (const token of tokens) {
1032
- html += text.slice(cursor, token.index);
1044
+ html += text2.slice(cursor, token.index);
1033
1045
  const path = resolve(token.key);
1034
1046
  const label = escapeHtmlText(token.label);
1035
1047
  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>`;
1036
1048
  cursor = token.index + token.raw.length;
1037
1049
  }
1038
- html += text.slice(cursor);
1050
+ html += text2.slice(cursor);
1039
1051
  return { html, hasTokens: true };
1040
1052
  }
1041
1053
  var INTERNAL_LINK_BLOCKED_HTML_TAGS = /* @__PURE__ */ new Set([
@@ -1069,12 +1081,12 @@ function renderInternalContentLinksInHtml(html, resolve) {
1069
1081
  let cursor = 0;
1070
1082
  let out = "";
1071
1083
  let hasTokens = false;
1072
- const emitText = (text) => {
1073
- if (blockedDepth > 0 || !text) {
1074
- out += text;
1084
+ const emitText = (text2) => {
1085
+ if (blockedDepth > 0 || !text2) {
1086
+ out += text2;
1075
1087
  return;
1076
1088
  }
1077
- const rendered = renderInternalContentLinksInText(text, resolve);
1089
+ const rendered = renderInternalContentLinksInText(text2, resolve);
1078
1090
  hasTokens ||= rendered.hasTokens;
1079
1091
  out += rendered.html;
1080
1092
  };
@@ -1213,8 +1225,8 @@ function fromCodePointSafe2(n) {
1213
1225
  }
1214
1226
  }
1215
1227
  function decodeUrlProbe(value) {
1216
- const decoded = value.replace(/&#x([0-9a-f]+);?/gi, (_, h) => fromCodePointSafe2(parseInt(h, 16))).replace(/&#(\d+);?/g, (_, d) => fromCodePointSafe2(parseInt(d, 10))).replace(/&([a-z]+);/gi, (m, name) => NAMED_ENTITY[name.toLowerCase()] ?? m);
1217
- return [...decoded].filter((c) => {
1228
+ const decoded2 = value.replace(/&#x([0-9a-f]+);?/gi, (_, h) => fromCodePointSafe2(parseInt(h, 16))).replace(/&#(\d+);?/g, (_, d) => fromCodePointSafe2(parseInt(d, 10))).replace(/&([a-z]+);/gi, (m, name) => NAMED_ENTITY[name.toLowerCase()] ?? m);
1229
+ return [...decoded2].filter((c) => {
1218
1230
  const n = c.codePointAt(0) ?? 0;
1219
1231
  return n > 32 && n !== 127;
1220
1232
  }).join("").toLowerCase();
@@ -1838,6 +1850,287 @@ function formatSectionLockViolations(violations) {
1838
1850
  return `${label} \uAD6C\uC870 \uBCC0\uACBD(${violation.field})`;
1839
1851
  }).join(" / ");
1840
1852
  }
1853
+
1854
+ // src/metadata-resolver.ts
1855
+ function text(value) {
1856
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
1857
+ }
1858
+ function imageUrl(value, base) {
1859
+ const candidate = text(value);
1860
+ if (!candidate || candidate.startsWith("//") || /[\\\u0000-\u0020\u007f]/u.test(candidate)) return void 0;
1861
+ if (candidate.startsWith("/") && !base) return candidate;
1862
+ try {
1863
+ const url = new URL(candidate, base);
1864
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) return void 0;
1865
+ return url.href;
1866
+ } catch {
1867
+ return void 0;
1868
+ }
1869
+ }
1870
+ function resolveMetadata(input) {
1871
+ const layers = [input.overrides, input.defaults, input.siteDefaults, input.fallback];
1872
+ const result = {};
1873
+ for (const field of ["title", "description", "ogImageAlt"]) {
1874
+ for (const layer of layers) {
1875
+ const value = text(layer?.[field]);
1876
+ if (value !== void 0) {
1877
+ result[field] = value;
1878
+ break;
1879
+ }
1880
+ }
1881
+ }
1882
+ for (const layer of layers) {
1883
+ const value = imageUrl(layer?.ogImage, input.imageBaseUrl);
1884
+ if (value !== void 0) {
1885
+ result.ogImage = value;
1886
+ break;
1887
+ }
1888
+ }
1889
+ for (const field of ["noindex", "nofollow"]) {
1890
+ for (const layer of layers) {
1891
+ const value = layer?.[field];
1892
+ if (typeof value === "boolean") {
1893
+ result[field] = value;
1894
+ break;
1895
+ }
1896
+ }
1897
+ }
1898
+ if (input.robots?.forceNoindex) result.noindex = true;
1899
+ if (input.robots?.forceNofollow) result.nofollow = true;
1900
+ return result;
1901
+ }
1902
+
1903
+ // src/metadata-contract.ts
1904
+ import { z as z2 } from "zod";
1905
+ var METADATA_FIELDS = ["title", "description", "ogImage", "ogImageAlt", "noindex", "nofollow"];
1906
+ var MetadataFieldSchema = z2.enum(METADATA_FIELDS);
1907
+ var MetadataTargetKindSchema = z2.enum(["route", "post", "term"]);
1908
+ var textValue = (max) => z2.string().trim().min(1).max(max);
1909
+ var imageValue = textValue(2048).refine((value) => {
1910
+ if (/[\\\u0000-\u0020\u007f]/u.test(value) || value.startsWith("//")) return false;
1911
+ if (value.startsWith("/")) return true;
1912
+ try {
1913
+ const url = new URL(value);
1914
+ return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password;
1915
+ } catch {
1916
+ return false;
1917
+ }
1918
+ }, "\uACF5\uC720 \uC774\uBBF8\uC9C0\uC5D0\uB294 \uC0AC\uC774\uD2B8 \uB0B4\uBD80 \uACBD\uB85C\uB098 HTTP/HTTPS \uC8FC\uC18C\uB97C \uC785\uB825\uD574 \uC8FC\uC138\uC694");
1919
+ var metadataFields = {
1920
+ // Existing content titles (500) plus a site title template (400) remain valid automatic values.
1921
+ title: textValue(1e3),
1922
+ description: textValue(2e3),
1923
+ ogImage: imageValue,
1924
+ // Automatic alt text may use the complete automatic title and its site suffix.
1925
+ ogImageAlt: textValue(1e3),
1926
+ noindex: z2.boolean(),
1927
+ nofollow: z2.boolean()
1928
+ };
1929
+ var MetadataValuesSchema = z2.object(metadataFields).partial().strict();
1930
+ var MetadataPatchSchema = z2.object({
1931
+ title: textValue(300).nullable().optional(),
1932
+ description: metadataFields.description.nullable().optional(),
1933
+ ogImage: metadataFields.ogImage.nullable().optional(),
1934
+ ogImageAlt: textValue(500).nullable().optional(),
1935
+ noindex: metadataFields.noindex.nullable().optional(),
1936
+ nofollow: metadataFields.nofollow.nullable().optional()
1937
+ }).strict();
1938
+ var MetadataRobotsPolicySchema = z2.object({
1939
+ forceNoindex: z2.boolean().optional(),
1940
+ forceNofollow: z2.boolean().optional()
1941
+ }).strict();
1942
+ function readMetadataValues(input) {
1943
+ if (!input || typeof input !== "object" || Array.isArray(input)) return {};
1944
+ const record = input;
1945
+ 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;
1949
+ }
1950
+ return MetadataValuesSchema.parse(output);
1951
+ }
1952
+ function mergeMetadataPatch(current, input) {
1953
+ const patch = MetadataPatchSchema.parse(input);
1954
+ 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];
1958
+ }
1959
+ return next;
1960
+ }
1961
+
1962
+ // src/metadata-defaults.ts
1963
+ import { z as z3 } from "zod";
1964
+ 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");
1973
+ var MetadataTextRuleSchema = z3.object({
1974
+ source: z3.enum(["content", "fallback"]).optional(),
1975
+ stripLeadingLabel: z3.enum(["question", "answer"]).optional(),
1976
+ replaceLineBreakTokens: z3.boolean().optional(),
1977
+ removeEmDashes: z3.boolean().optional(),
1978
+ normalizeWhitespace: z3.boolean().optional(),
1979
+ maxLength: z3.number().int().min(1).max(2e3).optional(),
1980
+ ellipsis: z3.string().max(10).optional(),
1981
+ fallback: fallbackText.optional(),
1982
+ fallbackWithParent: fallbackText.optional(),
1983
+ fallbackWithoutParent: fallbackText.optional(),
1984
+ 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) {
2015
+ const suffix = rule.ellipsis ?? "";
2016
+ const head = text2.slice(0, rule.maxLength - suffix.length);
2017
+ return suffix ? head.trimEnd() + suffix : head;
2018
+ }
2019
+ return text2;
2020
+ }
2021
+ function metadataContentImageUrl(value, normalizeCloudflareImages = false) {
2022
+ if (!normalizeCloudflareImages) return value;
2023
+ try {
2024
+ const url = new URL(value);
2025
+ const [account, id, variant] = url.pathname.split("/").filter(Boolean);
2026
+ if (url.protocol === "https:" && url.hostname === "imagedelivery.net" && account && id && variant) {
2027
+ url.pathname = `/${account}/${id}/lg`;
2028
+ return url.href;
2029
+ }
2030
+ } catch {
2031
+ }
2032
+ return value;
2033
+ }
2034
+ function legacyImageUrl(value, rule) {
2035
+ if (!value || rule?.legacyImageSource !== "legacy-thumbnail") return void 0;
2036
+ try {
2037
+ const url = new URL(value);
2038
+ if (!rule.legacyImageAllowedOrigins?.includes(url.origin)) return void 0;
2039
+ return readMetadataValues({ ogImage: url.href }).ogImage;
2040
+ } catch {
2041
+ return void 0;
2042
+ }
2043
+ }
2044
+ function contentCandidateAllowed(value, input, rule) {
2045
+ if (!rule?.imageAllowedOrigins) return true;
2046
+ if (value.startsWith("/") && !value.startsWith("//")) return true;
2047
+ try {
2048
+ const url = new URL(value);
2049
+ if (url.protocol !== "https:") return false;
2050
+ const origins = [...rule.imageAllowedOrigins];
2051
+ if (input.imageBaseUrl) origins.push(new URL(input.imageBaseUrl).origin);
2052
+ return origins.includes(url.origin);
2053
+ } catch {
2054
+ return false;
2055
+ }
2056
+ }
2057
+ function formatMetadataDefaults(input, declaration) {
2058
+ const dynamic = declaration[input.kind];
2059
+ 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);
2064
+ 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);
2066
+ const ogImage = candidateImage && contentCandidateAllowed(candidateImage, input, rule) ? readMetadataValues({ ogImage: metadataContentImageUrl(candidateImage, rule?.normalizeCloudflareImages) }).ogImage : void 0;
2067
+ return {
2068
+ ...title ? { title: template ? template.replace("%s", () => title) : title } : {},
2069
+ ...description ? { description } : {},
2070
+ ...ogImage ? { ogImage } : {}
2071
+ };
2072
+ }
2073
+
2074
+ // src/redirect-paths.ts
2075
+ var CONTROL_OR_BACKSLASH = /[\\\u0000-\u001f\u007f]/u;
2076
+ var PROTECTED_PREFIX = /^\/(?:api|_next|_vercel|auth|admin|login|logout|revalidate|preview|assets|static|images|fonts|\.well-known)(?:\/|$)/i;
2077
+ var STATIC_EXTENSION = /\.(?:ico|png|jpe?g|gif|svg|webp|avif|css|js|txt|xml|json|woff2?|map)$/i;
2078
+ function decoded(value) {
2079
+ try {
2080
+ return decodeURIComponent(value);
2081
+ } catch {
2082
+ return null;
2083
+ }
2084
+ }
2085
+ function trimInternalPath(path) {
2086
+ if (!path.startsWith("/")) path = `/${path}`;
2087
+ if (path.length > 1) path = path.replace(/\/+$/, "");
2088
+ return path || "/";
2089
+ }
2090
+ function normalizeRedirectPath(raw) {
2091
+ return trimInternalPath((decoded(raw) ?? raw).split(/[?#]/, 1)[0] ?? "");
2092
+ }
2093
+ function isProtectedRedirectPath(path) {
2094
+ const normalized = normalizeRedirectPath(path);
2095
+ return PROTECTED_PREFIX.test(normalized) || STATIC_EXTENSION.test(normalized);
2096
+ }
2097
+ function unsafePath(path) {
2098
+ return CONTROL_OR_BACKSLASH.test(path) || path.startsWith("//") || /(?:^|\/)\.{1,2}(?:\/|$)/.test(path) || /[%](?:2f|5c|0[0-9a-f]|1[0-9a-f]|7f)/i.test(path);
2099
+ }
2100
+ function normalizeRedirectSource(raw) {
2101
+ const invalid = { ok: false, code: "invalid_source_path" };
2102
+ if (CONTROL_OR_BACKSLASH.test(raw)) return invalid;
2103
+ const trimmed = raw.trim();
2104
+ if (!trimmed || /^[a-z][a-z\d+.-]*:/i.test(trimmed) || trimmed.startsWith("//")) return invalid;
2105
+ const rawPath = trimmed.split(/[?#]/, 1)[0] ?? "";
2106
+ const path = decoded(rawPath);
2107
+ if (!path || unsafePath(path) || /[?#*]/.test(path)) return invalid;
2108
+ const normalized = trimInternalPath(path);
2109
+ if (isProtectedRedirectPath(normalized)) return invalid;
2110
+ return { ok: true, value: normalized };
2111
+ }
2112
+ function normalizeRedirectTarget(raw) {
2113
+ const invalid = { ok: false, code: "invalid_target" };
2114
+ const decodedRaw = decoded(raw);
2115
+ if (decodedRaw === null || CONTROL_OR_BACKSLASH.test(raw) || CONTROL_OR_BACKSLASH.test(decodedRaw)) return invalid;
2116
+ const trimmed = raw.trim();
2117
+ if (!trimmed || trimmed.startsWith("//")) return invalid;
2118
+ if (trimmed.startsWith("/")) {
2119
+ const suffixIndex = trimmed.search(/[?#]/);
2120
+ const rawPath = suffixIndex === -1 ? trimmed : trimmed.slice(0, suffixIndex);
2121
+ const path = decoded(rawPath);
2122
+ if (!path || unsafePath(path) || /[?#]/.test(path)) return invalid;
2123
+ const suffix = suffixIndex === -1 ? "" : trimmed.slice(suffixIndex);
2124
+ return { ok: true, value: `${trimInternalPath(path)}${suffix}`, kind: "internal" };
2125
+ }
2126
+ try {
2127
+ const url = new URL(trimmed);
2128
+ if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password) return invalid;
2129
+ return { ok: true, value: url.toString(), kind: "external" };
2130
+ } catch {
2131
+ return invalid;
2132
+ }
2133
+ }
1841
2134
  export {
1842
2135
  BLOCK_NAME_PATTERN,
1843
2136
  CMS_FONT_SIZES,
@@ -1854,6 +2147,14 @@ export {
1854
2147
  INTERNAL_CONTENT_LINK_PENDING_CLASS,
1855
2148
  INTERNAL_CONTENT_LINK_TOKEN_SOURCE,
1856
2149
  LEGACY_BLOG_DETAIL_PATH,
2150
+ METADATA_FIELDS,
2151
+ MetadataDefaultRulesSchema,
2152
+ MetadataFieldSchema,
2153
+ MetadataPatchSchema,
2154
+ MetadataRobotsPolicySchema,
2155
+ MetadataTargetKindSchema,
2156
+ MetadataTextRuleSchema,
2157
+ MetadataValuesSchema,
1857
2158
  PHASE_1_CORE_BLOCKS,
1858
2159
  PHASE_1_CSP,
1859
2160
  POST_FOOTER_SLOT,
@@ -1882,6 +2183,7 @@ export {
1882
2183
  findInternalContentLinkTokens,
1883
2184
  findSectionLockViolations,
1884
2185
  findSitePatternPlacementRule,
2186
+ formatMetadataDefaults,
1885
2187
  formatSectionLockViolations,
1886
2188
  htmlToPlainText,
1887
2189
  injectStableIdsTree,
@@ -1891,18 +2193,25 @@ export {
1891
2193
  isAllowedEmbedSrc,
1892
2194
  isAllowedMapEmbedSrc,
1893
2195
  isInCollectionMode,
2196
+ isProtectedRedirectPath,
1894
2197
  isSectionContentPropPath,
1895
2198
  isSitePatternSlotKey,
1896
2199
  isValidBlock,
1897
2200
  isValidSitePatternKey,
1898
2201
  issuePreviewToken,
2202
+ mergeMetadataPatch,
2203
+ metadataContentImageUrl,
1899
2204
  normalizeEmbed,
1900
2205
  normalizeHexColor,
1901
2206
  normalizeImageAlt,
2207
+ normalizeRedirectPath,
2208
+ normalizeRedirectSource,
2209
+ normalizeRedirectTarget,
1902
2210
  parseBlock,
1903
2211
  postsForCollection,
1904
2212
  postsInAnyCollection,
1905
2213
  postsNotInAnyCollection,
2214
+ readMetadataValues,
1906
2215
  readSitePatternPlacements,
1907
2216
  readSitePatternPresentation,
1908
2217
  referencedSitePatternKeys,
@@ -1912,6 +2221,8 @@ export {
1912
2221
  resolveCategoryTreeChain,
1913
2222
  resolveCategoryTreeEntryPath,
1914
2223
  resolveCategoryTreeHubPaths,
2224
+ resolveCategoryTreeSelection,
2225
+ resolveMetadata,
1915
2226
  resolvePatternSlots,
1916
2227
  resolvePostCategorySlug,
1917
2228
  resolvePostCollection,