@mandujs/core 0.13.0 → 0.13.2
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 +4 -4
- package/README.md +653 -653
- package/package.json +1 -1
- package/src/bundler/build.ts +91 -91
- package/src/bundler/css.ts +302 -302
- 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 +9 -0
- package/src/config/validate.ts +12 -0
- 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/domains.ts +265 -265
- package/src/error/result.ts +46 -46
- 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 +24 -1
- package/src/filling/deps.ts +238 -238
- package/src/filling/index.ts +4 -0
- package/src/filling/sse-catchup.test.ts +56 -0
- package/src/filling/sse-catchup.ts +67 -0
- package/src/filling/sse.test.ts +168 -0
- package/src/filling/sse.ts +162 -0
- package/src/generator/index.ts +3 -3
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/ast-analyzer.ts +806 -806
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/presets/index.ts +291 -291
- 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 -358
- package/src/guard/types.ts +348 -348
- package/src/guard/validator.ts +834 -834
- package/src/guard/watcher.ts +404 -404
- package/src/index.ts +6 -1
- 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/plugins/index.ts +38 -38
- package/src/plugins/registry.ts +377 -377
- package/src/plugins/types.ts +363 -363
- package/src/report/index.ts +1 -1
- package/src/router/fs-patterns.ts +387 -387
- package/src/router/fs-scanner.ts +497 -497
- package/src/runtime/boundary.tsx +232 -232
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/escape.ts +44 -0
- 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 +257 -0
- package/src/runtime/session-key.ts +328 -328
- package/src/runtime/ssr.ts +16 -21
- package/src/runtime/streaming-ssr.ts +24 -33
- 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/seo/render/basic.ts
CHANGED
|
@@ -1,427 +1,427 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu SEO - Basic Meta Tags Rendering
|
|
3
|
-
*
|
|
4
|
-
* title, description, robots 등 기본 메타 태그 렌더링
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { ResolvedMetadata } from '../types'
|
|
8
|
-
import { urlToString } from '../resolve/url'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* 메타 태그 생성 헬퍼
|
|
12
|
-
*/
|
|
13
|
-
function meta(name: string, content: string): string {
|
|
14
|
-
const escapedContent = escapeHtml(content)
|
|
15
|
-
return `<meta name="${name}" content="${escapedContent}" />`
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* property 메타 태그 생성 헬퍼 (Open Graph용)
|
|
20
|
-
*/
|
|
21
|
-
function metaProperty(property: string, content: string): string {
|
|
22
|
-
const escapedContent = escapeHtml(content)
|
|
23
|
-
return `<meta property="${property}" content="${escapedContent}" />`
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* HTML 이스케이프
|
|
28
|
-
*/
|
|
29
|
-
function escapeHtml(str: string): string {
|
|
30
|
-
return str
|
|
31
|
-
.replace(/&/g, '&')
|
|
32
|
-
.replace(/</g, '<')
|
|
33
|
-
.replace(/>/g, '>')
|
|
34
|
-
.replace(/"/g, '"')
|
|
35
|
-
.replace(/'/g, ''')
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* <title> 태그 렌더링
|
|
40
|
-
*/
|
|
41
|
-
export function renderTitle(metadata: ResolvedMetadata): string {
|
|
42
|
-
const title = metadata.title?.absolute
|
|
43
|
-
if (!title) return ''
|
|
44
|
-
return `<title>${escapeHtml(title)}</title>`
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* 기본 메타 태그 렌더링
|
|
49
|
-
*/
|
|
50
|
-
export function renderBasicMeta(metadata: ResolvedMetadata): string {
|
|
51
|
-
const tags: string[] = []
|
|
52
|
-
|
|
53
|
-
// description
|
|
54
|
-
if (metadata.description) {
|
|
55
|
-
tags.push(meta('description', metadata.description))
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// application-name
|
|
59
|
-
if (metadata.applicationName) {
|
|
60
|
-
tags.push(meta('application-name', metadata.applicationName))
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// author
|
|
64
|
-
if (metadata.authors) {
|
|
65
|
-
for (const author of metadata.authors) {
|
|
66
|
-
if (author.name) {
|
|
67
|
-
tags.push(meta('author', author.name))
|
|
68
|
-
}
|
|
69
|
-
if (author.url) {
|
|
70
|
-
const url = typeof author.url === 'string' ? author.url : author.url.href
|
|
71
|
-
tags.push(`<link rel="author" href="${escapeHtml(url)}" />`)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// generator
|
|
77
|
-
if (metadata.generator) {
|
|
78
|
-
tags.push(meta('generator', metadata.generator))
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// keywords
|
|
82
|
-
if (metadata.keywords && metadata.keywords.length > 0) {
|
|
83
|
-
tags.push(meta('keywords', metadata.keywords.join(', ')))
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// referrer
|
|
87
|
-
if (metadata.referrer) {
|
|
88
|
-
tags.push(meta('referrer', metadata.referrer))
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// creator
|
|
92
|
-
if (metadata.creator) {
|
|
93
|
-
tags.push(meta('creator', metadata.creator))
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// publisher
|
|
97
|
-
if (metadata.publisher) {
|
|
98
|
-
tags.push(meta('publisher', metadata.publisher))
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// robots
|
|
102
|
-
if (metadata.robots?.basic) {
|
|
103
|
-
tags.push(meta('robots', metadata.robots.basic))
|
|
104
|
-
}
|
|
105
|
-
if (metadata.robots?.googleBot) {
|
|
106
|
-
tags.push(meta('googlebot', metadata.robots.googleBot))
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// category
|
|
110
|
-
if (metadata.category) {
|
|
111
|
-
tags.push(meta('category', metadata.category))
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// classification
|
|
115
|
-
if (metadata.classification) {
|
|
116
|
-
tags.push(meta('classification', metadata.classification))
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return tags.join('\n')
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Verification 메타 태그 렌더링
|
|
124
|
-
*/
|
|
125
|
-
export function renderVerification(metadata: ResolvedMetadata): string {
|
|
126
|
-
const verification = metadata.verification
|
|
127
|
-
if (!verification) return ''
|
|
128
|
-
|
|
129
|
-
const tags: string[] = []
|
|
130
|
-
|
|
131
|
-
if (verification.google) {
|
|
132
|
-
for (const value of verification.google) {
|
|
133
|
-
tags.push(meta('google-site-verification', value))
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (verification.yahoo) {
|
|
138
|
-
for (const value of verification.yahoo) {
|
|
139
|
-
tags.push(meta('y_key', value))
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (verification.yandex) {
|
|
144
|
-
for (const value of verification.yandex) {
|
|
145
|
-
tags.push(meta('yandex-verification', value))
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (verification.me) {
|
|
150
|
-
for (const value of verification.me) {
|
|
151
|
-
tags.push(meta('me', value))
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (verification.other) {
|
|
156
|
-
for (const [name, values] of Object.entries(verification.other)) {
|
|
157
|
-
for (const value of values) {
|
|
158
|
-
tags.push(meta(name, value))
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return tags.join('\n')
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Canonical & Alternates 렌더링
|
|
168
|
-
*/
|
|
169
|
-
export function renderAlternates(metadata: ResolvedMetadata): string {
|
|
170
|
-
const alternates = metadata.alternates
|
|
171
|
-
if (!alternates) return ''
|
|
172
|
-
|
|
173
|
-
const tags: string[] = []
|
|
174
|
-
|
|
175
|
-
// canonical
|
|
176
|
-
if (alternates.canonical) {
|
|
177
|
-
tags.push(`<link rel="canonical" href="${escapeHtml(alternates.canonical.href)}" />`)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// languages (hreflang)
|
|
181
|
-
if (alternates.languages) {
|
|
182
|
-
for (const [lang, url] of Object.entries(alternates.languages)) {
|
|
183
|
-
tags.push(`<link rel="alternate" hreflang="${lang}" href="${escapeHtml(url.href)}" />`)
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// media alternates
|
|
188
|
-
if (alternates.media) {
|
|
189
|
-
for (const [media, url] of Object.entries(alternates.media)) {
|
|
190
|
-
tags.push(`<link rel="alternate" media="${escapeHtml(media)}" href="${escapeHtml(url.href)}" />`)
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// type alternates
|
|
195
|
-
if (alternates.types) {
|
|
196
|
-
for (const [type, url] of Object.entries(alternates.types)) {
|
|
197
|
-
tags.push(`<link rel="alternate" type="${escapeHtml(type)}" href="${escapeHtml(url.href)}" />`)
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return tags.join('\n')
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Icons 렌더링
|
|
206
|
-
*/
|
|
207
|
-
export function renderIcons(metadata: ResolvedMetadata): string {
|
|
208
|
-
const icons = metadata.icons
|
|
209
|
-
if (!icons) return ''
|
|
210
|
-
|
|
211
|
-
const tags: string[] = []
|
|
212
|
-
|
|
213
|
-
// icon
|
|
214
|
-
for (const icon of icons.icon) {
|
|
215
|
-
const attrs = [`rel="icon"`, `href="${escapeHtml(urlToString(icon.url))}"`]
|
|
216
|
-
if (icon.type) attrs.push(`type="${escapeHtml(icon.type)}"`)
|
|
217
|
-
if (icon.sizes) attrs.push(`sizes="${escapeHtml(icon.sizes)}"`)
|
|
218
|
-
tags.push(`<link ${attrs.join(' ')} />`)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// apple-touch-icon
|
|
222
|
-
for (const icon of icons.apple) {
|
|
223
|
-
const attrs = [`rel="apple-touch-icon"`, `href="${escapeHtml(urlToString(icon.url))}"`]
|
|
224
|
-
if (icon.sizes) attrs.push(`sizes="${escapeHtml(icon.sizes)}"`)
|
|
225
|
-
tags.push(`<link ${attrs.join(' ')} />`)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// shortcut icon
|
|
229
|
-
for (const icon of icons.shortcut) {
|
|
230
|
-
tags.push(`<link rel="shortcut icon" href="${escapeHtml(urlToString(icon.url))}" />`)
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// other icons
|
|
234
|
-
for (const icon of icons.other) {
|
|
235
|
-
const attrs = [`href="${escapeHtml(urlToString(icon.url))}"`]
|
|
236
|
-
if (icon.rel) attrs.push(`rel="${escapeHtml(icon.rel)}"`)
|
|
237
|
-
if (icon.type) attrs.push(`type="${escapeHtml(icon.type)}"`)
|
|
238
|
-
if (icon.sizes) attrs.push(`sizes="${escapeHtml(icon.sizes)}"`)
|
|
239
|
-
if (icon.color) attrs.push(`color="${escapeHtml(icon.color)}"`)
|
|
240
|
-
if (icon.media) attrs.push(`media="${escapeHtml(icon.media)}"`)
|
|
241
|
-
tags.push(`<link ${attrs.join(' ')} />`)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return tags.join('\n')
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Manifest 렌더링
|
|
249
|
-
*/
|
|
250
|
-
export function renderManifest(metadata: ResolvedMetadata): string {
|
|
251
|
-
if (!metadata.manifest) return ''
|
|
252
|
-
return `<link rel="manifest" href="${escapeHtml(metadata.manifest.href)}" />`
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Other custom meta tags 렌더링
|
|
257
|
-
*/
|
|
258
|
-
export function renderOther(metadata: ResolvedMetadata): string {
|
|
259
|
-
if (!metadata.other) return ''
|
|
260
|
-
|
|
261
|
-
const tags: string[] = []
|
|
262
|
-
|
|
263
|
-
for (const [name, value] of Object.entries(metadata.other)) {
|
|
264
|
-
const values = Array.isArray(value) ? value : [value]
|
|
265
|
-
for (const v of values) {
|
|
266
|
-
tags.push(meta(name, String(v)))
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return tags.join('\n')
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// ============================================================================
|
|
274
|
-
// Google SEO 최적화 렌더링
|
|
275
|
-
// ============================================================================
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Google 전용 메타 태그 렌더링
|
|
279
|
-
*/
|
|
280
|
-
export function renderGoogle(metadata: ResolvedMetadata): string {
|
|
281
|
-
const google = metadata.google
|
|
282
|
-
if (!google) return ''
|
|
283
|
-
|
|
284
|
-
const tags: string[] = []
|
|
285
|
-
|
|
286
|
-
if (google.nositelinkssearchbox) {
|
|
287
|
-
tags.push(meta('google', 'nositelinkssearchbox'))
|
|
288
|
-
}
|
|
289
|
-
if (google.notranslate) {
|
|
290
|
-
tags.push(meta('google', 'notranslate'))
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return tags.join('\n')
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Format Detection 메타 태그 렌더링
|
|
298
|
-
*/
|
|
299
|
-
export function renderFormatDetection(metadata: ResolvedMetadata): string {
|
|
300
|
-
const fd = metadata.formatDetection
|
|
301
|
-
if (!fd) return ''
|
|
302
|
-
|
|
303
|
-
const values: string[] = []
|
|
304
|
-
|
|
305
|
-
if (fd.telephone === false) values.push('telephone=no')
|
|
306
|
-
if (fd.date === false) values.push('date=no')
|
|
307
|
-
if (fd.address === false) values.push('address=no')
|
|
308
|
-
if (fd.email === false) values.push('email=no')
|
|
309
|
-
|
|
310
|
-
if (values.length === 0) return ''
|
|
311
|
-
return meta('format-detection', values.join(', '))
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Theme Color 메타 태그 렌더링
|
|
316
|
-
*/
|
|
317
|
-
export function renderThemeColor(metadata: ResolvedMetadata): string {
|
|
318
|
-
const themeColor = metadata.themeColor
|
|
319
|
-
if (!themeColor || themeColor.length === 0) return ''
|
|
320
|
-
|
|
321
|
-
const tags: string[] = []
|
|
322
|
-
|
|
323
|
-
for (const tc of themeColor) {
|
|
324
|
-
if (tc.media) {
|
|
325
|
-
tags.push(`<meta name="theme-color" content="${escapeHtml(tc.color)}" media="${escapeHtml(tc.media)}" />`)
|
|
326
|
-
} else {
|
|
327
|
-
tags.push(`<meta name="theme-color" content="${escapeHtml(tc.color)}" />`)
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return tags.join('\n')
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Viewport 메타 태그 렌더링
|
|
336
|
-
*/
|
|
337
|
-
export function renderViewport(metadata: ResolvedMetadata): string {
|
|
338
|
-
if (!metadata.viewport) return ''
|
|
339
|
-
return meta('viewport', metadata.viewport)
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Resource Hints 렌더링 (preconnect, dns-prefetch, preload)
|
|
344
|
-
*/
|
|
345
|
-
export function renderResourceHints(metadata: ResolvedMetadata): string {
|
|
346
|
-
const hints = metadata.resourceHints
|
|
347
|
-
if (!hints) return ''
|
|
348
|
-
|
|
349
|
-
const tags: string[] = []
|
|
350
|
-
|
|
351
|
-
// dns-prefetch
|
|
352
|
-
if (hints.dnsPrefetch) {
|
|
353
|
-
for (const url of hints.dnsPrefetch) {
|
|
354
|
-
tags.push(`<link rel="dns-prefetch" href="${escapeHtml(url)}" />`)
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// preconnect
|
|
359
|
-
if (hints.preconnect) {
|
|
360
|
-
for (const url of hints.preconnect) {
|
|
361
|
-
tags.push(`<link rel="preconnect" href="${escapeHtml(url)}" crossorigin />`)
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// preload
|
|
366
|
-
if (hints.preload) {
|
|
367
|
-
for (const resource of hints.preload) {
|
|
368
|
-
const attrs = [
|
|
369
|
-
`rel="preload"`,
|
|
370
|
-
`href="${escapeHtml(resource.href)}"`,
|
|
371
|
-
`as="${resource.as}"`,
|
|
372
|
-
]
|
|
373
|
-
if (resource.type) {
|
|
374
|
-
attrs.push(`type="${escapeHtml(resource.type)}"`)
|
|
375
|
-
}
|
|
376
|
-
if (resource.crossOrigin) {
|
|
377
|
-
attrs.push(`crossorigin="${resource.crossOrigin}"`)
|
|
378
|
-
}
|
|
379
|
-
tags.push(`<link ${attrs.join(' ')} />`)
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// prefetch
|
|
384
|
-
if (hints.prefetch) {
|
|
385
|
-
for (const url of hints.prefetch) {
|
|
386
|
-
tags.push(`<link rel="prefetch" href="${escapeHtml(url)}" />`)
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
return tags.join('\n')
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* App Links 메타 태그 렌더링 (iOS/Android 앱 연동)
|
|
395
|
-
*/
|
|
396
|
-
export function renderAppLinks(metadata: ResolvedMetadata): string {
|
|
397
|
-
const al = metadata.appLinks
|
|
398
|
-
if (!al) return ''
|
|
399
|
-
|
|
400
|
-
const tags: string[] = []
|
|
401
|
-
|
|
402
|
-
// iOS
|
|
403
|
-
if (al.iosAppStoreId) {
|
|
404
|
-
tags.push(meta('apple-itunes-app', `app-id=${al.iosAppStoreId}`))
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Android
|
|
408
|
-
if (al.androidPackage) {
|
|
409
|
-
tags.push(metaProperty('al:android:package', al.androidPackage))
|
|
410
|
-
}
|
|
411
|
-
if (al.androidAppName) {
|
|
412
|
-
tags.push(metaProperty('al:android:app_name', al.androidAppName))
|
|
413
|
-
}
|
|
414
|
-
if (al.androidUrl) {
|
|
415
|
-
tags.push(metaProperty('al:android:url', al.androidUrl))
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// iOS App Links
|
|
419
|
-
if (al.iosAppName) {
|
|
420
|
-
tags.push(metaProperty('al:ios:app_name', al.iosAppName))
|
|
421
|
-
}
|
|
422
|
-
if (al.iosUrl) {
|
|
423
|
-
tags.push(metaProperty('al:ios:url', al.iosUrl))
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
return tags.join('\n')
|
|
427
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Mandu SEO - Basic Meta Tags Rendering
|
|
3
|
+
*
|
|
4
|
+
* title, description, robots 등 기본 메타 태그 렌더링
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ResolvedMetadata } from '../types'
|
|
8
|
+
import { urlToString } from '../resolve/url'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 메타 태그 생성 헬퍼
|
|
12
|
+
*/
|
|
13
|
+
function meta(name: string, content: string): string {
|
|
14
|
+
const escapedContent = escapeHtml(content)
|
|
15
|
+
return `<meta name="${name}" content="${escapedContent}" />`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* property 메타 태그 생성 헬퍼 (Open Graph용)
|
|
20
|
+
*/
|
|
21
|
+
function metaProperty(property: string, content: string): string {
|
|
22
|
+
const escapedContent = escapeHtml(content)
|
|
23
|
+
return `<meta property="${property}" content="${escapedContent}" />`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* HTML 이스케이프
|
|
28
|
+
*/
|
|
29
|
+
function escapeHtml(str: string): string {
|
|
30
|
+
return str
|
|
31
|
+
.replace(/&/g, '&')
|
|
32
|
+
.replace(/</g, '<')
|
|
33
|
+
.replace(/>/g, '>')
|
|
34
|
+
.replace(/"/g, '"')
|
|
35
|
+
.replace(/'/g, ''')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* <title> 태그 렌더링
|
|
40
|
+
*/
|
|
41
|
+
export function renderTitle(metadata: ResolvedMetadata): string {
|
|
42
|
+
const title = metadata.title?.absolute
|
|
43
|
+
if (!title) return ''
|
|
44
|
+
return `<title>${escapeHtml(title)}</title>`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 기본 메타 태그 렌더링
|
|
49
|
+
*/
|
|
50
|
+
export function renderBasicMeta(metadata: ResolvedMetadata): string {
|
|
51
|
+
const tags: string[] = []
|
|
52
|
+
|
|
53
|
+
// description
|
|
54
|
+
if (metadata.description) {
|
|
55
|
+
tags.push(meta('description', metadata.description))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// application-name
|
|
59
|
+
if (metadata.applicationName) {
|
|
60
|
+
tags.push(meta('application-name', metadata.applicationName))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// author
|
|
64
|
+
if (metadata.authors) {
|
|
65
|
+
for (const author of metadata.authors) {
|
|
66
|
+
if (author.name) {
|
|
67
|
+
tags.push(meta('author', author.name))
|
|
68
|
+
}
|
|
69
|
+
if (author.url) {
|
|
70
|
+
const url = typeof author.url === 'string' ? author.url : author.url.href
|
|
71
|
+
tags.push(`<link rel="author" href="${escapeHtml(url)}" />`)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// generator
|
|
77
|
+
if (metadata.generator) {
|
|
78
|
+
tags.push(meta('generator', metadata.generator))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// keywords
|
|
82
|
+
if (metadata.keywords && metadata.keywords.length > 0) {
|
|
83
|
+
tags.push(meta('keywords', metadata.keywords.join(', ')))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// referrer
|
|
87
|
+
if (metadata.referrer) {
|
|
88
|
+
tags.push(meta('referrer', metadata.referrer))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// creator
|
|
92
|
+
if (metadata.creator) {
|
|
93
|
+
tags.push(meta('creator', metadata.creator))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// publisher
|
|
97
|
+
if (metadata.publisher) {
|
|
98
|
+
tags.push(meta('publisher', metadata.publisher))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// robots
|
|
102
|
+
if (metadata.robots?.basic) {
|
|
103
|
+
tags.push(meta('robots', metadata.robots.basic))
|
|
104
|
+
}
|
|
105
|
+
if (metadata.robots?.googleBot) {
|
|
106
|
+
tags.push(meta('googlebot', metadata.robots.googleBot))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// category
|
|
110
|
+
if (metadata.category) {
|
|
111
|
+
tags.push(meta('category', metadata.category))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// classification
|
|
115
|
+
if (metadata.classification) {
|
|
116
|
+
tags.push(meta('classification', metadata.classification))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return tags.join('\n')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Verification 메타 태그 렌더링
|
|
124
|
+
*/
|
|
125
|
+
export function renderVerification(metadata: ResolvedMetadata): string {
|
|
126
|
+
const verification = metadata.verification
|
|
127
|
+
if (!verification) return ''
|
|
128
|
+
|
|
129
|
+
const tags: string[] = []
|
|
130
|
+
|
|
131
|
+
if (verification.google) {
|
|
132
|
+
for (const value of verification.google) {
|
|
133
|
+
tags.push(meta('google-site-verification', value))
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (verification.yahoo) {
|
|
138
|
+
for (const value of verification.yahoo) {
|
|
139
|
+
tags.push(meta('y_key', value))
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (verification.yandex) {
|
|
144
|
+
for (const value of verification.yandex) {
|
|
145
|
+
tags.push(meta('yandex-verification', value))
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (verification.me) {
|
|
150
|
+
for (const value of verification.me) {
|
|
151
|
+
tags.push(meta('me', value))
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (verification.other) {
|
|
156
|
+
for (const [name, values] of Object.entries(verification.other)) {
|
|
157
|
+
for (const value of values) {
|
|
158
|
+
tags.push(meta(name, value))
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return tags.join('\n')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Canonical & Alternates 렌더링
|
|
168
|
+
*/
|
|
169
|
+
export function renderAlternates(metadata: ResolvedMetadata): string {
|
|
170
|
+
const alternates = metadata.alternates
|
|
171
|
+
if (!alternates) return ''
|
|
172
|
+
|
|
173
|
+
const tags: string[] = []
|
|
174
|
+
|
|
175
|
+
// canonical
|
|
176
|
+
if (alternates.canonical) {
|
|
177
|
+
tags.push(`<link rel="canonical" href="${escapeHtml(alternates.canonical.href)}" />`)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// languages (hreflang)
|
|
181
|
+
if (alternates.languages) {
|
|
182
|
+
for (const [lang, url] of Object.entries(alternates.languages)) {
|
|
183
|
+
tags.push(`<link rel="alternate" hreflang="${lang}" href="${escapeHtml(url.href)}" />`)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// media alternates
|
|
188
|
+
if (alternates.media) {
|
|
189
|
+
for (const [media, url] of Object.entries(alternates.media)) {
|
|
190
|
+
tags.push(`<link rel="alternate" media="${escapeHtml(media)}" href="${escapeHtml(url.href)}" />`)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// type alternates
|
|
195
|
+
if (alternates.types) {
|
|
196
|
+
for (const [type, url] of Object.entries(alternates.types)) {
|
|
197
|
+
tags.push(`<link rel="alternate" type="${escapeHtml(type)}" href="${escapeHtml(url.href)}" />`)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return tags.join('\n')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Icons 렌더링
|
|
206
|
+
*/
|
|
207
|
+
export function renderIcons(metadata: ResolvedMetadata): string {
|
|
208
|
+
const icons = metadata.icons
|
|
209
|
+
if (!icons) return ''
|
|
210
|
+
|
|
211
|
+
const tags: string[] = []
|
|
212
|
+
|
|
213
|
+
// icon
|
|
214
|
+
for (const icon of icons.icon) {
|
|
215
|
+
const attrs = [`rel="icon"`, `href="${escapeHtml(urlToString(icon.url))}"`]
|
|
216
|
+
if (icon.type) attrs.push(`type="${escapeHtml(icon.type)}"`)
|
|
217
|
+
if (icon.sizes) attrs.push(`sizes="${escapeHtml(icon.sizes)}"`)
|
|
218
|
+
tags.push(`<link ${attrs.join(' ')} />`)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// apple-touch-icon
|
|
222
|
+
for (const icon of icons.apple) {
|
|
223
|
+
const attrs = [`rel="apple-touch-icon"`, `href="${escapeHtml(urlToString(icon.url))}"`]
|
|
224
|
+
if (icon.sizes) attrs.push(`sizes="${escapeHtml(icon.sizes)}"`)
|
|
225
|
+
tags.push(`<link ${attrs.join(' ')} />`)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// shortcut icon
|
|
229
|
+
for (const icon of icons.shortcut) {
|
|
230
|
+
tags.push(`<link rel="shortcut icon" href="${escapeHtml(urlToString(icon.url))}" />`)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// other icons
|
|
234
|
+
for (const icon of icons.other) {
|
|
235
|
+
const attrs = [`href="${escapeHtml(urlToString(icon.url))}"`]
|
|
236
|
+
if (icon.rel) attrs.push(`rel="${escapeHtml(icon.rel)}"`)
|
|
237
|
+
if (icon.type) attrs.push(`type="${escapeHtml(icon.type)}"`)
|
|
238
|
+
if (icon.sizes) attrs.push(`sizes="${escapeHtml(icon.sizes)}"`)
|
|
239
|
+
if (icon.color) attrs.push(`color="${escapeHtml(icon.color)}"`)
|
|
240
|
+
if (icon.media) attrs.push(`media="${escapeHtml(icon.media)}"`)
|
|
241
|
+
tags.push(`<link ${attrs.join(' ')} />`)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return tags.join('\n')
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Manifest 렌더링
|
|
249
|
+
*/
|
|
250
|
+
export function renderManifest(metadata: ResolvedMetadata): string {
|
|
251
|
+
if (!metadata.manifest) return ''
|
|
252
|
+
return `<link rel="manifest" href="${escapeHtml(metadata.manifest.href)}" />`
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Other custom meta tags 렌더링
|
|
257
|
+
*/
|
|
258
|
+
export function renderOther(metadata: ResolvedMetadata): string {
|
|
259
|
+
if (!metadata.other) return ''
|
|
260
|
+
|
|
261
|
+
const tags: string[] = []
|
|
262
|
+
|
|
263
|
+
for (const [name, value] of Object.entries(metadata.other)) {
|
|
264
|
+
const values = Array.isArray(value) ? value : [value]
|
|
265
|
+
for (const v of values) {
|
|
266
|
+
tags.push(meta(name, String(v)))
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return tags.join('\n')
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ============================================================================
|
|
274
|
+
// Google SEO 최적화 렌더링
|
|
275
|
+
// ============================================================================
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Google 전용 메타 태그 렌더링
|
|
279
|
+
*/
|
|
280
|
+
export function renderGoogle(metadata: ResolvedMetadata): string {
|
|
281
|
+
const google = metadata.google
|
|
282
|
+
if (!google) return ''
|
|
283
|
+
|
|
284
|
+
const tags: string[] = []
|
|
285
|
+
|
|
286
|
+
if (google.nositelinkssearchbox) {
|
|
287
|
+
tags.push(meta('google', 'nositelinkssearchbox'))
|
|
288
|
+
}
|
|
289
|
+
if (google.notranslate) {
|
|
290
|
+
tags.push(meta('google', 'notranslate'))
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return tags.join('\n')
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Format Detection 메타 태그 렌더링
|
|
298
|
+
*/
|
|
299
|
+
export function renderFormatDetection(metadata: ResolvedMetadata): string {
|
|
300
|
+
const fd = metadata.formatDetection
|
|
301
|
+
if (!fd) return ''
|
|
302
|
+
|
|
303
|
+
const values: string[] = []
|
|
304
|
+
|
|
305
|
+
if (fd.telephone === false) values.push('telephone=no')
|
|
306
|
+
if (fd.date === false) values.push('date=no')
|
|
307
|
+
if (fd.address === false) values.push('address=no')
|
|
308
|
+
if (fd.email === false) values.push('email=no')
|
|
309
|
+
|
|
310
|
+
if (values.length === 0) return ''
|
|
311
|
+
return meta('format-detection', values.join(', '))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Theme Color 메타 태그 렌더링
|
|
316
|
+
*/
|
|
317
|
+
export function renderThemeColor(metadata: ResolvedMetadata): string {
|
|
318
|
+
const themeColor = metadata.themeColor
|
|
319
|
+
if (!themeColor || themeColor.length === 0) return ''
|
|
320
|
+
|
|
321
|
+
const tags: string[] = []
|
|
322
|
+
|
|
323
|
+
for (const tc of themeColor) {
|
|
324
|
+
if (tc.media) {
|
|
325
|
+
tags.push(`<meta name="theme-color" content="${escapeHtml(tc.color)}" media="${escapeHtml(tc.media)}" />`)
|
|
326
|
+
} else {
|
|
327
|
+
tags.push(`<meta name="theme-color" content="${escapeHtml(tc.color)}" />`)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return tags.join('\n')
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Viewport 메타 태그 렌더링
|
|
336
|
+
*/
|
|
337
|
+
export function renderViewport(metadata: ResolvedMetadata): string {
|
|
338
|
+
if (!metadata.viewport) return ''
|
|
339
|
+
return meta('viewport', metadata.viewport)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Resource Hints 렌더링 (preconnect, dns-prefetch, preload)
|
|
344
|
+
*/
|
|
345
|
+
export function renderResourceHints(metadata: ResolvedMetadata): string {
|
|
346
|
+
const hints = metadata.resourceHints
|
|
347
|
+
if (!hints) return ''
|
|
348
|
+
|
|
349
|
+
const tags: string[] = []
|
|
350
|
+
|
|
351
|
+
// dns-prefetch
|
|
352
|
+
if (hints.dnsPrefetch) {
|
|
353
|
+
for (const url of hints.dnsPrefetch) {
|
|
354
|
+
tags.push(`<link rel="dns-prefetch" href="${escapeHtml(url)}" />`)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// preconnect
|
|
359
|
+
if (hints.preconnect) {
|
|
360
|
+
for (const url of hints.preconnect) {
|
|
361
|
+
tags.push(`<link rel="preconnect" href="${escapeHtml(url)}" crossorigin />`)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// preload
|
|
366
|
+
if (hints.preload) {
|
|
367
|
+
for (const resource of hints.preload) {
|
|
368
|
+
const attrs = [
|
|
369
|
+
`rel="preload"`,
|
|
370
|
+
`href="${escapeHtml(resource.href)}"`,
|
|
371
|
+
`as="${resource.as}"`,
|
|
372
|
+
]
|
|
373
|
+
if (resource.type) {
|
|
374
|
+
attrs.push(`type="${escapeHtml(resource.type)}"`)
|
|
375
|
+
}
|
|
376
|
+
if (resource.crossOrigin) {
|
|
377
|
+
attrs.push(`crossorigin="${resource.crossOrigin}"`)
|
|
378
|
+
}
|
|
379
|
+
tags.push(`<link ${attrs.join(' ')} />`)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// prefetch
|
|
384
|
+
if (hints.prefetch) {
|
|
385
|
+
for (const url of hints.prefetch) {
|
|
386
|
+
tags.push(`<link rel="prefetch" href="${escapeHtml(url)}" />`)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return tags.join('\n')
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* App Links 메타 태그 렌더링 (iOS/Android 앱 연동)
|
|
395
|
+
*/
|
|
396
|
+
export function renderAppLinks(metadata: ResolvedMetadata): string {
|
|
397
|
+
const al = metadata.appLinks
|
|
398
|
+
if (!al) return ''
|
|
399
|
+
|
|
400
|
+
const tags: string[] = []
|
|
401
|
+
|
|
402
|
+
// iOS
|
|
403
|
+
if (al.iosAppStoreId) {
|
|
404
|
+
tags.push(meta('apple-itunes-app', `app-id=${al.iosAppStoreId}`))
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Android
|
|
408
|
+
if (al.androidPackage) {
|
|
409
|
+
tags.push(metaProperty('al:android:package', al.androidPackage))
|
|
410
|
+
}
|
|
411
|
+
if (al.androidAppName) {
|
|
412
|
+
tags.push(metaProperty('al:android:app_name', al.androidAppName))
|
|
413
|
+
}
|
|
414
|
+
if (al.androidUrl) {
|
|
415
|
+
tags.push(metaProperty('al:android:url', al.androidUrl))
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// iOS App Links
|
|
419
|
+
if (al.iosAppName) {
|
|
420
|
+
tags.push(metaProperty('al:ios:app_name', al.iosAppName))
|
|
421
|
+
}
|
|
422
|
+
if (al.iosUrl) {
|
|
423
|
+
tags.push(metaProperty('al:ios:url', al.iosUrl))
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return tags.join('\n')
|
|
427
|
+
}
|