@roottale/cms-core 0.66.0 → 0.67.1
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/CHANGELOG.md +8 -0
- package/dist/index.cjs +328 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +547 -1
- package/dist/index.d.ts +547 -1
- package/dist/index.js +311 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @roottale/cms-core
|
|
2
2
|
|
|
3
|
+
## 0.67.1
|
|
4
|
+
|
|
5
|
+
## 0.67.0
|
|
6
|
+
|
|
7
|
+
### Minor Changes
|
|
8
|
+
|
|
9
|
+
- 30d26ac: 공통 메타데이터 해석기·고유 경로 조회·Next SSR 빌더와 캐시 태그를 추가합니다. 기존 canonical과 강제 robots 정책을 보존하고 공유 이미지 대체 텍스트를 OG·X에 동일하게 적용합니다. 분류의 공유 전용 설정과 메타데이터 조회·수정 MCP 도구, Pro 하향 후 공개 적용 유지 계약을 문서와 예제에 반영합니다. 분류 색인 전환을 선언한 사이트가 공개 분류 응답의 `category_index_activated`를 확인한 뒤에만 분류 검색 제외를 적용하도록 안내합니다.
|
|
10
|
+
|
|
3
11
|
## 0.66.0
|
|
4
12
|
|
|
5
13
|
### Minor Changes
|
package/dist/index.cjs
CHANGED
|
@@ -45,6 +45,14 @@ __export(src_exports, {
|
|
|
45
45
|
INTERNAL_CONTENT_LINK_PENDING_CLASS: () => INTERNAL_CONTENT_LINK_PENDING_CLASS,
|
|
46
46
|
INTERNAL_CONTENT_LINK_TOKEN_SOURCE: () => INTERNAL_CONTENT_LINK_TOKEN_SOURCE,
|
|
47
47
|
LEGACY_BLOG_DETAIL_PATH: () => LEGACY_BLOG_DETAIL_PATH,
|
|
48
|
+
METADATA_FIELDS: () => METADATA_FIELDS,
|
|
49
|
+
MetadataDefaultRulesSchema: () => MetadataDefaultRulesSchema,
|
|
50
|
+
MetadataFieldSchema: () => MetadataFieldSchema,
|
|
51
|
+
MetadataPatchSchema: () => MetadataPatchSchema,
|
|
52
|
+
MetadataRobotsPolicySchema: () => MetadataRobotsPolicySchema,
|
|
53
|
+
MetadataTargetKindSchema: () => MetadataTargetKindSchema,
|
|
54
|
+
MetadataTextRuleSchema: () => MetadataTextRuleSchema,
|
|
55
|
+
MetadataValuesSchema: () => MetadataValuesSchema,
|
|
48
56
|
PHASE_1_CORE_BLOCKS: () => PHASE_1_CORE_BLOCKS,
|
|
49
57
|
PHASE_1_CSP: () => PHASE_1_CSP,
|
|
50
58
|
POST_FOOTER_SLOT: () => POST_FOOTER_SLOT,
|
|
@@ -73,6 +81,7 @@ __export(src_exports, {
|
|
|
73
81
|
findInternalContentLinkTokens: () => findInternalContentLinkTokens,
|
|
74
82
|
findSectionLockViolations: () => findSectionLockViolations,
|
|
75
83
|
findSitePatternPlacementRule: () => findSitePatternPlacementRule,
|
|
84
|
+
formatMetadataDefaults: () => formatMetadataDefaults,
|
|
76
85
|
formatSectionLockViolations: () => formatSectionLockViolations,
|
|
77
86
|
htmlToPlainText: () => htmlToPlainText,
|
|
78
87
|
injectStableIdsTree: () => injectStableIdsTree,
|
|
@@ -82,18 +91,25 @@ __export(src_exports, {
|
|
|
82
91
|
isAllowedEmbedSrc: () => isAllowedEmbedSrc,
|
|
83
92
|
isAllowedMapEmbedSrc: () => isAllowedMapEmbedSrc,
|
|
84
93
|
isInCollectionMode: () => isInCollectionMode,
|
|
94
|
+
isProtectedRedirectPath: () => isProtectedRedirectPath,
|
|
85
95
|
isSectionContentPropPath: () => isSectionContentPropPath,
|
|
86
96
|
isSitePatternSlotKey: () => isSitePatternSlotKey,
|
|
87
97
|
isValidBlock: () => isValidBlock,
|
|
88
98
|
isValidSitePatternKey: () => isValidSitePatternKey,
|
|
89
99
|
issuePreviewToken: () => issuePreviewToken,
|
|
100
|
+
mergeMetadataPatch: () => mergeMetadataPatch,
|
|
101
|
+
metadataContentImageUrl: () => metadataContentImageUrl,
|
|
90
102
|
normalizeEmbed: () => normalizeEmbed,
|
|
91
103
|
normalizeHexColor: () => normalizeHexColor,
|
|
92
104
|
normalizeImageAlt: () => normalizeImageAlt,
|
|
105
|
+
normalizeRedirectPath: () => normalizeRedirectPath,
|
|
106
|
+
normalizeRedirectSource: () => normalizeRedirectSource,
|
|
107
|
+
normalizeRedirectTarget: () => normalizeRedirectTarget,
|
|
93
108
|
parseBlock: () => parseBlock,
|
|
94
109
|
postsForCollection: () => postsForCollection,
|
|
95
110
|
postsInAnyCollection: () => postsInAnyCollection,
|
|
96
111
|
postsNotInAnyCollection: () => postsNotInAnyCollection,
|
|
112
|
+
readMetadataValues: () => readMetadataValues,
|
|
97
113
|
readSitePatternPlacements: () => readSitePatternPlacements,
|
|
98
114
|
readSitePatternPresentation: () => readSitePatternPresentation,
|
|
99
115
|
referencedSitePatternKeys: () => referencedSitePatternKeys,
|
|
@@ -104,6 +120,7 @@ __export(src_exports, {
|
|
|
104
120
|
resolveCategoryTreeEntryPath: () => resolveCategoryTreeEntryPath,
|
|
105
121
|
resolveCategoryTreeHubPaths: () => resolveCategoryTreeHubPaths,
|
|
106
122
|
resolveCategoryTreeSelection: () => resolveCategoryTreeSelection,
|
|
123
|
+
resolveMetadata: () => resolveMetadata,
|
|
107
124
|
resolvePatternSlots: () => resolvePatternSlots,
|
|
108
125
|
resolvePostCategorySlug: () => resolvePostCategorySlug,
|
|
109
126
|
resolvePostCollection: () => resolvePostCollection,
|
|
@@ -1098,10 +1115,10 @@ function scopeImportedCss(css, scope, allowedAssetUrls = []) {
|
|
|
1098
1115
|
|
|
1099
1116
|
// src/internal-content-links.ts
|
|
1100
1117
|
var INTERNAL_CONTENT_LINK_TOKEN_SOURCE = String.raw`\[\[internal:([a-z0-9가-힣]+(?:[.-][a-z0-9가-힣]+)*)\|([^\]\n|]+)\]\]`;
|
|
1101
|
-
function findInternalContentLinkTokens(
|
|
1118
|
+
function findInternalContentLinkTokens(text2) {
|
|
1102
1119
|
const pattern = new RegExp(INTERNAL_CONTENT_LINK_TOKEN_SOURCE, "giu");
|
|
1103
1120
|
const tokens = [];
|
|
1104
|
-
for (const match of
|
|
1121
|
+
for (const match of text2.matchAll(pattern)) {
|
|
1105
1122
|
const [raw, key, label] = match;
|
|
1106
1123
|
if (!key || !label || match.index === void 0) continue;
|
|
1107
1124
|
tokens.push({
|
|
@@ -1160,19 +1177,19 @@ function escapeHtmlText(value) {
|
|
|
1160
1177
|
var INTERNAL_CONTENT_LINK_CLASS = "rt-internal-link";
|
|
1161
1178
|
var INTERNAL_CONTENT_LINK_PENDING_CLASS = "rt-internal-link--pending";
|
|
1162
1179
|
var INTERNAL_CONTENT_LINK_PENDING_ATTR = "data-rt-internal-link-pending";
|
|
1163
|
-
function renderInternalContentLinksInText(
|
|
1164
|
-
const tokens = findInternalContentLinkTokens(
|
|
1165
|
-
if (tokens.length === 0) return { html:
|
|
1180
|
+
function renderInternalContentLinksInText(text2, resolve) {
|
|
1181
|
+
const tokens = findInternalContentLinkTokens(text2);
|
|
1182
|
+
if (tokens.length === 0) return { html: text2, hasTokens: false };
|
|
1166
1183
|
let cursor = 0;
|
|
1167
1184
|
let html = "";
|
|
1168
1185
|
for (const token of tokens) {
|
|
1169
|
-
html +=
|
|
1186
|
+
html += text2.slice(cursor, token.index);
|
|
1170
1187
|
const path = resolve(token.key);
|
|
1171
1188
|
const label = escapeHtmlText(token.label);
|
|
1172
1189
|
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>`;
|
|
1173
1190
|
cursor = token.index + token.raw.length;
|
|
1174
1191
|
}
|
|
1175
|
-
html +=
|
|
1192
|
+
html += text2.slice(cursor);
|
|
1176
1193
|
return { html, hasTokens: true };
|
|
1177
1194
|
}
|
|
1178
1195
|
var INTERNAL_LINK_BLOCKED_HTML_TAGS = /* @__PURE__ */ new Set([
|
|
@@ -1206,12 +1223,12 @@ function renderInternalContentLinksInHtml(html, resolve) {
|
|
|
1206
1223
|
let cursor = 0;
|
|
1207
1224
|
let out = "";
|
|
1208
1225
|
let hasTokens = false;
|
|
1209
|
-
const emitText = (
|
|
1210
|
-
if (blockedDepth > 0 || !
|
|
1211
|
-
out +=
|
|
1226
|
+
const emitText = (text2) => {
|
|
1227
|
+
if (blockedDepth > 0 || !text2) {
|
|
1228
|
+
out += text2;
|
|
1212
1229
|
return;
|
|
1213
1230
|
}
|
|
1214
|
-
const rendered = renderInternalContentLinksInText(
|
|
1231
|
+
const rendered = renderInternalContentLinksInText(text2, resolve);
|
|
1215
1232
|
hasTokens ||= rendered.hasTokens;
|
|
1216
1233
|
out += rendered.html;
|
|
1217
1234
|
};
|
|
@@ -1350,8 +1367,8 @@ function fromCodePointSafe2(n) {
|
|
|
1350
1367
|
}
|
|
1351
1368
|
}
|
|
1352
1369
|
function decodeUrlProbe(value) {
|
|
1353
|
-
const
|
|
1354
|
-
return [...
|
|
1370
|
+
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);
|
|
1371
|
+
return [...decoded2].filter((c) => {
|
|
1355
1372
|
const n = c.codePointAt(0) ?? 0;
|
|
1356
1373
|
return n > 32 && n !== 127;
|
|
1357
1374
|
}).join("").toLowerCase();
|
|
@@ -1975,6 +1992,287 @@ function formatSectionLockViolations(violations) {
|
|
|
1975
1992
|
return `${label} \uAD6C\uC870 \uBCC0\uACBD(${violation.field})`;
|
|
1976
1993
|
}).join(" / ");
|
|
1977
1994
|
}
|
|
1995
|
+
|
|
1996
|
+
// src/metadata-resolver.ts
|
|
1997
|
+
function text(value) {
|
|
1998
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
1999
|
+
}
|
|
2000
|
+
function imageUrl(value, base) {
|
|
2001
|
+
const candidate = text(value);
|
|
2002
|
+
if (!candidate || candidate.startsWith("//") || /[\\\u0000-\u0020\u007f]/u.test(candidate)) return void 0;
|
|
2003
|
+
if (candidate.startsWith("/") && !base) return candidate;
|
|
2004
|
+
try {
|
|
2005
|
+
const url = new URL(candidate, base);
|
|
2006
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) return void 0;
|
|
2007
|
+
return url.href;
|
|
2008
|
+
} catch {
|
|
2009
|
+
return void 0;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
function resolveMetadata(input) {
|
|
2013
|
+
const layers = [input.overrides, input.defaults, input.siteDefaults, input.fallback];
|
|
2014
|
+
const result = {};
|
|
2015
|
+
for (const field of ["title", "description", "ogImageAlt"]) {
|
|
2016
|
+
for (const layer of layers) {
|
|
2017
|
+
const value = text(layer?.[field]);
|
|
2018
|
+
if (value !== void 0) {
|
|
2019
|
+
result[field] = value;
|
|
2020
|
+
break;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
for (const layer of layers) {
|
|
2025
|
+
const value = imageUrl(layer?.ogImage, input.imageBaseUrl);
|
|
2026
|
+
if (value !== void 0) {
|
|
2027
|
+
result.ogImage = value;
|
|
2028
|
+
break;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
for (const field of ["noindex", "nofollow"]) {
|
|
2032
|
+
for (const layer of layers) {
|
|
2033
|
+
const value = layer?.[field];
|
|
2034
|
+
if (typeof value === "boolean") {
|
|
2035
|
+
result[field] = value;
|
|
2036
|
+
break;
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
if (input.robots?.forceNoindex) result.noindex = true;
|
|
2041
|
+
if (input.robots?.forceNofollow) result.nofollow = true;
|
|
2042
|
+
return result;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
// src/metadata-contract.ts
|
|
2046
|
+
var import_zod2 = require("zod");
|
|
2047
|
+
var METADATA_FIELDS = ["title", "description", "ogImage", "ogImageAlt", "noindex", "nofollow"];
|
|
2048
|
+
var MetadataFieldSchema = import_zod2.z.enum(METADATA_FIELDS);
|
|
2049
|
+
var MetadataTargetKindSchema = import_zod2.z.enum(["route", "post", "term"]);
|
|
2050
|
+
var textValue = (max) => import_zod2.z.string().trim().min(1).max(max);
|
|
2051
|
+
var imageValue = textValue(2048).refine((value) => {
|
|
2052
|
+
if (/[\\\u0000-\u0020\u007f]/u.test(value) || value.startsWith("//")) return false;
|
|
2053
|
+
if (value.startsWith("/")) return true;
|
|
2054
|
+
try {
|
|
2055
|
+
const url = new URL(value);
|
|
2056
|
+
return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password;
|
|
2057
|
+
} catch {
|
|
2058
|
+
return false;
|
|
2059
|
+
}
|
|
2060
|
+
}, "\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");
|
|
2061
|
+
var metadataFields = {
|
|
2062
|
+
// Existing content titles (500) plus a site title template (400) remain valid automatic values.
|
|
2063
|
+
title: textValue(1e3),
|
|
2064
|
+
description: textValue(2e3),
|
|
2065
|
+
ogImage: imageValue,
|
|
2066
|
+
// Automatic alt text may use the complete automatic title and its site suffix.
|
|
2067
|
+
ogImageAlt: textValue(1e3),
|
|
2068
|
+
noindex: import_zod2.z.boolean(),
|
|
2069
|
+
nofollow: import_zod2.z.boolean()
|
|
2070
|
+
};
|
|
2071
|
+
var MetadataValuesSchema = import_zod2.z.object(metadataFields).partial().strict();
|
|
2072
|
+
var MetadataPatchSchema = import_zod2.z.object({
|
|
2073
|
+
title: textValue(300).nullable().optional(),
|
|
2074
|
+
description: metadataFields.description.nullable().optional(),
|
|
2075
|
+
ogImage: metadataFields.ogImage.nullable().optional(),
|
|
2076
|
+
ogImageAlt: textValue(500).nullable().optional(),
|
|
2077
|
+
noindex: metadataFields.noindex.nullable().optional(),
|
|
2078
|
+
nofollow: metadataFields.nofollow.nullable().optional()
|
|
2079
|
+
}).strict();
|
|
2080
|
+
var MetadataRobotsPolicySchema = import_zod2.z.object({
|
|
2081
|
+
forceNoindex: import_zod2.z.boolean().optional(),
|
|
2082
|
+
forceNofollow: import_zod2.z.boolean().optional()
|
|
2083
|
+
}).strict();
|
|
2084
|
+
function readMetadataValues(input) {
|
|
2085
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return {};
|
|
2086
|
+
const record = input;
|
|
2087
|
+
const output = {};
|
|
2088
|
+
for (const field of METADATA_FIELDS) {
|
|
2089
|
+
const parsed = metadataFields[field].safeParse(record[field]);
|
|
2090
|
+
if (parsed.success) output[field] = parsed.data;
|
|
2091
|
+
}
|
|
2092
|
+
return MetadataValuesSchema.parse(output);
|
|
2093
|
+
}
|
|
2094
|
+
function mergeMetadataPatch(current, input) {
|
|
2095
|
+
const patch = MetadataPatchSchema.parse(input);
|
|
2096
|
+
const next = { ...current };
|
|
2097
|
+
for (const field of METADATA_FIELDS) {
|
|
2098
|
+
if (patch[field] === null) delete next[field];
|
|
2099
|
+
else if (patch[field] !== void 0) next[field] = patch[field];
|
|
2100
|
+
}
|
|
2101
|
+
return next;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
// src/metadata-defaults.ts
|
|
2105
|
+
var import_zod3 = require("zod");
|
|
2106
|
+
var fallbackText = import_zod3.z.string().max(2e3);
|
|
2107
|
+
var imageOrigin = import_zod3.z.string().max(2048).refine((value) => {
|
|
2108
|
+
try {
|
|
2109
|
+
const url = new URL(value);
|
|
2110
|
+
return url.protocol === "https:" && url.origin === value;
|
|
2111
|
+
} catch {
|
|
2112
|
+
return false;
|
|
2113
|
+
}
|
|
2114
|
+
}, "\uC774\uBBF8\uC9C0 \uD5C8\uC6A9 \uBAA9\uB85D\uC5D0\uB294 \uACBD\uB85C \uC5C6\uB294 HTTPS origin\uB9CC \uC120\uC5B8\uD569\uB2C8\uB2E4");
|
|
2115
|
+
var MetadataTextRuleSchema = import_zod3.z.object({
|
|
2116
|
+
source: import_zod3.z.enum(["content", "fallback"]).optional(),
|
|
2117
|
+
stripLeadingLabel: import_zod3.z.enum(["question", "answer"]).optional(),
|
|
2118
|
+
replaceLineBreakTokens: import_zod3.z.boolean().optional(),
|
|
2119
|
+
removeEmDashes: import_zod3.z.boolean().optional(),
|
|
2120
|
+
normalizeWhitespace: import_zod3.z.boolean().optional(),
|
|
2121
|
+
maxLength: import_zod3.z.number().int().min(1).max(2e3).optional(),
|
|
2122
|
+
ellipsis: import_zod3.z.string().max(10).optional(),
|
|
2123
|
+
fallback: fallbackText.optional(),
|
|
2124
|
+
fallbackWithParent: fallbackText.optional(),
|
|
2125
|
+
fallbackWithoutParent: fallbackText.optional(),
|
|
2126
|
+
fallbackBySlug: import_zod3.z.record(import_zod3.z.string().max(200), fallbackText).optional()
|
|
2127
|
+
}).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");
|
|
2128
|
+
var MetadataDefaultRulesSchema = import_zod3.z.object({
|
|
2129
|
+
title: MetadataTextRuleSchema.optional(),
|
|
2130
|
+
description: MetadataTextRuleSchema.optional(),
|
|
2131
|
+
useFirstBodyImage: import_zod3.z.boolean().optional(),
|
|
2132
|
+
useTermImage: import_zod3.z.boolean().optional(),
|
|
2133
|
+
imageAltSource: import_zod3.z.enum(["content", "title"]).optional(),
|
|
2134
|
+
legacyImageSource: import_zod3.z.literal("legacy-thumbnail").optional(),
|
|
2135
|
+
imageAllowedOrigins: import_zod3.z.array(imageOrigin).min(1).max(32).optional(),
|
|
2136
|
+
legacyImageAllowedOrigins: import_zod3.z.array(imageOrigin).min(1).max(32).optional(),
|
|
2137
|
+
normalizeCloudflareImages: import_zod3.z.boolean().optional(),
|
|
2138
|
+
fallback: MetadataValuesSchema.optional()
|
|
2139
|
+
}).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");
|
|
2140
|
+
function cleanText(value, rule) {
|
|
2141
|
+
let text2 = value.trim();
|
|
2142
|
+
if (rule?.stripLeadingLabel === "question") text2 = text2.replace(/^\s*Q[..:]?\s*/iu, "");
|
|
2143
|
+
if (rule?.stripLeadingLabel === "answer") text2 = text2.replace(/^\s*A[..:]?\s*/iu, "");
|
|
2144
|
+
if (rule?.replaceLineBreakTokens) text2 = text2.replace(/\[\[br(?::[^\]]*)?\]\]/giu, " ");
|
|
2145
|
+
if (rule?.removeEmDashes) text2 = text2.replaceAll("\u2014", "");
|
|
2146
|
+
if (rule?.normalizeWhitespace) text2 = text2.replace(/\s+/gu, " ");
|
|
2147
|
+
return text2.trim();
|
|
2148
|
+
}
|
|
2149
|
+
function formatText(value, rule, input, name) {
|
|
2150
|
+
let text2 = rule?.source === "fallback" ? "" : cleanText(value ?? "", rule);
|
|
2151
|
+
if (!text2) {
|
|
2152
|
+
const fallback = input.parentId ? rule?.fallbackWithParent ?? rule?.fallback : (input.slug ? rule?.fallbackBySlug?.[input.slug] : void 0) ?? rule?.fallbackWithoutParent ?? rule?.fallback;
|
|
2153
|
+
text2 = cleanText((fallback ?? "").replaceAll("%s", () => name), rule);
|
|
2154
|
+
}
|
|
2155
|
+
if (!text2) return void 0;
|
|
2156
|
+
if (rule?.maxLength && text2.length > rule.maxLength) {
|
|
2157
|
+
const suffix = rule.ellipsis ?? "";
|
|
2158
|
+
const head = text2.slice(0, rule.maxLength - suffix.length);
|
|
2159
|
+
return suffix ? head.trimEnd() + suffix : head;
|
|
2160
|
+
}
|
|
2161
|
+
return text2;
|
|
2162
|
+
}
|
|
2163
|
+
function metadataContentImageUrl(value, normalizeCloudflareImages = false) {
|
|
2164
|
+
if (!normalizeCloudflareImages) return value;
|
|
2165
|
+
try {
|
|
2166
|
+
const url = new URL(value);
|
|
2167
|
+
const [account, id, variant] = url.pathname.split("/").filter(Boolean);
|
|
2168
|
+
if (url.protocol === "https:" && url.hostname === "imagedelivery.net" && account && id && variant) {
|
|
2169
|
+
url.pathname = `/${account}/${id}/lg`;
|
|
2170
|
+
return url.href;
|
|
2171
|
+
}
|
|
2172
|
+
} catch {
|
|
2173
|
+
}
|
|
2174
|
+
return value;
|
|
2175
|
+
}
|
|
2176
|
+
function legacyImageUrl(value, rule) {
|
|
2177
|
+
if (!value || rule?.legacyImageSource !== "legacy-thumbnail") return void 0;
|
|
2178
|
+
try {
|
|
2179
|
+
const url = new URL(value);
|
|
2180
|
+
if (!rule.legacyImageAllowedOrigins?.includes(url.origin)) return void 0;
|
|
2181
|
+
return readMetadataValues({ ogImage: url.href }).ogImage;
|
|
2182
|
+
} catch {
|
|
2183
|
+
return void 0;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
function contentCandidateAllowed(value, input, rule) {
|
|
2187
|
+
if (!rule?.imageAllowedOrigins) return true;
|
|
2188
|
+
if (value.startsWith("/") && !value.startsWith("//")) return true;
|
|
2189
|
+
try {
|
|
2190
|
+
const url = new URL(value);
|
|
2191
|
+
if (url.protocol !== "https:") return false;
|
|
2192
|
+
const origins = [...rule.imageAllowedOrigins];
|
|
2193
|
+
if (input.imageBaseUrl) origins.push(new URL(input.imageBaseUrl).origin);
|
|
2194
|
+
return origins.includes(url.origin);
|
|
2195
|
+
} catch {
|
|
2196
|
+
return false;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
function formatMetadataDefaults(input, declaration) {
|
|
2200
|
+
const dynamic = declaration[input.kind];
|
|
2201
|
+
const rule = dynamic?.defaultRules?.[input.modelKey];
|
|
2202
|
+
const name = cleanText(input.title, rule?.title);
|
|
2203
|
+
const title = formatText(input.title, rule?.title, input, name);
|
|
2204
|
+
const template = dynamic?.titleTemplates?.[input.modelKey];
|
|
2205
|
+
const description = formatText(input.description, rule?.description, input, name);
|
|
2206
|
+
const bodyImage = input.firstBodyImage && (input.firstBodyImage.startsWith("https://") || /^\/(?!\/)/u.test(input.firstBodyImage)) ? input.firstBodyImage : void 0;
|
|
2207
|
+
const candidateImage = input.kind === "terms" && rule?.useTermImage === false ? void 0 : input.featuredImage || (rule?.useFirstBodyImage ? bodyImage : void 0) || legacyImageUrl(input.legacyImage, rule);
|
|
2208
|
+
const ogImage = candidateImage && contentCandidateAllowed(candidateImage, input, rule) ? readMetadataValues({ ogImage: metadataContentImageUrl(candidateImage, rule?.normalizeCloudflareImages) }).ogImage : void 0;
|
|
2209
|
+
return {
|
|
2210
|
+
...title ? { title: template ? template.replace("%s", () => title) : title } : {},
|
|
2211
|
+
...description ? { description } : {},
|
|
2212
|
+
...ogImage ? { ogImage } : {}
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
// src/redirect-paths.ts
|
|
2217
|
+
var CONTROL_OR_BACKSLASH = /[\\\u0000-\u001f\u007f]/u;
|
|
2218
|
+
var PROTECTED_PREFIX = /^\/(?:api|_next|_vercel|auth|admin|login|logout|revalidate|preview|assets|static|images|fonts|\.well-known)(?:\/|$)/i;
|
|
2219
|
+
var STATIC_EXTENSION = /\.(?:ico|png|jpe?g|gif|svg|webp|avif|css|js|txt|xml|json|woff2?|map)$/i;
|
|
2220
|
+
function decoded(value) {
|
|
2221
|
+
try {
|
|
2222
|
+
return decodeURIComponent(value);
|
|
2223
|
+
} catch {
|
|
2224
|
+
return null;
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
function trimInternalPath(path) {
|
|
2228
|
+
if (!path.startsWith("/")) path = `/${path}`;
|
|
2229
|
+
if (path.length > 1) path = path.replace(/\/+$/, "");
|
|
2230
|
+
return path || "/";
|
|
2231
|
+
}
|
|
2232
|
+
function normalizeRedirectPath(raw) {
|
|
2233
|
+
return trimInternalPath((decoded(raw) ?? raw).split(/[?#]/, 1)[0] ?? "");
|
|
2234
|
+
}
|
|
2235
|
+
function isProtectedRedirectPath(path) {
|
|
2236
|
+
const normalized = normalizeRedirectPath(path);
|
|
2237
|
+
return PROTECTED_PREFIX.test(normalized) || STATIC_EXTENSION.test(normalized);
|
|
2238
|
+
}
|
|
2239
|
+
function unsafePath(path) {
|
|
2240
|
+
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);
|
|
2241
|
+
}
|
|
2242
|
+
function normalizeRedirectSource(raw) {
|
|
2243
|
+
const invalid = { ok: false, code: "invalid_source_path" };
|
|
2244
|
+
if (CONTROL_OR_BACKSLASH.test(raw)) return invalid;
|
|
2245
|
+
const trimmed = raw.trim();
|
|
2246
|
+
if (!trimmed || /^[a-z][a-z\d+.-]*:/i.test(trimmed) || trimmed.startsWith("//")) return invalid;
|
|
2247
|
+
const rawPath = trimmed.split(/[?#]/, 1)[0] ?? "";
|
|
2248
|
+
const path = decoded(rawPath);
|
|
2249
|
+
if (!path || unsafePath(path) || /[?#*]/.test(path)) return invalid;
|
|
2250
|
+
const normalized = trimInternalPath(path);
|
|
2251
|
+
if (isProtectedRedirectPath(normalized)) return invalid;
|
|
2252
|
+
return { ok: true, value: normalized };
|
|
2253
|
+
}
|
|
2254
|
+
function normalizeRedirectTarget(raw) {
|
|
2255
|
+
const invalid = { ok: false, code: "invalid_target" };
|
|
2256
|
+
const decodedRaw = decoded(raw);
|
|
2257
|
+
if (decodedRaw === null || CONTROL_OR_BACKSLASH.test(raw) || CONTROL_OR_BACKSLASH.test(decodedRaw)) return invalid;
|
|
2258
|
+
const trimmed = raw.trim();
|
|
2259
|
+
if (!trimmed || trimmed.startsWith("//")) return invalid;
|
|
2260
|
+
if (trimmed.startsWith("/")) {
|
|
2261
|
+
const suffixIndex = trimmed.search(/[?#]/);
|
|
2262
|
+
const rawPath = suffixIndex === -1 ? trimmed : trimmed.slice(0, suffixIndex);
|
|
2263
|
+
const path = decoded(rawPath);
|
|
2264
|
+
if (!path || unsafePath(path) || /[?#]/.test(path)) return invalid;
|
|
2265
|
+
const suffix = suffixIndex === -1 ? "" : trimmed.slice(suffixIndex);
|
|
2266
|
+
return { ok: true, value: `${trimInternalPath(path)}${suffix}`, kind: "internal" };
|
|
2267
|
+
}
|
|
2268
|
+
try {
|
|
2269
|
+
const url = new URL(trimmed);
|
|
2270
|
+
if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password) return invalid;
|
|
2271
|
+
return { ok: true, value: url.toString(), kind: "external" };
|
|
2272
|
+
} catch {
|
|
2273
|
+
return invalid;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
1978
2276
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1979
2277
|
0 && (module.exports = {
|
|
1980
2278
|
BLOCK_NAME_PATTERN,
|
|
@@ -1992,6 +2290,14 @@ function formatSectionLockViolations(violations) {
|
|
|
1992
2290
|
INTERNAL_CONTENT_LINK_PENDING_CLASS,
|
|
1993
2291
|
INTERNAL_CONTENT_LINK_TOKEN_SOURCE,
|
|
1994
2292
|
LEGACY_BLOG_DETAIL_PATH,
|
|
2293
|
+
METADATA_FIELDS,
|
|
2294
|
+
MetadataDefaultRulesSchema,
|
|
2295
|
+
MetadataFieldSchema,
|
|
2296
|
+
MetadataPatchSchema,
|
|
2297
|
+
MetadataRobotsPolicySchema,
|
|
2298
|
+
MetadataTargetKindSchema,
|
|
2299
|
+
MetadataTextRuleSchema,
|
|
2300
|
+
MetadataValuesSchema,
|
|
1995
2301
|
PHASE_1_CORE_BLOCKS,
|
|
1996
2302
|
PHASE_1_CSP,
|
|
1997
2303
|
POST_FOOTER_SLOT,
|
|
@@ -2020,6 +2326,7 @@ function formatSectionLockViolations(violations) {
|
|
|
2020
2326
|
findInternalContentLinkTokens,
|
|
2021
2327
|
findSectionLockViolations,
|
|
2022
2328
|
findSitePatternPlacementRule,
|
|
2329
|
+
formatMetadataDefaults,
|
|
2023
2330
|
formatSectionLockViolations,
|
|
2024
2331
|
htmlToPlainText,
|
|
2025
2332
|
injectStableIdsTree,
|
|
@@ -2029,18 +2336,25 @@ function formatSectionLockViolations(violations) {
|
|
|
2029
2336
|
isAllowedEmbedSrc,
|
|
2030
2337
|
isAllowedMapEmbedSrc,
|
|
2031
2338
|
isInCollectionMode,
|
|
2339
|
+
isProtectedRedirectPath,
|
|
2032
2340
|
isSectionContentPropPath,
|
|
2033
2341
|
isSitePatternSlotKey,
|
|
2034
2342
|
isValidBlock,
|
|
2035
2343
|
isValidSitePatternKey,
|
|
2036
2344
|
issuePreviewToken,
|
|
2345
|
+
mergeMetadataPatch,
|
|
2346
|
+
metadataContentImageUrl,
|
|
2037
2347
|
normalizeEmbed,
|
|
2038
2348
|
normalizeHexColor,
|
|
2039
2349
|
normalizeImageAlt,
|
|
2350
|
+
normalizeRedirectPath,
|
|
2351
|
+
normalizeRedirectSource,
|
|
2352
|
+
normalizeRedirectTarget,
|
|
2040
2353
|
parseBlock,
|
|
2041
2354
|
postsForCollection,
|
|
2042
2355
|
postsInAnyCollection,
|
|
2043
2356
|
postsNotInAnyCollection,
|
|
2357
|
+
readMetadataValues,
|
|
2044
2358
|
readSitePatternPlacements,
|
|
2045
2359
|
readSitePatternPresentation,
|
|
2046
2360
|
referencedSitePatternKeys,
|
|
@@ -2051,6 +2365,7 @@ function formatSectionLockViolations(violations) {
|
|
|
2051
2365
|
resolveCategoryTreeEntryPath,
|
|
2052
2366
|
resolveCategoryTreeHubPaths,
|
|
2053
2367
|
resolveCategoryTreeSelection,
|
|
2368
|
+
resolveMetadata,
|
|
2054
2369
|
resolvePatternSlots,
|
|
2055
2370
|
resolvePostCategorySlug,
|
|
2056
2371
|
resolvePostCollection,
|