@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/CHANGELOG.md +19 -0
- package/dist/index.cjs +354 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +568 -4
- package/dist/index.d.ts +568 -4
- package/dist/index.js +336 -25
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @roottale/cms-core
|
|
2
2
|
|
|
3
|
+
## 0.67.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 30d26ac: 공통 메타데이터 해석기·고유 경로 조회·Next SSR 빌더와 캐시 태그를 추가합니다. 기존 canonical과 강제 robots 정책을 보존하고 공유 이미지 대체 텍스트를 OG·X에 동일하게 적용합니다. 분류의 공유 전용 설정과 메타데이터 조회·수정 MCP 도구, Pro 하향 후 공개 적용 유지 계약을 문서와 예제에 반영합니다. 분류 색인 전환을 선언한 사이트가 공개 분류 응답의 `category_index_activated`를 확인한 뒤에만 분류 검색 제외를 적용하도록 안내합니다.
|
|
8
|
+
|
|
9
|
+
## 0.66.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- a58c5d2: 계층형 분류 모델(`category_tree`)에 `entryCategory: "leaf"`를 추가한다. 하위 분류가 없는
|
|
14
|
+
분류면 단계와 무관하게 글을 붙일 수 있고(`categoryDepth`는 최대 단계), 새로 고를 때는
|
|
15
|
+
`resolveCategoryTreeSelection`으로 하위가 없는지 확인한다. 생략하면 기존처럼
|
|
16
|
+
`categoryDepth` 단계 말단에만 붙는다.
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- d5ef48e: `basePath: "/"`인 계층형 분류 모델(`category_tree`)도 허브·상세 주소를 계산합니다. 이전에는 루트를 빈 basePath(주소 없음)와 같게 보아 `resolveCategoryTreeHubPaths`·`resolveCategoryTreeEntryPath`·`resolvePostPublicPath`가 `null`을 돌려주었고, 웹훅에 상세 경로가 빠져 글 상세 캐시가 갱신되지 않았습니다.
|
|
21
|
+
|
|
3
22
|
## 0.65.1
|
|
4
23
|
|
|
5
24
|
## 0.65.0
|
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,
|
|
@@ -103,6 +119,8 @@ __export(src_exports, {
|
|
|
103
119
|
resolveCategoryTreeChain: () => resolveCategoryTreeChain,
|
|
104
120
|
resolveCategoryTreeEntryPath: () => resolveCategoryTreeEntryPath,
|
|
105
121
|
resolveCategoryTreeHubPaths: () => resolveCategoryTreeHubPaths,
|
|
122
|
+
resolveCategoryTreeSelection: () => resolveCategoryTreeSelection,
|
|
123
|
+
resolveMetadata: () => resolveMetadata,
|
|
106
124
|
resolvePatternSlots: () => resolvePatternSlots,
|
|
107
125
|
resolvePostCategorySlug: () => resolvePostCategorySlug,
|
|
108
126
|
resolvePostCollection: () => resolvePostCollection,
|
|
@@ -349,39 +367,51 @@ function validateCollections(collections) {
|
|
|
349
367
|
}
|
|
350
368
|
|
|
351
369
|
// src/category-tree.ts
|
|
352
|
-
function
|
|
370
|
+
function basePathPrefix(basePath) {
|
|
371
|
+
if (!basePath.trim()) return null;
|
|
353
372
|
return basePath.replace(/\/+$/, "");
|
|
354
373
|
}
|
|
355
|
-
function
|
|
356
|
-
if (!Number.isInteger(
|
|
357
|
-
return null;
|
|
358
|
-
}
|
|
374
|
+
function categoryTreeAncestry(categoryDepth, termId, terms) {
|
|
375
|
+
if (!Number.isInteger(categoryDepth) || categoryDepth < 1) return null;
|
|
359
376
|
const termsById = new Map(terms.map((term) => [term.id, term]));
|
|
360
377
|
if (termsById.size !== terms.length) return null;
|
|
361
|
-
if (terms.some((term) => term.parentId === leafId)) return null;
|
|
362
378
|
const reversed = [];
|
|
363
379
|
const visited = /* @__PURE__ */ new Set();
|
|
364
|
-
let currentId =
|
|
380
|
+
let currentId = termId;
|
|
365
381
|
while (currentId) {
|
|
366
382
|
if (visited.has(currentId)) return null;
|
|
367
383
|
visited.add(currentId);
|
|
368
384
|
const current = termsById.get(currentId);
|
|
369
385
|
if (!current || !current.slug) return null;
|
|
370
386
|
reversed.push(current);
|
|
387
|
+
if (reversed.length > categoryDepth) return null;
|
|
371
388
|
currentId = current.parentId ?? null;
|
|
372
389
|
}
|
|
373
|
-
if (reversed.length !== presentation.categoryDepth) return null;
|
|
374
390
|
return reversed.reverse();
|
|
375
391
|
}
|
|
392
|
+
function hasChildCategory(termId, terms) {
|
|
393
|
+
return terms.some((term) => term.parentId === termId);
|
|
394
|
+
}
|
|
395
|
+
function resolveCategoryTreeChain(presentation, termId, terms) {
|
|
396
|
+
const chain = categoryTreeAncestry(presentation.categoryDepth, termId, terms);
|
|
397
|
+
if (!chain) return null;
|
|
398
|
+
if (presentation.entryCategory === "leaf") return chain;
|
|
399
|
+
if (hasChildCategory(termId, terms)) return null;
|
|
400
|
+
return chain.length === presentation.categoryDepth ? chain : null;
|
|
401
|
+
}
|
|
402
|
+
function resolveCategoryTreeSelection(presentation, termId, terms) {
|
|
403
|
+
if (hasChildCategory(termId, terms)) return null;
|
|
404
|
+
return resolveCategoryTreeChain(presentation, termId, terms);
|
|
405
|
+
}
|
|
376
406
|
function resolveCategoryTreeHubPaths(presentation, termId, terms) {
|
|
377
407
|
const chain = resolveCategoryTreeChain(presentation, termId, terms);
|
|
378
|
-
const
|
|
379
|
-
if (!chain ||
|
|
380
|
-
const paths = [
|
|
408
|
+
const prefix = basePathPrefix(presentation.basePath);
|
|
409
|
+
if (!chain || prefix === null) return null;
|
|
410
|
+
const paths = [prefix || "/"];
|
|
381
411
|
const segments = [];
|
|
382
412
|
for (const term of chain) {
|
|
383
413
|
segments.push(encodeURIComponent(term.slug));
|
|
384
|
-
paths.push(`${
|
|
414
|
+
paths.push(`${prefix}/${segments.join("/")}`);
|
|
385
415
|
}
|
|
386
416
|
return paths;
|
|
387
417
|
}
|
|
@@ -1085,10 +1115,10 @@ function scopeImportedCss(css, scope, allowedAssetUrls = []) {
|
|
|
1085
1115
|
|
|
1086
1116
|
// src/internal-content-links.ts
|
|
1087
1117
|
var INTERNAL_CONTENT_LINK_TOKEN_SOURCE = String.raw`\[\[internal:([a-z0-9가-힣]+(?:[.-][a-z0-9가-힣]+)*)\|([^\]\n|]+)\]\]`;
|
|
1088
|
-
function findInternalContentLinkTokens(
|
|
1118
|
+
function findInternalContentLinkTokens(text2) {
|
|
1089
1119
|
const pattern = new RegExp(INTERNAL_CONTENT_LINK_TOKEN_SOURCE, "giu");
|
|
1090
1120
|
const tokens = [];
|
|
1091
|
-
for (const match of
|
|
1121
|
+
for (const match of text2.matchAll(pattern)) {
|
|
1092
1122
|
const [raw, key, label] = match;
|
|
1093
1123
|
if (!key || !label || match.index === void 0) continue;
|
|
1094
1124
|
tokens.push({
|
|
@@ -1147,19 +1177,19 @@ function escapeHtmlText(value) {
|
|
|
1147
1177
|
var INTERNAL_CONTENT_LINK_CLASS = "rt-internal-link";
|
|
1148
1178
|
var INTERNAL_CONTENT_LINK_PENDING_CLASS = "rt-internal-link--pending";
|
|
1149
1179
|
var INTERNAL_CONTENT_LINK_PENDING_ATTR = "data-rt-internal-link-pending";
|
|
1150
|
-
function renderInternalContentLinksInText(
|
|
1151
|
-
const tokens = findInternalContentLinkTokens(
|
|
1152
|
-
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 };
|
|
1153
1183
|
let cursor = 0;
|
|
1154
1184
|
let html = "";
|
|
1155
1185
|
for (const token of tokens) {
|
|
1156
|
-
html +=
|
|
1186
|
+
html += text2.slice(cursor, token.index);
|
|
1157
1187
|
const path = resolve(token.key);
|
|
1158
1188
|
const label = escapeHtmlText(token.label);
|
|
1159
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>`;
|
|
1160
1190
|
cursor = token.index + token.raw.length;
|
|
1161
1191
|
}
|
|
1162
|
-
html +=
|
|
1192
|
+
html += text2.slice(cursor);
|
|
1163
1193
|
return { html, hasTokens: true };
|
|
1164
1194
|
}
|
|
1165
1195
|
var INTERNAL_LINK_BLOCKED_HTML_TAGS = /* @__PURE__ */ new Set([
|
|
@@ -1193,12 +1223,12 @@ function renderInternalContentLinksInHtml(html, resolve) {
|
|
|
1193
1223
|
let cursor = 0;
|
|
1194
1224
|
let out = "";
|
|
1195
1225
|
let hasTokens = false;
|
|
1196
|
-
const emitText = (
|
|
1197
|
-
if (blockedDepth > 0 || !
|
|
1198
|
-
out +=
|
|
1226
|
+
const emitText = (text2) => {
|
|
1227
|
+
if (blockedDepth > 0 || !text2) {
|
|
1228
|
+
out += text2;
|
|
1199
1229
|
return;
|
|
1200
1230
|
}
|
|
1201
|
-
const rendered = renderInternalContentLinksInText(
|
|
1231
|
+
const rendered = renderInternalContentLinksInText(text2, resolve);
|
|
1202
1232
|
hasTokens ||= rendered.hasTokens;
|
|
1203
1233
|
out += rendered.html;
|
|
1204
1234
|
};
|
|
@@ -1337,8 +1367,8 @@ function fromCodePointSafe2(n) {
|
|
|
1337
1367
|
}
|
|
1338
1368
|
}
|
|
1339
1369
|
function decodeUrlProbe(value) {
|
|
1340
|
-
const
|
|
1341
|
-
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) => {
|
|
1342
1372
|
const n = c.codePointAt(0) ?? 0;
|
|
1343
1373
|
return n > 32 && n !== 127;
|
|
1344
1374
|
}).join("").toLowerCase();
|
|
@@ -1962,6 +1992,287 @@ function formatSectionLockViolations(violations) {
|
|
|
1962
1992
|
return `${label} \uAD6C\uC870 \uBCC0\uACBD(${violation.field})`;
|
|
1963
1993
|
}).join(" / ");
|
|
1964
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
|
+
}
|
|
1965
2276
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1966
2277
|
0 && (module.exports = {
|
|
1967
2278
|
BLOCK_NAME_PATTERN,
|
|
@@ -1979,6 +2290,14 @@ function formatSectionLockViolations(violations) {
|
|
|
1979
2290
|
INTERNAL_CONTENT_LINK_PENDING_CLASS,
|
|
1980
2291
|
INTERNAL_CONTENT_LINK_TOKEN_SOURCE,
|
|
1981
2292
|
LEGACY_BLOG_DETAIL_PATH,
|
|
2293
|
+
METADATA_FIELDS,
|
|
2294
|
+
MetadataDefaultRulesSchema,
|
|
2295
|
+
MetadataFieldSchema,
|
|
2296
|
+
MetadataPatchSchema,
|
|
2297
|
+
MetadataRobotsPolicySchema,
|
|
2298
|
+
MetadataTargetKindSchema,
|
|
2299
|
+
MetadataTextRuleSchema,
|
|
2300
|
+
MetadataValuesSchema,
|
|
1982
2301
|
PHASE_1_CORE_BLOCKS,
|
|
1983
2302
|
PHASE_1_CSP,
|
|
1984
2303
|
POST_FOOTER_SLOT,
|
|
@@ -2007,6 +2326,7 @@ function formatSectionLockViolations(violations) {
|
|
|
2007
2326
|
findInternalContentLinkTokens,
|
|
2008
2327
|
findSectionLockViolations,
|
|
2009
2328
|
findSitePatternPlacementRule,
|
|
2329
|
+
formatMetadataDefaults,
|
|
2010
2330
|
formatSectionLockViolations,
|
|
2011
2331
|
htmlToPlainText,
|
|
2012
2332
|
injectStableIdsTree,
|
|
@@ -2016,18 +2336,25 @@ function formatSectionLockViolations(violations) {
|
|
|
2016
2336
|
isAllowedEmbedSrc,
|
|
2017
2337
|
isAllowedMapEmbedSrc,
|
|
2018
2338
|
isInCollectionMode,
|
|
2339
|
+
isProtectedRedirectPath,
|
|
2019
2340
|
isSectionContentPropPath,
|
|
2020
2341
|
isSitePatternSlotKey,
|
|
2021
2342
|
isValidBlock,
|
|
2022
2343
|
isValidSitePatternKey,
|
|
2023
2344
|
issuePreviewToken,
|
|
2345
|
+
mergeMetadataPatch,
|
|
2346
|
+
metadataContentImageUrl,
|
|
2024
2347
|
normalizeEmbed,
|
|
2025
2348
|
normalizeHexColor,
|
|
2026
2349
|
normalizeImageAlt,
|
|
2350
|
+
normalizeRedirectPath,
|
|
2351
|
+
normalizeRedirectSource,
|
|
2352
|
+
normalizeRedirectTarget,
|
|
2027
2353
|
parseBlock,
|
|
2028
2354
|
postsForCollection,
|
|
2029
2355
|
postsInAnyCollection,
|
|
2030
2356
|
postsNotInAnyCollection,
|
|
2357
|
+
readMetadataValues,
|
|
2031
2358
|
readSitePatternPlacements,
|
|
2032
2359
|
readSitePatternPresentation,
|
|
2033
2360
|
referencedSitePatternKeys,
|
|
@@ -2037,6 +2364,8 @@ function formatSectionLockViolations(violations) {
|
|
|
2037
2364
|
resolveCategoryTreeChain,
|
|
2038
2365
|
resolveCategoryTreeEntryPath,
|
|
2039
2366
|
resolveCategoryTreeHubPaths,
|
|
2367
|
+
resolveCategoryTreeSelection,
|
|
2368
|
+
resolveMetadata,
|
|
2040
2369
|
resolvePatternSlots,
|
|
2041
2370
|
resolvePostCategorySlug,
|
|
2042
2371
|
resolvePostCollection,
|