@roottale/cms-core 0.66.0 → 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
@@ -973,10 +973,10 @@ function scopeImportedCss(css, scope, allowedAssetUrls = []) {
973
973
 
974
974
  // src/internal-content-links.ts
975
975
  var INTERNAL_CONTENT_LINK_TOKEN_SOURCE = String.raw`\[\[internal:([a-z0-9가-힣]+(?:[.-][a-z0-9가-힣]+)*)\|([^\]\n|]+)\]\]`;
976
- function findInternalContentLinkTokens(text) {
976
+ function findInternalContentLinkTokens(text2) {
977
977
  const pattern = new RegExp(INTERNAL_CONTENT_LINK_TOKEN_SOURCE, "giu");
978
978
  const tokens = [];
979
- for (const match of text.matchAll(pattern)) {
979
+ for (const match of text2.matchAll(pattern)) {
980
980
  const [raw, key, label] = match;
981
981
  if (!key || !label || match.index === void 0) continue;
982
982
  tokens.push({
@@ -1035,19 +1035,19 @@ function escapeHtmlText(value) {
1035
1035
  var INTERNAL_CONTENT_LINK_CLASS = "rt-internal-link";
1036
1036
  var INTERNAL_CONTENT_LINK_PENDING_CLASS = "rt-internal-link--pending";
1037
1037
  var INTERNAL_CONTENT_LINK_PENDING_ATTR = "data-rt-internal-link-pending";
1038
- function renderInternalContentLinksInText(text, resolve) {
1039
- const tokens = findInternalContentLinkTokens(text);
1040
- 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 };
1041
1041
  let cursor = 0;
1042
1042
  let html = "";
1043
1043
  for (const token of tokens) {
1044
- html += text.slice(cursor, token.index);
1044
+ html += text2.slice(cursor, token.index);
1045
1045
  const path = resolve(token.key);
1046
1046
  const label = escapeHtmlText(token.label);
1047
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>`;
1048
1048
  cursor = token.index + token.raw.length;
1049
1049
  }
1050
- html += text.slice(cursor);
1050
+ html += text2.slice(cursor);
1051
1051
  return { html, hasTokens: true };
1052
1052
  }
1053
1053
  var INTERNAL_LINK_BLOCKED_HTML_TAGS = /* @__PURE__ */ new Set([
@@ -1081,12 +1081,12 @@ function renderInternalContentLinksInHtml(html, resolve) {
1081
1081
  let cursor = 0;
1082
1082
  let out = "";
1083
1083
  let hasTokens = false;
1084
- const emitText = (text) => {
1085
- if (blockedDepth > 0 || !text) {
1086
- out += text;
1084
+ const emitText = (text2) => {
1085
+ if (blockedDepth > 0 || !text2) {
1086
+ out += text2;
1087
1087
  return;
1088
1088
  }
1089
- const rendered = renderInternalContentLinksInText(text, resolve);
1089
+ const rendered = renderInternalContentLinksInText(text2, resolve);
1090
1090
  hasTokens ||= rendered.hasTokens;
1091
1091
  out += rendered.html;
1092
1092
  };
@@ -1225,8 +1225,8 @@ function fromCodePointSafe2(n) {
1225
1225
  }
1226
1226
  }
1227
1227
  function decodeUrlProbe(value) {
1228
- 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);
1229
- 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) => {
1230
1230
  const n = c.codePointAt(0) ?? 0;
1231
1231
  return n > 32 && n !== 127;
1232
1232
  }).join("").toLowerCase();
@@ -1850,6 +1850,287 @@ function formatSectionLockViolations(violations) {
1850
1850
  return `${label} \uAD6C\uC870 \uBCC0\uACBD(${violation.field})`;
1851
1851
  }).join(" / ");
1852
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
+ }
1853
2134
  export {
1854
2135
  BLOCK_NAME_PATTERN,
1855
2136
  CMS_FONT_SIZES,
@@ -1866,6 +2147,14 @@ export {
1866
2147
  INTERNAL_CONTENT_LINK_PENDING_CLASS,
1867
2148
  INTERNAL_CONTENT_LINK_TOKEN_SOURCE,
1868
2149
  LEGACY_BLOG_DETAIL_PATH,
2150
+ METADATA_FIELDS,
2151
+ MetadataDefaultRulesSchema,
2152
+ MetadataFieldSchema,
2153
+ MetadataPatchSchema,
2154
+ MetadataRobotsPolicySchema,
2155
+ MetadataTargetKindSchema,
2156
+ MetadataTextRuleSchema,
2157
+ MetadataValuesSchema,
1869
2158
  PHASE_1_CORE_BLOCKS,
1870
2159
  PHASE_1_CSP,
1871
2160
  POST_FOOTER_SLOT,
@@ -1894,6 +2183,7 @@ export {
1894
2183
  findInternalContentLinkTokens,
1895
2184
  findSectionLockViolations,
1896
2185
  findSitePatternPlacementRule,
2186
+ formatMetadataDefaults,
1897
2187
  formatSectionLockViolations,
1898
2188
  htmlToPlainText,
1899
2189
  injectStableIdsTree,
@@ -1903,18 +2193,25 @@ export {
1903
2193
  isAllowedEmbedSrc,
1904
2194
  isAllowedMapEmbedSrc,
1905
2195
  isInCollectionMode,
2196
+ isProtectedRedirectPath,
1906
2197
  isSectionContentPropPath,
1907
2198
  isSitePatternSlotKey,
1908
2199
  isValidBlock,
1909
2200
  isValidSitePatternKey,
1910
2201
  issuePreviewToken,
2202
+ mergeMetadataPatch,
2203
+ metadataContentImageUrl,
1911
2204
  normalizeEmbed,
1912
2205
  normalizeHexColor,
1913
2206
  normalizeImageAlt,
2207
+ normalizeRedirectPath,
2208
+ normalizeRedirectSource,
2209
+ normalizeRedirectTarget,
1914
2210
  parseBlock,
1915
2211
  postsForCollection,
1916
2212
  postsInAnyCollection,
1917
2213
  postsNotInAnyCollection,
2214
+ readMetadataValues,
1918
2215
  readSitePatternPlacements,
1919
2216
  readSitePatternPresentation,
1920
2217
  referencedSitePatternKeys,
@@ -1925,6 +2222,7 @@ export {
1925
2222
  resolveCategoryTreeEntryPath,
1926
2223
  resolveCategoryTreeHubPaths,
1927
2224
  resolveCategoryTreeSelection,
2225
+ resolveMetadata,
1928
2226
  resolvePatternSlots,
1929
2227
  resolvePostCategorySlug,
1930
2228
  resolvePostCollection,