@timber-js/app 0.1.24 → 0.1.25

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.
Files changed (47) hide show
  1. package/dist/adapters/nitro.d.ts.map +1 -1
  2. package/dist/adapters/nitro.js +4 -3
  3. package/dist/adapters/nitro.js.map +1 -1
  4. package/dist/cli.js +1 -1
  5. package/dist/cli.js.map +1 -1
  6. package/dist/client/browser-dev.d.ts +29 -0
  7. package/dist/client/browser-dev.d.ts.map +1 -0
  8. package/dist/client/browser-links.d.ts +32 -0
  9. package/dist/client/browser-links.d.ts.map +1 -0
  10. package/dist/client/index.d.ts +1 -1
  11. package/dist/client/index.d.ts.map +1 -1
  12. package/dist/client/index.js +46 -20
  13. package/dist/client/index.js.map +1 -1
  14. package/dist/client/navigation-context.d.ts +10 -8
  15. package/dist/client/navigation-context.d.ts.map +1 -1
  16. package/dist/client/transition-root.d.ts +54 -0
  17. package/dist/client/transition-root.d.ts.map +1 -0
  18. package/dist/client/use-router.d.ts +14 -0
  19. package/dist/client/use-router.d.ts.map +1 -1
  20. package/dist/server/index.js +264 -218
  21. package/dist/server/index.js.map +1 -1
  22. package/dist/server/metadata-platform.d.ts +34 -0
  23. package/dist/server/metadata-platform.d.ts.map +1 -0
  24. package/dist/server/metadata-render.d.ts.map +1 -1
  25. package/dist/server/metadata-social.d.ts +24 -0
  26. package/dist/server/metadata-social.d.ts.map +1 -0
  27. package/dist/server/pipeline-interception.d.ts +32 -0
  28. package/dist/server/pipeline-interception.d.ts.map +1 -0
  29. package/dist/server/pipeline-metadata.d.ts +31 -0
  30. package/dist/server/pipeline-metadata.d.ts.map +1 -0
  31. package/dist/server/pipeline.d.ts.map +1 -1
  32. package/package.json +1 -1
  33. package/src/adapters/nitro.ts +9 -7
  34. package/src/cli.ts +9 -2
  35. package/src/client/browser-dev.ts +142 -0
  36. package/src/client/browser-entry.ts +32 -222
  37. package/src/client/browser-links.ts +90 -0
  38. package/src/client/index.ts +1 -1
  39. package/src/client/navigation-context.ts +39 -9
  40. package/src/client/transition-root.tsx +86 -0
  41. package/src/client/use-router.ts +17 -15
  42. package/src/server/metadata-platform.ts +229 -0
  43. package/src/server/metadata-render.ts +9 -363
  44. package/src/server/metadata-social.ts +184 -0
  45. package/src/server/pipeline-interception.ts +76 -0
  46. package/src/server/pipeline-metadata.ts +90 -0
  47. package/src/server/pipeline.ts +2 -148
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Platform-specific metadata rendering — icons, Apple Web App, App Links, iTunes.
3
+ *
4
+ * Extracted from metadata-render.ts to keep files under 500 lines.
5
+ *
6
+ * See design/16-metadata.md
7
+ */
8
+
9
+ import type { Metadata } from './types.js';
10
+ import type { HeadElement } from './metadata.js';
11
+
12
+ /**
13
+ * Render icon link elements (favicon, shortcut, apple-touch-icon, custom).
14
+ */
15
+ export function renderIcons(icons: NonNullable<Metadata['icons']>, elements: HeadElement[]): void {
16
+ // Icon
17
+ if (icons.icon) {
18
+ if (typeof icons.icon === 'string') {
19
+ elements.push({ tag: 'link', attrs: { rel: 'icon', href: icons.icon } });
20
+ } else if (Array.isArray(icons.icon)) {
21
+ for (const icon of icons.icon) {
22
+ const attrs: Record<string, string> = { rel: 'icon', href: icon.url };
23
+ if (icon.sizes) attrs.sizes = icon.sizes;
24
+ if (icon.type) attrs.type = icon.type;
25
+ elements.push({ tag: 'link', attrs });
26
+ }
27
+ }
28
+ }
29
+
30
+ // Shortcut
31
+ if (icons.shortcut) {
32
+ const urls = Array.isArray(icons.shortcut) ? icons.shortcut : [icons.shortcut];
33
+ for (const url of urls) {
34
+ elements.push({ tag: 'link', attrs: { rel: 'shortcut icon', href: url } });
35
+ }
36
+ }
37
+
38
+ // Apple
39
+ if (icons.apple) {
40
+ if (typeof icons.apple === 'string') {
41
+ elements.push({ tag: 'link', attrs: { rel: 'apple-touch-icon', href: icons.apple } });
42
+ } else if (Array.isArray(icons.apple)) {
43
+ for (const icon of icons.apple) {
44
+ const attrs: Record<string, string> = { rel: 'apple-touch-icon', href: icon.url };
45
+ if (icon.sizes) attrs.sizes = icon.sizes;
46
+ elements.push({ tag: 'link', attrs });
47
+ }
48
+ }
49
+ }
50
+
51
+ // Other
52
+ if (icons.other) {
53
+ for (const icon of icons.other) {
54
+ const attrs: Record<string, string> = { rel: icon.rel, href: icon.url };
55
+ if (icon.sizes) attrs.sizes = icon.sizes;
56
+ if (icon.type) attrs.type = icon.type;
57
+ elements.push({ tag: 'link', attrs });
58
+ }
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Render alternate link elements (canonical, hreflang, media, types).
64
+ */
65
+ export function renderAlternates(
66
+ alternates: NonNullable<Metadata['alternates']>,
67
+ elements: HeadElement[]
68
+ ): void {
69
+ if (alternates.canonical) {
70
+ elements.push({ tag: 'link', attrs: { rel: 'canonical', href: alternates.canonical } });
71
+ }
72
+
73
+ if (alternates.languages) {
74
+ for (const [lang, href] of Object.entries(alternates.languages)) {
75
+ elements.push({
76
+ tag: 'link',
77
+ attrs: { rel: 'alternate', hreflang: lang, href },
78
+ });
79
+ }
80
+ }
81
+
82
+ if (alternates.media) {
83
+ for (const [media, href] of Object.entries(alternates.media)) {
84
+ elements.push({
85
+ tag: 'link',
86
+ attrs: { rel: 'alternate', media, href },
87
+ });
88
+ }
89
+ }
90
+
91
+ if (alternates.types) {
92
+ for (const [type, href] of Object.entries(alternates.types)) {
93
+ elements.push({
94
+ tag: 'link',
95
+ attrs: { rel: 'alternate', type, href },
96
+ });
97
+ }
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Render site verification meta tags (Google, Yahoo, Yandex, custom).
103
+ */
104
+ export function renderVerification(
105
+ verification: NonNullable<Metadata['verification']>,
106
+ elements: HeadElement[]
107
+ ): void {
108
+ const verificationProps: Array<[string, string | undefined]> = [
109
+ ['google-site-verification', verification.google],
110
+ ['y_key', verification.yahoo],
111
+ ['yandex-verification', verification.yandex],
112
+ ];
113
+
114
+ for (const [name, content] of verificationProps) {
115
+ if (content) {
116
+ elements.push({ tag: 'meta', attrs: { name, content } });
117
+ }
118
+ }
119
+ if (verification.other) {
120
+ for (const [name, value] of Object.entries(verification.other)) {
121
+ const content = Array.isArray(value) ? value.join(', ') : value;
122
+ elements.push({ tag: 'meta', attrs: { name, content } });
123
+ }
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Render Apple Web App meta tags and startup image links.
129
+ */
130
+ export function renderAppleWebApp(
131
+ appleWebApp: NonNullable<Metadata['appleWebApp']>,
132
+ elements: HeadElement[]
133
+ ): void {
134
+ if (appleWebApp.capable) {
135
+ elements.push({
136
+ tag: 'meta',
137
+ attrs: { name: 'apple-mobile-web-app-capable', content: 'yes' },
138
+ });
139
+ }
140
+ if (appleWebApp.title) {
141
+ elements.push({
142
+ tag: 'meta',
143
+ attrs: { name: 'apple-mobile-web-app-title', content: appleWebApp.title },
144
+ });
145
+ }
146
+ if (appleWebApp.statusBarStyle) {
147
+ elements.push({
148
+ tag: 'meta',
149
+ attrs: {
150
+ name: 'apple-mobile-web-app-status-bar-style',
151
+ content: appleWebApp.statusBarStyle,
152
+ },
153
+ });
154
+ }
155
+ if (appleWebApp.startupImage) {
156
+ const images = Array.isArray(appleWebApp.startupImage)
157
+ ? appleWebApp.startupImage
158
+ : [{ url: appleWebApp.startupImage }];
159
+ for (const img of images) {
160
+ const url = typeof img === 'string' ? img : img.url;
161
+ const attrs: Record<string, string> = { rel: 'apple-touch-startup-image', href: url };
162
+ if (typeof img === 'object' && img.media) {
163
+ attrs.media = img.media;
164
+ }
165
+ elements.push({ tag: 'link', attrs });
166
+ }
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Render App Links (al:*) meta tags for deep linking across platforms.
172
+ */
173
+ export function renderAppLinks(
174
+ appLinks: NonNullable<Metadata['appLinks']>,
175
+ elements: HeadElement[]
176
+ ): void {
177
+ const platformEntries: Array<[string, Array<Record<string, unknown>> | undefined]> = [
178
+ ['ios', appLinks.ios],
179
+ ['android', appLinks.android],
180
+ ['windows', appLinks.windows],
181
+ ['windows_phone', appLinks.windowsPhone],
182
+ ['windows_universal', appLinks.windowsUniversal],
183
+ ];
184
+
185
+ for (const [platform, entries] of platformEntries) {
186
+ if (!entries) continue;
187
+ for (const entry of entries) {
188
+ for (const [key, value] of Object.entries(entry)) {
189
+ if (value !== undefined && value !== null) {
190
+ elements.push({
191
+ tag: 'meta',
192
+ attrs: { property: `al:${platform}:${key}`, content: String(value) },
193
+ });
194
+ }
195
+ }
196
+ }
197
+ }
198
+
199
+ if (appLinks.web) {
200
+ if (appLinks.web.url) {
201
+ elements.push({
202
+ tag: 'meta',
203
+ attrs: { property: 'al:web:url', content: appLinks.web.url },
204
+ });
205
+ }
206
+ if (appLinks.web.shouldFallback !== undefined) {
207
+ elements.push({
208
+ tag: 'meta',
209
+ attrs: {
210
+ property: 'al:web:should_fallback',
211
+ content: appLinks.web.shouldFallback ? 'true' : 'false',
212
+ },
213
+ });
214
+ }
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Render Apple iTunes smart banner meta tag.
220
+ */
221
+ export function renderItunes(itunes: NonNullable<Metadata['itunes']>, elements: HeadElement[]): void {
222
+ const parts = [`app-id=${itunes.appId}`];
223
+ if (itunes.affiliateData) parts.push(`affiliate-data=${itunes.affiliateData}`);
224
+ if (itunes.appArgument) parts.push(`app-argument=${itunes.appArgument}`);
225
+ elements.push({
226
+ tag: 'meta',
227
+ attrs: { name: 'apple-itunes-app', content: parts.join(', ') },
228
+ });
229
+ }
@@ -8,6 +8,15 @@
8
8
 
9
9
  import type { Metadata } from './types.js';
10
10
  import type { HeadElement } from './metadata.js';
11
+ import { renderOpenGraph, renderTwitter } from './metadata-social.js';
12
+ import {
13
+ renderIcons,
14
+ renderAlternates,
15
+ renderVerification,
16
+ renderAppleWebApp,
17
+ renderAppLinks,
18
+ renderItunes,
19
+ } from './metadata-platform.js';
11
20
 
12
21
  // ─── Render to Elements ──────────────────────────────────────────────────────
13
22
 
@@ -162,366 +171,3 @@ function renderRobotsObject(robots: Record<string, unknown>): string {
162
171
  if (robots.follow === false) parts.push('nofollow');
163
172
  return parts.join(', ');
164
173
  }
165
-
166
- function renderOpenGraph(og: NonNullable<Metadata['openGraph']>, elements: HeadElement[]): void {
167
- const simpleProps: Array<[string, string | undefined]> = [
168
- ['og:title', og.title],
169
- ['og:description', og.description],
170
- ['og:url', og.url],
171
- ['og:site_name', og.siteName],
172
- ['og:locale', og.locale],
173
- ['og:type', og.type],
174
- ['og:article:published_time', og.publishedTime],
175
- ['og:article:modified_time', og.modifiedTime],
176
- ];
177
-
178
- for (const [property, content] of simpleProps) {
179
- if (content) {
180
- elements.push({ tag: 'meta', attrs: { property, content } });
181
- }
182
- }
183
-
184
- // Images — normalize single object to array for uniform handling
185
- if (og.images) {
186
- if (typeof og.images === 'string') {
187
- elements.push({ tag: 'meta', attrs: { property: 'og:image', content: og.images } });
188
- } else {
189
- const imgList = Array.isArray(og.images) ? og.images : [og.images];
190
- for (const img of imgList) {
191
- elements.push({ tag: 'meta', attrs: { property: 'og:image', content: img.url } });
192
- if (img.width) {
193
- elements.push({
194
- tag: 'meta',
195
- attrs: { property: 'og:image:width', content: String(img.width) },
196
- });
197
- }
198
- if (img.height) {
199
- elements.push({
200
- tag: 'meta',
201
- attrs: { property: 'og:image:height', content: String(img.height) },
202
- });
203
- }
204
- if (img.alt) {
205
- elements.push({ tag: 'meta', attrs: { property: 'og:image:alt', content: img.alt } });
206
- }
207
- }
208
- }
209
- }
210
-
211
- // Videos
212
- if (og.videos) {
213
- for (const video of og.videos) {
214
- elements.push({ tag: 'meta', attrs: { property: 'og:video', content: video.url } });
215
- }
216
- }
217
-
218
- // Audio
219
- if (og.audio) {
220
- for (const audio of og.audio) {
221
- elements.push({ tag: 'meta', attrs: { property: 'og:audio', content: audio.url } });
222
- }
223
- }
224
-
225
- // Authors
226
- if (og.authors) {
227
- for (const author of og.authors) {
228
- elements.push({
229
- tag: 'meta',
230
- attrs: { property: 'og:article:author', content: author },
231
- });
232
- }
233
- }
234
- }
235
-
236
- function renderTwitter(tw: NonNullable<Metadata['twitter']>, elements: HeadElement[]): void {
237
- const simpleProps: Array<[string, string | undefined]> = [
238
- ['twitter:card', tw.card],
239
- ['twitter:site', tw.site],
240
- ['twitter:site:id', tw.siteId],
241
- ['twitter:title', tw.title],
242
- ['twitter:description', tw.description],
243
- ['twitter:creator', tw.creator],
244
- ['twitter:creator:id', tw.creatorId],
245
- ];
246
-
247
- for (const [name, content] of simpleProps) {
248
- if (content) {
249
- elements.push({ tag: 'meta', attrs: { name, content } });
250
- }
251
- }
252
-
253
- // Images — normalize single object to array for uniform handling
254
- if (tw.images) {
255
- if (typeof tw.images === 'string') {
256
- elements.push({ tag: 'meta', attrs: { name: 'twitter:image', content: tw.images } });
257
- } else {
258
- const imgList = Array.isArray(tw.images) ? tw.images : [tw.images];
259
- for (const img of imgList) {
260
- const url = typeof img === 'string' ? img : img.url;
261
- elements.push({ tag: 'meta', attrs: { name: 'twitter:image', content: url } });
262
- }
263
- }
264
- }
265
-
266
- // Player card fields
267
- if (tw.players) {
268
- for (const player of tw.players) {
269
- elements.push({ tag: 'meta', attrs: { name: 'twitter:player', content: player.playerUrl } });
270
- if (player.width) {
271
- elements.push({
272
- tag: 'meta',
273
- attrs: { name: 'twitter:player:width', content: String(player.width) },
274
- });
275
- }
276
- if (player.height) {
277
- elements.push({
278
- tag: 'meta',
279
- attrs: { name: 'twitter:player:height', content: String(player.height) },
280
- });
281
- }
282
- if (player.streamUrl) {
283
- elements.push({
284
- tag: 'meta',
285
- attrs: { name: 'twitter:player:stream', content: player.streamUrl },
286
- });
287
- }
288
- }
289
- }
290
-
291
- // App card fields — 3 platforms × 3 attributes (name, id, url)
292
- if (tw.app) {
293
- const platforms: Array<[keyof NonNullable<typeof tw.app.id>, string]> = [
294
- ['iPhone', 'iphone'],
295
- ['iPad', 'ipad'],
296
- ['googlePlay', 'googleplay'],
297
- ];
298
-
299
- // App name is shared across platforms but the spec uses per-platform names.
300
- // Emit for each platform that has an ID.
301
- if (tw.app.name) {
302
- for (const [key, tag] of platforms) {
303
- if (tw.app.id?.[key]) {
304
- elements.push({
305
- tag: 'meta',
306
- attrs: { name: `twitter:app:name:${tag}`, content: tw.app.name },
307
- });
308
- }
309
- }
310
- }
311
-
312
- for (const [key, tag] of platforms) {
313
- const id = tw.app.id?.[key];
314
- if (id) {
315
- elements.push({ tag: 'meta', attrs: { name: `twitter:app:id:${tag}`, content: id } });
316
- }
317
- }
318
-
319
- for (const [key, tag] of platforms) {
320
- const url = tw.app.url?.[key];
321
- if (url) {
322
- elements.push({ tag: 'meta', attrs: { name: `twitter:app:url:${tag}`, content: url } });
323
- }
324
- }
325
- }
326
- }
327
-
328
- function renderIcons(icons: NonNullable<Metadata['icons']>, elements: HeadElement[]): void {
329
- // Icon
330
- if (icons.icon) {
331
- if (typeof icons.icon === 'string') {
332
- elements.push({ tag: 'link', attrs: { rel: 'icon', href: icons.icon } });
333
- } else if (Array.isArray(icons.icon)) {
334
- for (const icon of icons.icon) {
335
- const attrs: Record<string, string> = { rel: 'icon', href: icon.url };
336
- if (icon.sizes) attrs.sizes = icon.sizes;
337
- if (icon.type) attrs.type = icon.type;
338
- elements.push({ tag: 'link', attrs });
339
- }
340
- }
341
- }
342
-
343
- // Shortcut
344
- if (icons.shortcut) {
345
- const urls = Array.isArray(icons.shortcut) ? icons.shortcut : [icons.shortcut];
346
- for (const url of urls) {
347
- elements.push({ tag: 'link', attrs: { rel: 'shortcut icon', href: url } });
348
- }
349
- }
350
-
351
- // Apple
352
- if (icons.apple) {
353
- if (typeof icons.apple === 'string') {
354
- elements.push({ tag: 'link', attrs: { rel: 'apple-touch-icon', href: icons.apple } });
355
- } else if (Array.isArray(icons.apple)) {
356
- for (const icon of icons.apple) {
357
- const attrs: Record<string, string> = { rel: 'apple-touch-icon', href: icon.url };
358
- if (icon.sizes) attrs.sizes = icon.sizes;
359
- elements.push({ tag: 'link', attrs });
360
- }
361
- }
362
- }
363
-
364
- // Other
365
- if (icons.other) {
366
- for (const icon of icons.other) {
367
- const attrs: Record<string, string> = { rel: icon.rel, href: icon.url };
368
- if (icon.sizes) attrs.sizes = icon.sizes;
369
- if (icon.type) attrs.type = icon.type;
370
- elements.push({ tag: 'link', attrs });
371
- }
372
- }
373
- }
374
-
375
- function renderAlternates(
376
- alternates: NonNullable<Metadata['alternates']>,
377
- elements: HeadElement[]
378
- ): void {
379
- if (alternates.canonical) {
380
- elements.push({ tag: 'link', attrs: { rel: 'canonical', href: alternates.canonical } });
381
- }
382
-
383
- if (alternates.languages) {
384
- for (const [lang, href] of Object.entries(alternates.languages)) {
385
- elements.push({
386
- tag: 'link',
387
- attrs: { rel: 'alternate', hreflang: lang, href },
388
- });
389
- }
390
- }
391
-
392
- if (alternates.media) {
393
- for (const [media, href] of Object.entries(alternates.media)) {
394
- elements.push({
395
- tag: 'link',
396
- attrs: { rel: 'alternate', media, href },
397
- });
398
- }
399
- }
400
-
401
- if (alternates.types) {
402
- for (const [type, href] of Object.entries(alternates.types)) {
403
- elements.push({
404
- tag: 'link',
405
- attrs: { rel: 'alternate', type, href },
406
- });
407
- }
408
- }
409
- }
410
-
411
- function renderVerification(
412
- verification: NonNullable<Metadata['verification']>,
413
- elements: HeadElement[]
414
- ): void {
415
- const verificationProps: Array<[string, string | undefined]> = [
416
- ['google-site-verification', verification.google],
417
- ['y_key', verification.yahoo],
418
- ['yandex-verification', verification.yandex],
419
- ];
420
-
421
- for (const [name, content] of verificationProps) {
422
- if (content) {
423
- elements.push({ tag: 'meta', attrs: { name, content } });
424
- }
425
- }
426
- if (verification.other) {
427
- for (const [name, value] of Object.entries(verification.other)) {
428
- const content = Array.isArray(value) ? value.join(', ') : value;
429
- elements.push({ tag: 'meta', attrs: { name, content } });
430
- }
431
- }
432
- }
433
-
434
- function renderAppleWebApp(
435
- appleWebApp: NonNullable<Metadata['appleWebApp']>,
436
- elements: HeadElement[]
437
- ): void {
438
- if (appleWebApp.capable) {
439
- elements.push({
440
- tag: 'meta',
441
- attrs: { name: 'apple-mobile-web-app-capable', content: 'yes' },
442
- });
443
- }
444
- if (appleWebApp.title) {
445
- elements.push({
446
- tag: 'meta',
447
- attrs: { name: 'apple-mobile-web-app-title', content: appleWebApp.title },
448
- });
449
- }
450
- if (appleWebApp.statusBarStyle) {
451
- elements.push({
452
- tag: 'meta',
453
- attrs: {
454
- name: 'apple-mobile-web-app-status-bar-style',
455
- content: appleWebApp.statusBarStyle,
456
- },
457
- });
458
- }
459
- if (appleWebApp.startupImage) {
460
- const images = Array.isArray(appleWebApp.startupImage)
461
- ? appleWebApp.startupImage
462
- : [{ url: appleWebApp.startupImage }];
463
- for (const img of images) {
464
- const url = typeof img === 'string' ? img : img.url;
465
- const attrs: Record<string, string> = { rel: 'apple-touch-startup-image', href: url };
466
- if (typeof img === 'object' && img.media) {
467
- attrs.media = img.media;
468
- }
469
- elements.push({ tag: 'link', attrs });
470
- }
471
- }
472
- }
473
-
474
- function renderAppLinks(
475
- appLinks: NonNullable<Metadata['appLinks']>,
476
- elements: HeadElement[]
477
- ): void {
478
- const platformEntries: Array<[string, Array<Record<string, unknown>> | undefined]> = [
479
- ['ios', appLinks.ios],
480
- ['android', appLinks.android],
481
- ['windows', appLinks.windows],
482
- ['windows_phone', appLinks.windowsPhone],
483
- ['windows_universal', appLinks.windowsUniversal],
484
- ];
485
-
486
- for (const [platform, entries] of platformEntries) {
487
- if (!entries) continue;
488
- for (const entry of entries) {
489
- for (const [key, value] of Object.entries(entry)) {
490
- if (value !== undefined && value !== null) {
491
- elements.push({
492
- tag: 'meta',
493
- attrs: { property: `al:${platform}:${key}`, content: String(value) },
494
- });
495
- }
496
- }
497
- }
498
- }
499
-
500
- if (appLinks.web) {
501
- if (appLinks.web.url) {
502
- elements.push({
503
- tag: 'meta',
504
- attrs: { property: 'al:web:url', content: appLinks.web.url },
505
- });
506
- }
507
- if (appLinks.web.shouldFallback !== undefined) {
508
- elements.push({
509
- tag: 'meta',
510
- attrs: {
511
- property: 'al:web:should_fallback',
512
- content: appLinks.web.shouldFallback ? 'true' : 'false',
513
- },
514
- });
515
- }
516
- }
517
- }
518
-
519
- function renderItunes(itunes: NonNullable<Metadata['itunes']>, elements: HeadElement[]): void {
520
- const parts = [`app-id=${itunes.appId}`];
521
- if (itunes.affiliateData) parts.push(`affiliate-data=${itunes.affiliateData}`);
522
- if (itunes.appArgument) parts.push(`app-argument=${itunes.appArgument}`);
523
- elements.push({
524
- tag: 'meta',
525
- attrs: { name: 'apple-itunes-app', content: parts.join(', ') },
526
- });
527
- }