@mandujs/core 0.12.1 → 0.13.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/README.ko.md +304 -304
- package/README.md +653 -653
- package/package.json +8 -8
- package/src/brain/architecture/analyzer.ts +28 -26
- package/src/brain/doctor/analyzer.ts +1 -1
- package/src/bundler/build.ts +91 -91
- package/src/bundler/css.ts +302 -302
- package/src/bundler/dev.ts +0 -1
- package/src/change/history.ts +3 -3
- package/src/change/snapshot.ts +10 -9
- package/src/change/transaction.ts +2 -2
- package/src/client/Link.tsx +227 -227
- package/src/client/globals.ts +44 -44
- package/src/client/hooks.ts +267 -267
- package/src/client/index.ts +5 -5
- package/src/client/island.ts +8 -8
- package/src/client/router.ts +435 -435
- package/src/client/runtime.ts +23 -23
- package/src/client/serialize.ts +404 -404
- package/src/client/window-state.ts +101 -101
- package/src/config/mandu.ts +94 -96
- package/src/config/validate.ts +213 -215
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/content-layer.ts +314 -314
- package/src/content/content.test.ts +433 -433
- package/src/content/data-store.ts +245 -245
- package/src/content/digest.ts +133 -133
- package/src/content/index.ts +164 -164
- package/src/content/loader-context.ts +172 -172
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +169 -169
- package/src/content/loaders/glob.ts +252 -252
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/client.ts +16 -16
- package/src/contract/define.ts +459 -459
- package/src/contract/handler.ts +10 -10
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +404 -404
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/registry.ts +568 -568
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/contract/validator.ts +32 -32
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +465 -465
- package/src/devtools/client/catchers/error-catcher.ts +327 -327
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/catchers/network-proxy.ts +363 -363
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/kitchen-root.tsx +362 -362
- package/src/devtools/client/components/mandu-character.tsx +241 -241
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +244 -244
- package/src/devtools/client/components/panel/index.ts +32 -32
- package/src/devtools/client/components/panel/islands-panel.tsx +304 -304
- package/src/devtools/client/components/panel/network-panel.tsx +292 -292
- package/src/devtools/client/components/panel/panel-container.tsx +259 -259
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/client/state-manager.ts +478 -478
- package/src/devtools/design-tokens.ts +263 -263
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +266 -266
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +444 -444
- package/src/devtools/types.ts +319 -319
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +222 -222
- package/src/devtools/worker/worker-manager.ts +409 -409
- package/src/error/classifier.ts +2 -2
- package/src/error/domains.ts +265 -265
- package/src/error/formatter.ts +32 -32
- package/src/error/result.ts +46 -46
- package/src/error/stack-analyzer.ts +5 -0
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/auth.ts +308 -308
- package/src/filling/context.ts +569 -569
- package/src/filling/deps.ts +238 -238
- package/src/generator/contract-glue.ts +2 -1
- package/src/generator/generate.ts +12 -10
- package/src/generator/index.ts +3 -3
- package/src/generator/templates.ts +80 -79
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/ast-analyzer.ts +806 -806
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/check.ts +128 -128
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/healing.ts +2 -0
- package/src/guard/index.ts +2 -0
- package/src/guard/negotiation.ts +430 -4
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/cqrs.test.ts +175 -0
- package/src/guard/presets/cqrs.ts +107 -0
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/presets/index.ts +291 -288
- package/src/guard/reporter.ts +445 -445
- package/src/guard/rules.ts +12 -12
- package/src/guard/statistics.ts +578 -578
- package/src/guard/suggestions.ts +358 -352
- package/src/guard/types.ts +348 -347
- package/src/guard/validator.ts +834 -834
- package/src/guard/watcher.ts +404 -404
- package/src/index.ts +1 -0
- package/src/intent/index.ts +310 -310
- package/src/island/index.ts +304 -304
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/paths.test.ts +47 -0
- package/src/paths.ts +47 -0
- package/src/plugins/index.ts +38 -38
- package/src/plugins/registry.ts +377 -377
- package/src/plugins/types.ts +363 -363
- package/src/report/build.ts +1 -1
- package/src/report/index.ts +1 -1
- package/src/router/fs-patterns.ts +387 -387
- package/src/router/fs-routes.ts +344 -401
- package/src/router/fs-scanner.ts +497 -497
- package/src/router/fs-types.ts +270 -278
- package/src/router/index.ts +81 -81
- package/src/runtime/boundary.tsx +232 -232
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/logger.ts +677 -677
- package/src/runtime/router.test.ts +476 -476
- package/src/runtime/router.ts +105 -105
- package/src/runtime/security.ts +155 -155
- package/src/runtime/server.ts +24 -24
- package/src/runtime/session-key.ts +328 -328
- package/src/runtime/ssr.ts +367 -367
- package/src/runtime/streaming-ssr.ts +1245 -1245
- package/src/runtime/trace.ts +144 -144
- package/src/seo/index.ts +214 -214
- package/src/seo/integration/ssr.ts +307 -307
- package/src/seo/render/basic.ts +427 -427
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +191 -191
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +126 -126
- package/src/seo/resolve/index.ts +353 -353
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +97 -97
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +575 -575
- package/src/slot/validator.ts +39 -39
- package/src/spec/index.ts +3 -3
- package/src/spec/load.ts +76 -76
- package/src/spec/lock.ts +56 -56
- package/src/utils/bun.ts +8 -8
- package/src/utils/lru-cache.ts +75 -75
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/rules.ts +5 -5
package/src/seo/resolve/index.ts
CHANGED
|
@@ -1,353 +1,353 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu SEO - Metadata Resolution
|
|
3
|
-
*
|
|
4
|
-
* Layout 체인에서 메타데이터를 수집하고 병합
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type {
|
|
8
|
-
Metadata,
|
|
9
|
-
ResolvedMetadata,
|
|
10
|
-
MetadataItem,
|
|
11
|
-
MetadataParams,
|
|
12
|
-
Author,
|
|
13
|
-
Icons,
|
|
14
|
-
IconDescriptor,
|
|
15
|
-
ResolvedIcons,
|
|
16
|
-
Verification,
|
|
17
|
-
ResolvedVerification,
|
|
18
|
-
JsonLd,
|
|
19
|
-
ThemeColor,
|
|
20
|
-
ResourceHint,
|
|
21
|
-
FormatDetection,
|
|
22
|
-
GoogleMeta,
|
|
23
|
-
AppLinks,
|
|
24
|
-
} from '../types'
|
|
25
|
-
import { resolveTitle, extractTitleTemplate } from './title'
|
|
26
|
-
import { normalizeMetadataBase, resolveUrl } from './url'
|
|
27
|
-
import { resolveRobots } from './robots'
|
|
28
|
-
import { resolveOpenGraph } from './opengraph'
|
|
29
|
-
import { resolveTwitter } from './twitter'
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* 기본 메타데이터 생성
|
|
33
|
-
*/
|
|
34
|
-
export function createDefaultMetadata(): ResolvedMetadata {
|
|
35
|
-
return {
|
|
36
|
-
metadataBase: null,
|
|
37
|
-
title: null,
|
|
38
|
-
description: null,
|
|
39
|
-
applicationName: null,
|
|
40
|
-
authors: null,
|
|
41
|
-
generator: 'Mandu',
|
|
42
|
-
keywords: null,
|
|
43
|
-
referrer: null,
|
|
44
|
-
creator: null,
|
|
45
|
-
publisher: null,
|
|
46
|
-
robots: null,
|
|
47
|
-
alternates: null,
|
|
48
|
-
icons: null,
|
|
49
|
-
manifest: null,
|
|
50
|
-
openGraph: null,
|
|
51
|
-
twitter: null,
|
|
52
|
-
verification: null,
|
|
53
|
-
category: null,
|
|
54
|
-
classification: null,
|
|
55
|
-
jsonLd: null,
|
|
56
|
-
// Google SEO 최적화
|
|
57
|
-
google: null,
|
|
58
|
-
formatDetection: null,
|
|
59
|
-
resourceHints: null,
|
|
60
|
-
themeColor: null,
|
|
61
|
-
viewport: null,
|
|
62
|
-
appLinks: null,
|
|
63
|
-
other: null,
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Author 배열로 정규화
|
|
69
|
-
*/
|
|
70
|
-
function resolveAuthors(authors: Author | Author[] | null | undefined): Author[] | null {
|
|
71
|
-
if (!authors) return null
|
|
72
|
-
return Array.isArray(authors) ? authors : [authors]
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Keywords 배열로 정규화
|
|
77
|
-
*/
|
|
78
|
-
function resolveKeywords(keywords: string | string[] | null | undefined): string[] | null {
|
|
79
|
-
if (!keywords) return null
|
|
80
|
-
if (typeof keywords === 'string') {
|
|
81
|
-
return keywords.split(',').map(k => k.trim())
|
|
82
|
-
}
|
|
83
|
-
return keywords
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Icons 해석
|
|
88
|
-
*/
|
|
89
|
-
function resolveIcons(icons: Metadata['icons'], metadataBase: URL | null): ResolvedIcons | null {
|
|
90
|
-
if (!icons) return null
|
|
91
|
-
|
|
92
|
-
const result: ResolvedIcons = {
|
|
93
|
-
icon: [],
|
|
94
|
-
apple: [],
|
|
95
|
-
shortcut: [],
|
|
96
|
-
other: [],
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// 단순 문자열 URL
|
|
100
|
-
if (typeof icons === 'string') {
|
|
101
|
-
const url = resolveUrl(icons, metadataBase)
|
|
102
|
-
if (url) {
|
|
103
|
-
result.icon.push({ url: url.href })
|
|
104
|
-
}
|
|
105
|
-
return result
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// 배열
|
|
109
|
-
if (Array.isArray(icons)) {
|
|
110
|
-
for (const icon of icons) {
|
|
111
|
-
const descriptor = typeof icon === 'string'
|
|
112
|
-
? { url: icon }
|
|
113
|
-
: icon instanceof URL
|
|
114
|
-
? { url: icon.href }
|
|
115
|
-
: { ...icon, url: typeof icon.url === 'string' ? icon.url : icon.url.href }
|
|
116
|
-
|
|
117
|
-
const resolvedUrl = resolveUrl(descriptor.url, metadataBase)
|
|
118
|
-
if (resolvedUrl) {
|
|
119
|
-
result.icon.push({ ...descriptor, url: resolvedUrl.href })
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return result
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Icons 객체
|
|
126
|
-
const iconsObj = icons as Icons
|
|
127
|
-
|
|
128
|
-
const processIconArray = (
|
|
129
|
-
items: typeof iconsObj.icon,
|
|
130
|
-
target: IconDescriptor[]
|
|
131
|
-
) => {
|
|
132
|
-
if (!items) return
|
|
133
|
-
const arr = Array.isArray(items) ? items : [items]
|
|
134
|
-
for (const item of arr) {
|
|
135
|
-
const descriptor = typeof item === 'string'
|
|
136
|
-
? { url: item }
|
|
137
|
-
: item instanceof URL
|
|
138
|
-
? { url: item.href }
|
|
139
|
-
: { ...item, url: typeof item.url === 'string' ? item.url : item.url.href }
|
|
140
|
-
|
|
141
|
-
const resolvedUrl = resolveUrl(descriptor.url, metadataBase)
|
|
142
|
-
if (resolvedUrl) {
|
|
143
|
-
target.push({ ...descriptor, url: resolvedUrl.href })
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
processIconArray(iconsObj.icon, result.icon)
|
|
149
|
-
processIconArray(iconsObj.apple, result.apple)
|
|
150
|
-
processIconArray(iconsObj.shortcut, result.shortcut)
|
|
151
|
-
|
|
152
|
-
if (iconsObj.other) {
|
|
153
|
-
const others = Array.isArray(iconsObj.other) ? iconsObj.other : [iconsObj.other]
|
|
154
|
-
for (const other of others) {
|
|
155
|
-
const resolvedUrl = resolveUrl(other.url, metadataBase)
|
|
156
|
-
if (resolvedUrl) {
|
|
157
|
-
result.other.push({ ...other, url: resolvedUrl.href })
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return result
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Verification 해석
|
|
167
|
-
*/
|
|
168
|
-
function resolveVerification(verification: Verification | null | undefined): ResolvedVerification | null {
|
|
169
|
-
if (!verification) return null
|
|
170
|
-
|
|
171
|
-
const toArray = (val: string | string[] | undefined): string[] | null => {
|
|
172
|
-
if (!val) return null
|
|
173
|
-
return Array.isArray(val) ? val : [val]
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
google: toArray(verification.google),
|
|
178
|
-
yahoo: toArray(verification.yahoo),
|
|
179
|
-
yandex: toArray(verification.yandex),
|
|
180
|
-
me: toArray(verification.me),
|
|
181
|
-
other: verification.other
|
|
182
|
-
? Object.fromEntries(
|
|
183
|
-
Object.entries(verification.other).map(([k, v]) => [k, toArray(v)!])
|
|
184
|
-
)
|
|
185
|
-
: null,
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* JSON-LD 배열로 정규화
|
|
191
|
-
*/
|
|
192
|
-
function resolveJsonLd(jsonLd: JsonLd | JsonLd[] | null | undefined): JsonLd[] | null {
|
|
193
|
-
if (!jsonLd) return null
|
|
194
|
-
const arr = Array.isArray(jsonLd) ? jsonLd : [jsonLd]
|
|
195
|
-
// @context 기본값 추가
|
|
196
|
-
return arr.map(item => ({
|
|
197
|
-
'@context': 'https://schema.org',
|
|
198
|
-
...item,
|
|
199
|
-
}))
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Theme Color 배열로 정규화
|
|
204
|
-
*/
|
|
205
|
-
function resolveThemeColor(
|
|
206
|
-
themeColor: string | ThemeColor | ThemeColor[] | null | undefined
|
|
207
|
-
): ThemeColor[] | null {
|
|
208
|
-
if (!themeColor) return null
|
|
209
|
-
if (typeof themeColor === 'string') {
|
|
210
|
-
return [{ color: themeColor }]
|
|
211
|
-
}
|
|
212
|
-
return Array.isArray(themeColor) ? themeColor : [themeColor]
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Viewport 문자열로 정규화
|
|
217
|
-
*/
|
|
218
|
-
function resolveViewport(
|
|
219
|
-
viewport: Metadata['viewport']
|
|
220
|
-
): string | null {
|
|
221
|
-
if (!viewport) return null
|
|
222
|
-
if (typeof viewport === 'string') return viewport
|
|
223
|
-
|
|
224
|
-
const parts: string[] = []
|
|
225
|
-
|
|
226
|
-
if (viewport.width !== undefined) {
|
|
227
|
-
parts.push(`width=${viewport.width}`)
|
|
228
|
-
}
|
|
229
|
-
if (viewport.height !== undefined) {
|
|
230
|
-
parts.push(`height=${viewport.height}`)
|
|
231
|
-
}
|
|
232
|
-
if (viewport.initialScale !== undefined) {
|
|
233
|
-
parts.push(`initial-scale=${viewport.initialScale}`)
|
|
234
|
-
}
|
|
235
|
-
if (viewport.minimumScale !== undefined) {
|
|
236
|
-
parts.push(`minimum-scale=${viewport.minimumScale}`)
|
|
237
|
-
}
|
|
238
|
-
if (viewport.maximumScale !== undefined) {
|
|
239
|
-
parts.push(`maximum-scale=${viewport.maximumScale}`)
|
|
240
|
-
}
|
|
241
|
-
if (viewport.userScalable !== undefined) {
|
|
242
|
-
parts.push(`user-scalable=${viewport.userScalable ? 'yes' : 'no'}`)
|
|
243
|
-
}
|
|
244
|
-
if (viewport.viewportFit !== undefined) {
|
|
245
|
-
parts.push(`viewport-fit=${viewport.viewportFit}`)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return parts.length > 0 ? parts.join(', ') : null
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* 두 메타데이터 객체 병합
|
|
253
|
-
*/
|
|
254
|
-
export function mergeMetadata(
|
|
255
|
-
parent: ResolvedMetadata,
|
|
256
|
-
child: Metadata
|
|
257
|
-
): ResolvedMetadata {
|
|
258
|
-
const metadataBase = normalizeMetadataBase(child.metadataBase) || parent.metadataBase
|
|
259
|
-
|
|
260
|
-
// 부모의 title template 추출
|
|
261
|
-
const parentTemplate = parent.title?.template || null
|
|
262
|
-
|
|
263
|
-
return {
|
|
264
|
-
metadataBase,
|
|
265
|
-
title: resolveTitle(child.title, parentTemplate) ?? parent.title,
|
|
266
|
-
description: child.description ?? parent.description,
|
|
267
|
-
applicationName: child.applicationName ?? parent.applicationName,
|
|
268
|
-
authors: resolveAuthors(child.authors) ?? parent.authors,
|
|
269
|
-
generator: child.generator ?? parent.generator,
|
|
270
|
-
keywords: resolveKeywords(child.keywords) ?? parent.keywords,
|
|
271
|
-
referrer: child.referrer ?? parent.referrer,
|
|
272
|
-
creator: child.creator ?? parent.creator,
|
|
273
|
-
publisher: child.publisher ?? parent.publisher,
|
|
274
|
-
robots: resolveRobots(child.robots) ?? parent.robots,
|
|
275
|
-
alternates: parent.alternates, // TODO: resolve alternates
|
|
276
|
-
icons: resolveIcons(child.icons, metadataBase) ?? parent.icons,
|
|
277
|
-
manifest: resolveUrl(child.manifest, metadataBase) ?? parent.manifest,
|
|
278
|
-
openGraph: resolveOpenGraph(child.openGraph, metadataBase) ?? parent.openGraph,
|
|
279
|
-
twitter: resolveTwitter(child.twitter, metadataBase) ?? parent.twitter,
|
|
280
|
-
verification: resolveVerification(child.verification) ?? parent.verification,
|
|
281
|
-
category: child.category ?? parent.category,
|
|
282
|
-
classification: child.classification ?? parent.classification,
|
|
283
|
-
jsonLd: resolveJsonLd(child.jsonLd) ?? parent.jsonLd,
|
|
284
|
-
// Google SEO 최적화
|
|
285
|
-
google: child.google ?? parent.google,
|
|
286
|
-
formatDetection: child.formatDetection ?? parent.formatDetection,
|
|
287
|
-
resourceHints: child.resourceHints ?? parent.resourceHints,
|
|
288
|
-
themeColor: resolveThemeColor(child.themeColor) ?? parent.themeColor,
|
|
289
|
-
viewport: resolveViewport(child.viewport) ?? parent.viewport,
|
|
290
|
-
appLinks: child.appLinks ?? parent.appLinks,
|
|
291
|
-
other: child.other ?? parent.other,
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* 메타데이터 항목 해석 (정적/동적)
|
|
297
|
-
*/
|
|
298
|
-
async function resolveMetadataItem(
|
|
299
|
-
item: MetadataItem,
|
|
300
|
-
params: MetadataParams,
|
|
301
|
-
parentPromise: Promise<ResolvedMetadata>
|
|
302
|
-
): Promise<Metadata | null> {
|
|
303
|
-
if (!item) return null
|
|
304
|
-
|
|
305
|
-
// 정적 메타데이터
|
|
306
|
-
if (typeof item !== 'function') {
|
|
307
|
-
return item
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// 동적 메타데이터 (generateMetadata)
|
|
311
|
-
return await item(params, parentPromise)
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* 전체 메타데이터 해석 파이프라인
|
|
316
|
-
*
|
|
317
|
-
* Layout 체인을 순회하며 메타데이터를 병합
|
|
318
|
-
*
|
|
319
|
-
* @param metadataItems - [rootLayout, ...nestedLayouts, page]
|
|
320
|
-
* @param params - URL 파라미터
|
|
321
|
-
* @param searchParams - 쿼리 파라미터
|
|
322
|
-
*/
|
|
323
|
-
export async function resolveMetadata(
|
|
324
|
-
metadataItems: MetadataItem[],
|
|
325
|
-
params: Record<string, string> = {},
|
|
326
|
-
searchParams: Record<string, string> = {}
|
|
327
|
-
): Promise<ResolvedMetadata> {
|
|
328
|
-
let resolved = createDefaultMetadata()
|
|
329
|
-
|
|
330
|
-
for (const item of metadataItems) {
|
|
331
|
-
// 현재 resolved를 Promise로 래핑 (generateMetadata의 parent 파라미터용)
|
|
332
|
-
const parentPromise = Promise.resolve(resolved)
|
|
333
|
-
|
|
334
|
-
const metadata = await resolveMetadataItem(
|
|
335
|
-
item,
|
|
336
|
-
{ params, searchParams },
|
|
337
|
-
parentPromise
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
if (metadata) {
|
|
341
|
-
resolved = mergeMetadata(resolved, metadata)
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return resolved
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Re-export sub-modules
|
|
349
|
-
export { resolveTitle, extractTitleTemplate } from './title'
|
|
350
|
-
export { normalizeMetadataBase, resolveUrl, urlToString } from './url'
|
|
351
|
-
export { resolveRobots } from './robots'
|
|
352
|
-
export { resolveOpenGraph } from './opengraph'
|
|
353
|
-
export { resolveTwitter } from './twitter'
|
|
1
|
+
/**
|
|
2
|
+
* Mandu SEO - Metadata Resolution
|
|
3
|
+
*
|
|
4
|
+
* Layout 체인에서 메타데이터를 수집하고 병합
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
Metadata,
|
|
9
|
+
ResolvedMetadata,
|
|
10
|
+
MetadataItem,
|
|
11
|
+
MetadataParams,
|
|
12
|
+
Author,
|
|
13
|
+
Icons,
|
|
14
|
+
IconDescriptor,
|
|
15
|
+
ResolvedIcons,
|
|
16
|
+
Verification,
|
|
17
|
+
ResolvedVerification,
|
|
18
|
+
JsonLd,
|
|
19
|
+
ThemeColor,
|
|
20
|
+
ResourceHint,
|
|
21
|
+
FormatDetection,
|
|
22
|
+
GoogleMeta,
|
|
23
|
+
AppLinks,
|
|
24
|
+
} from '../types'
|
|
25
|
+
import { resolveTitle, extractTitleTemplate } from './title'
|
|
26
|
+
import { normalizeMetadataBase, resolveUrl } from './url'
|
|
27
|
+
import { resolveRobots } from './robots'
|
|
28
|
+
import { resolveOpenGraph } from './opengraph'
|
|
29
|
+
import { resolveTwitter } from './twitter'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 기본 메타데이터 생성
|
|
33
|
+
*/
|
|
34
|
+
export function createDefaultMetadata(): ResolvedMetadata {
|
|
35
|
+
return {
|
|
36
|
+
metadataBase: null,
|
|
37
|
+
title: null,
|
|
38
|
+
description: null,
|
|
39
|
+
applicationName: null,
|
|
40
|
+
authors: null,
|
|
41
|
+
generator: 'Mandu',
|
|
42
|
+
keywords: null,
|
|
43
|
+
referrer: null,
|
|
44
|
+
creator: null,
|
|
45
|
+
publisher: null,
|
|
46
|
+
robots: null,
|
|
47
|
+
alternates: null,
|
|
48
|
+
icons: null,
|
|
49
|
+
manifest: null,
|
|
50
|
+
openGraph: null,
|
|
51
|
+
twitter: null,
|
|
52
|
+
verification: null,
|
|
53
|
+
category: null,
|
|
54
|
+
classification: null,
|
|
55
|
+
jsonLd: null,
|
|
56
|
+
// Google SEO 최적화
|
|
57
|
+
google: null,
|
|
58
|
+
formatDetection: null,
|
|
59
|
+
resourceHints: null,
|
|
60
|
+
themeColor: null,
|
|
61
|
+
viewport: null,
|
|
62
|
+
appLinks: null,
|
|
63
|
+
other: null,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Author 배열로 정규화
|
|
69
|
+
*/
|
|
70
|
+
function resolveAuthors(authors: Author | Author[] | null | undefined): Author[] | null {
|
|
71
|
+
if (!authors) return null
|
|
72
|
+
return Array.isArray(authors) ? authors : [authors]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Keywords 배열로 정규화
|
|
77
|
+
*/
|
|
78
|
+
function resolveKeywords(keywords: string | string[] | null | undefined): string[] | null {
|
|
79
|
+
if (!keywords) return null
|
|
80
|
+
if (typeof keywords === 'string') {
|
|
81
|
+
return keywords.split(',').map(k => k.trim())
|
|
82
|
+
}
|
|
83
|
+
return keywords
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Icons 해석
|
|
88
|
+
*/
|
|
89
|
+
function resolveIcons(icons: Metadata['icons'], metadataBase: URL | null): ResolvedIcons | null {
|
|
90
|
+
if (!icons) return null
|
|
91
|
+
|
|
92
|
+
const result: ResolvedIcons = {
|
|
93
|
+
icon: [],
|
|
94
|
+
apple: [],
|
|
95
|
+
shortcut: [],
|
|
96
|
+
other: [],
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 단순 문자열 URL
|
|
100
|
+
if (typeof icons === 'string') {
|
|
101
|
+
const url = resolveUrl(icons, metadataBase)
|
|
102
|
+
if (url) {
|
|
103
|
+
result.icon.push({ url: url.href })
|
|
104
|
+
}
|
|
105
|
+
return result
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 배열
|
|
109
|
+
if (Array.isArray(icons)) {
|
|
110
|
+
for (const icon of icons) {
|
|
111
|
+
const descriptor = typeof icon === 'string'
|
|
112
|
+
? { url: icon }
|
|
113
|
+
: icon instanceof URL
|
|
114
|
+
? { url: icon.href }
|
|
115
|
+
: { ...icon, url: typeof icon.url === 'string' ? icon.url : icon.url.href }
|
|
116
|
+
|
|
117
|
+
const resolvedUrl = resolveUrl(descriptor.url, metadataBase)
|
|
118
|
+
if (resolvedUrl) {
|
|
119
|
+
result.icon.push({ ...descriptor, url: resolvedUrl.href })
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return result
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Icons 객체
|
|
126
|
+
const iconsObj = icons as Icons
|
|
127
|
+
|
|
128
|
+
const processIconArray = (
|
|
129
|
+
items: typeof iconsObj.icon,
|
|
130
|
+
target: IconDescriptor[]
|
|
131
|
+
) => {
|
|
132
|
+
if (!items) return
|
|
133
|
+
const arr = Array.isArray(items) ? items : [items]
|
|
134
|
+
for (const item of arr) {
|
|
135
|
+
const descriptor = typeof item === 'string'
|
|
136
|
+
? { url: item }
|
|
137
|
+
: item instanceof URL
|
|
138
|
+
? { url: item.href }
|
|
139
|
+
: { ...item, url: typeof item.url === 'string' ? item.url : item.url.href }
|
|
140
|
+
|
|
141
|
+
const resolvedUrl = resolveUrl(descriptor.url, metadataBase)
|
|
142
|
+
if (resolvedUrl) {
|
|
143
|
+
target.push({ ...descriptor, url: resolvedUrl.href })
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
processIconArray(iconsObj.icon, result.icon)
|
|
149
|
+
processIconArray(iconsObj.apple, result.apple)
|
|
150
|
+
processIconArray(iconsObj.shortcut, result.shortcut)
|
|
151
|
+
|
|
152
|
+
if (iconsObj.other) {
|
|
153
|
+
const others = Array.isArray(iconsObj.other) ? iconsObj.other : [iconsObj.other]
|
|
154
|
+
for (const other of others) {
|
|
155
|
+
const resolvedUrl = resolveUrl(other.url, metadataBase)
|
|
156
|
+
if (resolvedUrl) {
|
|
157
|
+
result.other.push({ ...other, url: resolvedUrl.href })
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return result
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Verification 해석
|
|
167
|
+
*/
|
|
168
|
+
function resolveVerification(verification: Verification | null | undefined): ResolvedVerification | null {
|
|
169
|
+
if (!verification) return null
|
|
170
|
+
|
|
171
|
+
const toArray = (val: string | string[] | undefined): string[] | null => {
|
|
172
|
+
if (!val) return null
|
|
173
|
+
return Array.isArray(val) ? val : [val]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
google: toArray(verification.google),
|
|
178
|
+
yahoo: toArray(verification.yahoo),
|
|
179
|
+
yandex: toArray(verification.yandex),
|
|
180
|
+
me: toArray(verification.me),
|
|
181
|
+
other: verification.other
|
|
182
|
+
? Object.fromEntries(
|
|
183
|
+
Object.entries(verification.other).map(([k, v]) => [k, toArray(v)!])
|
|
184
|
+
)
|
|
185
|
+
: null,
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* JSON-LD 배열로 정규화
|
|
191
|
+
*/
|
|
192
|
+
function resolveJsonLd(jsonLd: JsonLd | JsonLd[] | null | undefined): JsonLd[] | null {
|
|
193
|
+
if (!jsonLd) return null
|
|
194
|
+
const arr = Array.isArray(jsonLd) ? jsonLd : [jsonLd]
|
|
195
|
+
// @context 기본값 추가
|
|
196
|
+
return arr.map(item => ({
|
|
197
|
+
'@context': 'https://schema.org',
|
|
198
|
+
...item,
|
|
199
|
+
}))
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Theme Color 배열로 정규화
|
|
204
|
+
*/
|
|
205
|
+
function resolveThemeColor(
|
|
206
|
+
themeColor: string | ThemeColor | ThemeColor[] | null | undefined
|
|
207
|
+
): ThemeColor[] | null {
|
|
208
|
+
if (!themeColor) return null
|
|
209
|
+
if (typeof themeColor === 'string') {
|
|
210
|
+
return [{ color: themeColor }]
|
|
211
|
+
}
|
|
212
|
+
return Array.isArray(themeColor) ? themeColor : [themeColor]
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Viewport 문자열로 정규화
|
|
217
|
+
*/
|
|
218
|
+
function resolveViewport(
|
|
219
|
+
viewport: Metadata['viewport']
|
|
220
|
+
): string | null {
|
|
221
|
+
if (!viewport) return null
|
|
222
|
+
if (typeof viewport === 'string') return viewport
|
|
223
|
+
|
|
224
|
+
const parts: string[] = []
|
|
225
|
+
|
|
226
|
+
if (viewport.width !== undefined) {
|
|
227
|
+
parts.push(`width=${viewport.width}`)
|
|
228
|
+
}
|
|
229
|
+
if (viewport.height !== undefined) {
|
|
230
|
+
parts.push(`height=${viewport.height}`)
|
|
231
|
+
}
|
|
232
|
+
if (viewport.initialScale !== undefined) {
|
|
233
|
+
parts.push(`initial-scale=${viewport.initialScale}`)
|
|
234
|
+
}
|
|
235
|
+
if (viewport.minimumScale !== undefined) {
|
|
236
|
+
parts.push(`minimum-scale=${viewport.minimumScale}`)
|
|
237
|
+
}
|
|
238
|
+
if (viewport.maximumScale !== undefined) {
|
|
239
|
+
parts.push(`maximum-scale=${viewport.maximumScale}`)
|
|
240
|
+
}
|
|
241
|
+
if (viewport.userScalable !== undefined) {
|
|
242
|
+
parts.push(`user-scalable=${viewport.userScalable ? 'yes' : 'no'}`)
|
|
243
|
+
}
|
|
244
|
+
if (viewport.viewportFit !== undefined) {
|
|
245
|
+
parts.push(`viewport-fit=${viewport.viewportFit}`)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return parts.length > 0 ? parts.join(', ') : null
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 두 메타데이터 객체 병합
|
|
253
|
+
*/
|
|
254
|
+
export function mergeMetadata(
|
|
255
|
+
parent: ResolvedMetadata,
|
|
256
|
+
child: Metadata
|
|
257
|
+
): ResolvedMetadata {
|
|
258
|
+
const metadataBase = normalizeMetadataBase(child.metadataBase) || parent.metadataBase
|
|
259
|
+
|
|
260
|
+
// 부모의 title template 추출
|
|
261
|
+
const parentTemplate = parent.title?.template || null
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
metadataBase,
|
|
265
|
+
title: resolveTitle(child.title, parentTemplate) ?? parent.title,
|
|
266
|
+
description: child.description ?? parent.description,
|
|
267
|
+
applicationName: child.applicationName ?? parent.applicationName,
|
|
268
|
+
authors: resolveAuthors(child.authors) ?? parent.authors,
|
|
269
|
+
generator: child.generator ?? parent.generator,
|
|
270
|
+
keywords: resolveKeywords(child.keywords) ?? parent.keywords,
|
|
271
|
+
referrer: child.referrer ?? parent.referrer,
|
|
272
|
+
creator: child.creator ?? parent.creator,
|
|
273
|
+
publisher: child.publisher ?? parent.publisher,
|
|
274
|
+
robots: resolveRobots(child.robots) ?? parent.robots,
|
|
275
|
+
alternates: parent.alternates, // TODO: resolve alternates
|
|
276
|
+
icons: resolveIcons(child.icons, metadataBase) ?? parent.icons,
|
|
277
|
+
manifest: resolveUrl(child.manifest, metadataBase) ?? parent.manifest,
|
|
278
|
+
openGraph: resolveOpenGraph(child.openGraph, metadataBase) ?? parent.openGraph,
|
|
279
|
+
twitter: resolveTwitter(child.twitter, metadataBase) ?? parent.twitter,
|
|
280
|
+
verification: resolveVerification(child.verification) ?? parent.verification,
|
|
281
|
+
category: child.category ?? parent.category,
|
|
282
|
+
classification: child.classification ?? parent.classification,
|
|
283
|
+
jsonLd: resolveJsonLd(child.jsonLd) ?? parent.jsonLd,
|
|
284
|
+
// Google SEO 최적화
|
|
285
|
+
google: child.google ?? parent.google,
|
|
286
|
+
formatDetection: child.formatDetection ?? parent.formatDetection,
|
|
287
|
+
resourceHints: child.resourceHints ?? parent.resourceHints,
|
|
288
|
+
themeColor: resolveThemeColor(child.themeColor) ?? parent.themeColor,
|
|
289
|
+
viewport: resolveViewport(child.viewport) ?? parent.viewport,
|
|
290
|
+
appLinks: child.appLinks ?? parent.appLinks,
|
|
291
|
+
other: child.other ?? parent.other,
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 메타데이터 항목 해석 (정적/동적)
|
|
297
|
+
*/
|
|
298
|
+
async function resolveMetadataItem(
|
|
299
|
+
item: MetadataItem,
|
|
300
|
+
params: MetadataParams,
|
|
301
|
+
parentPromise: Promise<ResolvedMetadata>
|
|
302
|
+
): Promise<Metadata | null> {
|
|
303
|
+
if (!item) return null
|
|
304
|
+
|
|
305
|
+
// 정적 메타데이터
|
|
306
|
+
if (typeof item !== 'function') {
|
|
307
|
+
return item
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 동적 메타데이터 (generateMetadata)
|
|
311
|
+
return await item(params, parentPromise)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 전체 메타데이터 해석 파이프라인
|
|
316
|
+
*
|
|
317
|
+
* Layout 체인을 순회하며 메타데이터를 병합
|
|
318
|
+
*
|
|
319
|
+
* @param metadataItems - [rootLayout, ...nestedLayouts, page]
|
|
320
|
+
* @param params - URL 파라미터
|
|
321
|
+
* @param searchParams - 쿼리 파라미터
|
|
322
|
+
*/
|
|
323
|
+
export async function resolveMetadata(
|
|
324
|
+
metadataItems: MetadataItem[],
|
|
325
|
+
params: Record<string, string> = {},
|
|
326
|
+
searchParams: Record<string, string> = {}
|
|
327
|
+
): Promise<ResolvedMetadata> {
|
|
328
|
+
let resolved = createDefaultMetadata()
|
|
329
|
+
|
|
330
|
+
for (const item of metadataItems) {
|
|
331
|
+
// 현재 resolved를 Promise로 래핑 (generateMetadata의 parent 파라미터용)
|
|
332
|
+
const parentPromise = Promise.resolve(resolved)
|
|
333
|
+
|
|
334
|
+
const metadata = await resolveMetadataItem(
|
|
335
|
+
item,
|
|
336
|
+
{ params, searchParams },
|
|
337
|
+
parentPromise
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
if (metadata) {
|
|
341
|
+
resolved = mergeMetadata(resolved, metadata)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return resolved
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Re-export sub-modules
|
|
349
|
+
export { resolveTitle, extractTitleTemplate } from './title'
|
|
350
|
+
export { normalizeMetadataBase, resolveUrl, urlToString } from './url'
|
|
351
|
+
export { resolveRobots } from './robots'
|
|
352
|
+
export { resolveOpenGraph } from './opengraph'
|
|
353
|
+
export { resolveTwitter } from './twitter'
|