@stnd/layout 0.2.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Meta.astro CHANGED
@@ -1,260 +1,230 @@
1
1
  ---
2
2
  /**
3
- * Meta.astro - Universal Meta Layout
3
+ * @component Meta
4
+ * @description Universal `<head>` meta tags — canonical link, description,
5
+ * Open Graph, Twitter Cards, Schema.org JSON-LD, favicons, theme-color, and
6
+ * mobile/Windows tile tags. Self-contained and zero-config: reads site
7
+ * defaults from `virtual:stnd/config`, with every prop overriding its
8
+ * matching config value for the current page only.
4
9
  *
5
- * A flexible meta tag component for any Astro project.
6
- * Supports standard meta tags, Open Graph, Twitter Cards, and Schema.org.
10
+ * @example astro
11
+ * <Meta />
12
+ * <Meta title="My Page" />
13
+ * <Meta title="Article" type="article" created="2024-01-01" />
7
14
  */
8
- import { getExcerpt } from "@stnd/utils";
9
- // import { ClientRouter } from "astro:transitions";
15
+ import config from "virtual:stnd/config";
10
16
 
11
- // Access site configuration from global
12
- const config = (globalThis['__STANDARD_CONFIG__'] as any) || {};
13
-
14
- // Props interface for universal usage
15
17
  interface Props {
16
- // Core metadata
17
- frontmatter?: any;
18
- site?: any;
19
- page?: any;
20
- title?: string;
21
- description?: string;
22
- image?: string;
23
- created?: string;
24
- modified?: string;
25
- tags?: string[];
26
- securityHeaders?: any;
27
-
28
- // Advanced options
29
- dataSource?: any; // Optional data object with toViewModel() method for dynamic content
30
- rssFeedUrl?: string | null; // Custom RSS feed URL
31
- rssFeedTitle?: string | undefined; // Custom RSS feed title
32
- enableClientRouter?: boolean; // Enable Astro view transitions
33
- injectState?: boolean; // Inject state to window for client-side hydration
34
- faviconPath?: string; // Custom favicon path (defaults to /favicons or /assets/favicon)
35
- fontsUrl?: string; // Optional external fonts URL
36
- language?: string; // Page language override
37
- headExtensions?: string | string[]; // Plugin head extensions
18
+ // Per-page metadata (overrides config defaults)
19
+ title?: string;
20
+ description?: string;
21
+ image?: string;
22
+ author?: string;
23
+ language?: string;
24
+ created?: string;
25
+ modified?: string;
26
+ tags?: string[];
27
+
28
+ /** Controls `og:type` and the Schema.org `@type` (article → BlogPosting, profile → ProfilePage, else WebSite). Default: "website" */
29
+ type?: "website" | "article" | "profile";
30
+
31
+ // Appearance
32
+ /** `theme-color` meta tag. A single string is used for both color schemes; pass `{light, dark}` to vary by `prefers-color-scheme`. */
33
+ themeColor?: string | { light: string; dark: string };
34
+ /** `msapplication-TileColor` for Windows tiles. Default: "#ffffff" */
35
+ tileColor?: string;
36
+
37
+ /** "private"/"unlisted" emit `noindex, nofollow`; "public" (default) allows indexing. */
38
+ visibility?: "public" | "private" | "unlisted";
39
+
40
+ // Optional features
41
+ rssFeedUrl?: string | null;
42
+ rssFeedTitle?: string;
43
+ /** Path prefix for the generated favicon package (.ico, PNG sizes, browserconfig.xml). Default: "/assets/favicon" */
44
+ faviconPath?: string;
45
+ /**
46
+ * A single image URL used as *the* site icon, replacing the whole
47
+ * `faviconPath` set. Takes precedence over `faviconPath` when both are given.
48
+ * Modern browsers are happy with one SVG or PNG; use `faviconPath` when you
49
+ * have a full generated package (.ico, apple-touch-icon, browserconfig…).
50
+ */
51
+ favicon?: string | null;
52
+ headExtensions?: string | string[];
53
+
54
+ /** Absolute origin overriding `Astro.site` (a build-time constant) — pass this when the actual per-request origin, e.g. a user's verified custom domain, differs from the build's default site. */
55
+ siteUrl?: string;
38
56
  }
39
57
 
40
58
  const {
41
- frontmatter,
42
- site: propsSite,
43
- page = {},
44
- title: propsTitle,
45
- description: propsDescription,
46
- image: propsImage,
47
- created: propsCreated,
48
- modified: propsModified,
49
- tags: propsTags,
50
- securityHeaders: propsSecurityHeaders,
51
- dataSource,
52
- rssFeedUrl,
53
- rssFeedTitle,
54
- enableClientRouter = false,
55
- injectState = false,
56
- faviconPath,
57
- fontsUrl,
58
- language: propsLanguage,
59
- headExtensions,
60
- // Backward compatibility (deprecated)
61
- note,
62
- injectVisitorState,
63
- } = Astro.props as Props & { note?: any; injectVisitorState?: boolean };
64
-
65
- // Backward compatibility: map old prop names to new ones
66
- const effectiveDataSource = dataSource || note;
67
- const effectiveInjectState = injectState || injectVisitorState || false;
68
-
69
- // Access context data if available (framework-agnostic)
70
- const contextData = (Astro.locals as any)?.garden || (Astro.locals as any)?.context;
71
- const dataView = effectiveDataSource && typeof effectiveDataSource.toViewModel === 'function'
72
- ? await effectiveDataSource.toViewModel()
73
- : null;
74
-
75
- // Build site configuration (prefer props, fallback to config)
76
- const site = propsSite || config?.site || config || {
77
- title: "Site Title",
78
- url: Astro.site || "",
79
- };
80
-
81
- // Get user preferences if context is available
82
- const userPrefs = contextData?.visitor?.raw?.preferences || contextData?.preferences || {
83
- theme: "default",
84
- colorMode: "light",
85
- uiLanguage: "en",
86
- };
87
-
88
- // Extract frontmatter from data source if available
89
- const sourceFrontmatter = dataView?.frontmatter || effectiveDataSource?.raw?.frontmatter || {};
90
- const effectiveFrontmatter = { ...frontmatter, ...sourceFrontmatter };
91
-
92
- // Build metadata with dynamic data support
93
- const ogData = dataView?.og || {};
94
- const title = propsTitle || ogData.title || effectiveFrontmatter?.title || config?.title;
95
- const description = propsDescription || ogData.description || effectiveFrontmatter?.description || config?.description;
96
- const image = propsImage || ogData.image || effectiveFrontmatter?.image;
97
- const created = propsCreated || effectiveFrontmatter?.created;
98
- const modified = propsModified || effectiveFrontmatter?.modified;
99
- const tags = propsTags || effectiveFrontmatter?.tags;
100
- const securityHeaders = propsSecurityHeaders || effectiveFrontmatter?.securityHeaders || site?.securityHeaders;
101
-
102
- const fullTitle = title && title !== site.title ? `${title} · ${site.title}` : site.title;
103
-
104
- // Ensure we have a valid base URL
105
- const baseUrl = site.url || config?.url || Astro.site || 'http://localhost:4321';
106
- const pageUrl = new URL(Astro.url.pathname, baseUrl);
59
+ title: propsTitle,
60
+ description: propsDescription,
61
+ image: propsImage,
62
+ author: propsAuthor,
63
+ language: propsLanguage,
64
+ created: propsCreated,
65
+ modified: propsModified,
66
+ tags: propsTags,
67
+ type = "website",
68
+ themeColor,
69
+ tileColor,
70
+ visibility = "public",
71
+ rssFeedUrl,
72
+ rssFeedTitle,
73
+ faviconPath,
74
+ favicon,
75
+ headExtensions,
76
+ siteUrl: propsSiteUrl,
77
+ } = Astro.props as Props;
78
+
79
+ // ── Site defaults from standard() config ──
80
+ const siteTitle = config.title || "";
81
+ const siteDescription = config.description || "";
82
+ const siteAuthor = config.author?.name || "";
83
+ const siteLanguage = config.language || "en";
84
+
85
+ // ── Resolved metadata — props override config ──
86
+ const title = propsTitle || siteTitle;
87
+ const description = propsDescription || siteDescription;
88
+ const authorName = propsAuthor || siteAuthor;
89
+ const pageLanguage = propsLanguage || siteLanguage;
90
+ const image = propsImage;
91
+ const created = propsCreated;
92
+ const modified = propsModified;
93
+ const tags = propsTags;
94
+
95
+ // ── URL construction ──
96
+ const baseUrl = (propsSiteUrl || Astro.site?.toString() || "").replace(/\/$/, "");
97
+ const pageUrl = new URL(Astro.url.pathname, baseUrl || "http://localhost:4321");
107
98
  const canonicalUrl = new URL(
108
- Astro.url.pathname.replace(/index\.html$/, ""),
109
- baseUrl,
99
+ Astro.url.pathname.replace(/index\.html$/, ""),
100
+ baseUrl || "http://localhost:4321",
110
101
  );
111
102
 
112
- // Description logic
113
- const pageExcerpt = page.rawInput ? String(page.rawInput) : "";
114
- const metaDescription = (description ||
115
- getExcerpt(pageExcerpt, { maxChars: 150 }) ||
116
- site.description ||
117
- "").toString();
103
+ // ── Description ──
104
+ const metaDescription = (description || "").toString();
118
105
 
119
- // Image logic
106
+ // ── Image ──
120
107
  const imageUrl = image
121
- ? `${String(site.url)}/assets/img/${image}`
122
- : ogData.image || (site.url
123
- ? `${String(site.url).replace(/\/$/, "")}/assets/img/default.png`
124
- : "/assets/img/default.png");
125
-
126
- const imageAlt = title || site.title;
127
-
128
- // Date logic
129
- const datePublished = ogData.date
130
- ? new Date(ogData.date).toISOString()
131
- : created
132
- ? new Date(created).toISOString()
133
- : dataView?.date
134
- ? new Date(dataView.date).toISOString()
135
- : null;
136
- const dateModified = modified || created
137
- ? new Date(modified || created).toISOString()
108
+ ? image.startsWith("http")
109
+ ? image
110
+ : `${baseUrl}/assets/img/${image}`
111
+ : baseUrl
112
+ ? `${baseUrl}/assets/img/default.png`
113
+ : "/assets/img/default.png";
114
+ const imageAlt = title || "";
115
+
116
+ // ── Dates ──
117
+ const datePublished = created ? new Date(created).toISOString() : null;
118
+ const dateModified = modified
119
+ ? new Date(modified).toISOString()
120
+ : created
121
+ ? new Date(created).toISOString()
138
122
  : null;
139
123
 
140
- const authorName = ogData.author || site.author?.name || site.author || "";
141
- const twitterUsername = site.twitter_username || site.twitter || "";
142
-
143
- // Determine language
144
- const pageLanguage = propsLanguage || userPrefs.uiLanguage || site.language || "en";
145
-
146
- // Determine favicon path structure
147
- const usesAssetsFavicon = faviconPath === "/assets/favicon" || (contextData && !faviconPath);
148
- const faviconBasePath = faviconPath || (usesAssetsFavicon ? "/assets/favicon" : `${site.url || ""}/favicons`);
124
+ // ── Favicon ──
125
+ // A single `favicon` image replaces the generated package entirely. Anything
126
+ // else falls back to the `faviconPath` set (.ico + PNG sizes + browserconfig).
127
+ const faviconBase = faviconPath || "/assets/favicon";
128
+ const singleFavicon =
129
+ typeof favicon === "string" && favicon.trim() !== "" ? favicon.trim() : null;
130
+
131
+ const FAVICON_MIME = {
132
+ svg: "image/svg+xml",
133
+ png: "image/png",
134
+ ico: "image/x-icon",
135
+ gif: "image/gif",
136
+ jpg: "image/jpeg",
137
+ jpeg: "image/jpeg",
138
+ webp: "image/webp",
139
+ avif: "image/avif",
140
+ };
149
141
 
150
- // Process head extensions
151
- const headExtraHtml = Array.isArray(headExtensions) ? headExtensions.join("\n") : (headExtensions || "");
142
+ // The extension can carry a query string (`/cdn/logo.svg?v=2`); strip it before
143
+ // matching, and leave `type` off entirely when the URL gives us nothing to go on.
144
+ const singleFaviconType = singleFavicon
145
+ ? (FAVICON_MIME[
146
+ (singleFavicon.split(/[?#]/)[0].split(".").pop() || "").toLowerCase()
147
+ ] ?? undefined)
148
+ : undefined;
149
+
150
+ // ── Theme colors ──
151
+ const resolvedThemeColor =
152
+ typeof themeColor === "string"
153
+ ? { light: themeColor, dark: themeColor }
154
+ : themeColor || { light: "#ffffff", dark: "#1a1a1a" };
155
+ const resolvedTileColor = tileColor || "#ffffff";
156
+
157
+ // ── OG & Schema.org types ──
158
+ const ogType = type;
159
+ const schemaType =
160
+ type === "article"
161
+ ? "BlogPosting"
162
+ : type === "profile"
163
+ ? "ProfilePage"
164
+ : "WebSite";
165
+
166
+ // ── Head extensions ──
167
+ const headExtraHtml = Array.isArray(headExtensions)
168
+ ? headExtensions.join("\n")
169
+ : headExtensions || "";
152
170
  ---
153
171
 
154
172
  <meta charset="utf-8" />
155
173
  {headExtraHtml && <Fragment set:html={headExtraHtml} />}
156
- <!-- Standard Framework | https://standard.ffp.co -->
157
174
 
158
175
  <meta
159
- name="viewport"
160
- content="width=device-width, initial-scale=1, viewport-fit=cover"
176
+ name="viewport"
177
+ content="width=device-width, initial-scale=1, viewport-fit=cover"
161
178
  />
162
179
 
163
- <!-- iOS PWA support for safe-area-inset -->
164
- {contextData && (
165
- <>
166
- <meta name="mobile-web-app-capable" content="yes" />
167
- <meta
168
- name="apple-mobile-web-app-status-bar-style"
169
- content="black-translucent"
170
- />
171
- </>
172
- )}
173
-
174
- {fontsUrl && (
175
- <!-- External fonts -->
176
- <link rel="stylesheet" href={fontsUrl} />
177
- )}
178
-
179
- <title>{fullTitle}</title>
180
+ <title>{title}</title>
180
181
  <meta name="description" content={metaDescription} />
181
- <meta name="author" content={authorName} />
182
+ {authorName && <meta name="author" content={authorName} />}
182
183
  <meta name="language" content={pageLanguage} />
183
184
  <meta name="referrer" content="strict-origin-when-cross-origin" />
184
185
 
185
- <!-- Canonical URL -->
186
186
  <link rel="canonical" href={canonicalUrl} />
187
187
 
188
- <!-- Feed & Sitemap -->
189
188
  {
190
- (site?.url || rssFeedUrl) ? (
191
- <>
192
- <link
193
- rel="alternate"
194
- type="application/atom+xml"
195
- title={rssFeedTitle || site.title}
196
- href={rssFeedUrl || `${site.url}/feed.xml`}
197
- />
198
- {site?.url && (
199
- <link
200
- rel="sitemap"
201
- type="application/xml"
202
- title="Sitemap"
203
- href={`${site.url}/sitemap.xml`}
204
- />
205
- )}
206
- </>
207
- ) : null
189
+ rssFeedUrl && (
190
+ <link
191
+ rel="alternate"
192
+ type="application/atom+xml"
193
+ title={rssFeedTitle || title}
194
+ href={rssFeedUrl}
195
+ />
196
+ )
208
197
  }
209
198
 
210
199
  {
211
- securityHeaders && (
212
- <>
213
- {securityHeaders.csp && (
214
- <meta
215
- http-equiv="Content-Security-Policy"
216
- content={securityHeaders.csp}
217
- />
218
- )}
219
- {securityHeaders.referrerPolicy && (
220
- <meta
221
- http-equiv="Referrer-Policy"
222
- content={securityHeaders.referrerPolicy}
223
- />
224
- )}
225
- {securityHeaders.permissions && (
226
- <meta
227
- http-equiv="Permissions-Policy"
228
- content={securityHeaders.permissions}
229
- />
230
- )}
231
- {securityHeaders.xContentTypeOptions && (
232
- <meta
233
- http-equiv="X-Content-Type-Options"
234
- content={securityHeaders.xContentTypeOptions}
235
- />
236
- )}
237
- </>
238
- )
200
+ baseUrl && (
201
+ <link
202
+ rel="sitemap"
203
+ type="application/xml"
204
+ title="Sitemap"
205
+ href={`${baseUrl}/sitemap.xml`}
206
+ />
207
+ )
239
208
  }
240
209
 
241
- <!-- Robots Meta -->
210
+ <!-- CSP is handled natively by Astro 6 via security.csp config -->
211
+
242
212
  {
243
- site?.visibility === "private" ? (
244
- <meta name="robots" content="noindex, nofollow" />
245
- ) : (
246
- <meta
247
- name="robots"
248
- content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"
249
- />
250
- )
213
+ visibility === "private" || visibility === "unlisted" ? (
214
+ <meta name="robots" content="noindex, nofollow" />
215
+ ) : (
216
+ <meta
217
+ name="robots"
218
+ content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"
219
+ />
220
+ )
251
221
  }
252
222
 
253
223
  <!-- Open Graph -->
254
- <meta property="og:title" content={title || site.title} />
255
- <meta property="og:type" content={title ? "article" : "website"} />
224
+ <meta property="og:title" content={title} />
225
+ <meta property="og:type" content={ogType} />
256
226
  <meta property="og:locale" content={pageLanguage} />
257
- <meta property="og:site_name" content={site.title} />
227
+ {siteTitle && <meta property="og:site_name" content={siteTitle} />}
258
228
  <meta property="og:url" content={pageUrl} />
259
229
  <meta property="og:description" content={metaDescription} />
260
230
 
@@ -264,139 +234,122 @@ const headExtraHtml = Array.isArray(headExtensions) ? headExtensions.join("\n")
264
234
  <meta property="og:image:height" content="630" />
265
235
 
266
236
  {
267
- datePublished && (
268
- <>
269
- <meta property="article:published_time" content={datePublished} />
270
- <meta property="article:author" content={authorName} />
271
- {tags &&
272
- tags.map((tag: string) => (
273
- <meta property="article:tag" content={tag} />
274
- ))}
275
- </>
276
- )
237
+ type === "article" && datePublished && (
238
+ <>
239
+ <meta property="article:published_time" content={datePublished} />
240
+ {authorName && <meta property="article:author" content={authorName} />}
241
+ {tags &&
242
+ tags.map((tag: string) => (
243
+ <meta property="article:tag" content={tag} />
244
+ ))}
245
+ </>
246
+ )
277
247
  }
278
248
 
279
249
  <!-- Twitter Cards -->
280
- <meta name="twitter:card" content={imageUrl ? "summary_large_image" : "summary"} />
250
+ <meta name="twitter:card" content={image ? "summary_large_image" : "summary"} />
281
251
  <meta name="twitter:image" content={imageUrl} />
282
252
  <meta name="twitter:image:alt" content={imageAlt} />
283
- <meta name="twitter:site" content={`@${twitterUsername}`} />
284
- <meta name="twitter:creator" content={`@${twitterUsername}`} />
285
- <meta name="twitter:title" content={fullTitle} />
253
+ <meta name="twitter:title" content={title} />
286
254
  <meta name="twitter:description" content={metaDescription} />
287
255
 
288
256
  <!-- Schema.org -->
289
257
  <script
290
- type="application/ld+json"
291
- set:html={JSON.stringify({
292
- "@context": "https://schema.org",
293
- "@type": title ? "BlogPosting" : "WebSite",
294
- ...(title
258
+ type="application/ld+json"
259
+ set:html={JSON.stringify({
260
+ "@context": "https://schema.org",
261
+ "@type": schemaType,
262
+ ...(type === "article"
263
+ ? {
264
+ headline: title,
265
+ description: metaDescription,
266
+ ...(authorName
267
+ ? {
268
+ author: { "@type": "Person", name: authorName },
269
+ publisher: { "@type": "Person", name: authorName },
270
+ }
271
+ : {}),
272
+ ...(datePublished ? { datePublished } : {}),
273
+ ...(dateModified ? { dateModified } : {}),
274
+ image: {
275
+ "@type": "ImageObject",
276
+ url: imageUrl,
277
+ width: 1200,
278
+ height: 630,
279
+ },
280
+ mainEntityOfPage: { "@type": "WebPage", "@id": pageUrl },
281
+ }
282
+ : {
283
+ name: siteTitle || title,
284
+ description: metaDescription,
285
+ url: baseUrl,
286
+ ...(authorName
295
287
  ? {
296
- headline: title,
297
- description: metaDescription,
298
- author: {
299
- "@type": "Person",
300
- name: authorName,
301
- url: `${site.url}/now/`,
302
- },
303
- publisher: {
304
- "@type": "Person",
305
- name: authorName,
306
- url: site.url,
307
- },
308
- ...(datePublished ? { datePublished } : {}),
309
- ...(dateModified ? { dateModified } : {}),
310
- image: {
311
- "@type": "ImageObject",
312
- url: imageUrl,
313
- width: 1200,
314
- height: 630,
315
- },
316
- mainEntityOfPage: {
317
- "@type": "WebPage",
318
- "@id": pageUrl,
319
- },
288
+ author: { "@type": "Person", name: authorName },
320
289
  }
321
- : {
322
- name: site.title,
323
- description: site.description,
324
- url: site.url,
325
- author: {
326
- "@type": "Person",
327
- name: authorName,
328
- url: site.url,
329
- },
330
- }),
331
- })}
290
+ : {}),
291
+ }),
292
+ })}
332
293
  />
333
294
 
334
295
  <!-- Favicons -->
335
- {usesAssetsFavicon ? (
336
- <>
337
- <link
338
- rel="icon"
339
- type="image/png"
340
- href={`${faviconBasePath}/favicon-96x96.png`}
341
- sizes="96x96"
342
- />
343
- <link rel="icon" type="image/svg+xml" href={`${faviconBasePath}/favicon.svg`} />
344
- <link rel="shortcut icon" href={`${faviconBasePath}/favicon.ico`} />
345
- <link
346
- rel="apple-touch-icon"
347
- sizes="180x180"
348
- href={`${faviconBasePath}/apple-touch-icon.png`}
349
- />
350
- <meta name="apple-mobile-web-app-title" content={site.title} />
351
- <link rel="manifest" href={`${faviconBasePath}/site.webmanifest`} />
352
- </>
353
- ) : (
354
- <>
355
- <link
356
- rel="apple-touch-icon"
357
- sizes="180x180"
358
- href={`${faviconBasePath}/apple-touch-icon.png`}
359
- />
360
- <link
361
- rel="icon"
362
- type="image/png"
363
- sizes="32x32"
364
- href={`${faviconBasePath}/favicon-32x32.png`}
365
- />
366
- <link
367
- rel="icon"
368
- type="image/png"
369
- sizes="16x16"
370
- href={`${faviconBasePath}/favicon-16x16.png`}
371
- />
372
- <link rel="manifest" href="/site.webmanifest" />
373
- <link
374
- rel="mask-icon"
375
- href={`${faviconBasePath}/safari-pinned-tab.svg`}
376
- color="#000000"
377
- />
378
- <link rel="shortcut icon" href={`${faviconBasePath}/favicon.ico`} />
379
- </>
380
- )}
296
+ {
297
+ singleFavicon ? (
298
+ <Fragment>
299
+ <link rel="icon" type={singleFaviconType} href={singleFavicon} />
300
+ <link rel="apple-touch-icon" href={singleFavicon} />
301
+ </Fragment>
302
+ ) : (
303
+ <Fragment>
304
+ <link
305
+ rel="icon"
306
+ type="image/png"
307
+ href={`${faviconBase}/favicon-96x96.png`}
308
+ sizes="96x96"
309
+ />
310
+ <link
311
+ rel="icon"
312
+ type="image/svg+xml"
313
+ href={`${faviconBase}/favicon.svg`}
314
+ />
315
+ <link rel="shortcut icon" href={`${faviconBase}/favicon.ico`} />
316
+ <link
317
+ rel="apple-touch-icon"
318
+ sizes="180x180"
319
+ href={`${faviconBase}/apple-touch-icon.png`}
320
+ />
321
+ </Fragment>
322
+ )
323
+ }
324
+ <meta name="apple-mobile-web-app-title" content={siteTitle || title || ""} />
325
+ <link rel="manifest" href="/site.webmanifest" />
381
326
 
382
327
  <!-- Mobile -->
383
328
  <meta
384
- name="theme-color"
385
- content="#ffffff"
386
- media="(prefers-color-scheme: light)"
329
+ name="theme-color"
330
+ content={resolvedThemeColor.light}
331
+ media="(prefers-color-scheme: light)"
387
332
  />
388
333
  <meta
389
- name="theme-color"
390
- content="#000000"
391
- media="(prefers-color-scheme: dark)"
334
+ name="theme-color"
335
+ content={resolvedThemeColor.dark}
336
+ media="(prefers-color-scheme: dark)"
392
337
  />
393
-
394
- <!-- Windows Tiles -->
395
- <meta name="msapplication-TileColor" content="#000000" />
338
+ <meta name="mobile-web-app-capable" content="yes" />
396
339
  <meta
397
- name="msapplication-config"
398
- content={`${site.url || ""}/favicons/browserconfig.xml`}
340
+ name="apple-mobile-web-app-status-bar-style"
341
+ content="black-translucent"
399
342
  />
400
343
 
401
- <!-- Client Router (Astro view transitions) -->
402
- <!-- {enableClientRouter && <ClientRouter />} -->
344
+ <!-- Windows Tiles -->
345
+ <meta name="msapplication-TileColor" content={resolvedTileColor} />
346
+ {
347
+ /* Only meaningful alongside the generated package — a single `favicon`
348
+ has no browserconfig.xml to point at. */
349
+ !singleFavicon && (
350
+ <meta
351
+ name="msapplication-config"
352
+ content={`${faviconBase}/browserconfig.xml`}
353
+ />
354
+ )
355
+ }