@mannisto/astro-metadata 1.0.0-alpha.3 → 1.0.0-alpha.5

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.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Astro Metadata
2
2
 
3
- ![banner](./assets/default/banner.jpg)
3
+ ![banner](./assets/banner.png)
4
4
 
5
5
  ![npm version](https://img.shields.io/npm/v/@mannisto/astro-metadata)
6
6
  ![license](https://img.shields.io/badge/license-MIT-green)
@@ -8,7 +8,6 @@
8
8
 
9
9
  Astro components for managing your page head — metadata, social sharing, favicons, and SEO.
10
10
 
11
-
12
11
  ## Table of contents
13
12
 
14
13
  - [Installation](#installation)
@@ -30,7 +29,6 @@ Astro components for managing your page head — metadata, social sharing, favic
30
29
  - [Twitter](#twitter)
31
30
  - [License](#license)
32
31
 
33
-
34
32
  ## Installation
35
33
  ```bash
36
34
  # pnpm
@@ -52,7 +50,6 @@ There are three ways to use this package. Pick what suits your project, or combi
52
50
  The simplest approach. Use `Head` in your layout and pass props down from your pages. Charset and viewport are included automatically.
53
51
  ```astro
54
52
  ---
55
-
56
53
  // layouts/Layout.astro
57
54
  import { Head } from "@mannisto/astro-metadata"
58
55
  import type { HeadProps } from "@mannisto/astro-metadata"
@@ -60,7 +57,6 @@ import type { HeadProps } from "@mannisto/astro-metadata"
60
57
  interface Props extends HeadProps {}
61
58
 
62
59
  const { title, description, ...rest } = Astro.props
63
-
64
60
  ---
65
61
 
66
62
  <html>
@@ -75,13 +71,10 @@ const { title, description, ...rest } = Astro.props
75
71
  </body>
76
72
  </html>
77
73
  ```
78
-
79
74
  ```astro
80
75
  ---
81
-
82
76
  // pages/index.astro
83
77
  import Layout from "../layouts/Layout.astro"
84
-
85
78
  ---
86
79
 
87
80
  <Layout title="Home" description="Welcome to my site">
@@ -91,16 +84,12 @@ import Layout from "../layouts/Layout.astro"
91
84
 
92
85
  Best for simple sites where pages pass metadata as props to their layout.
93
86
 
94
-
95
87
  ### 2. Individual components
96
88
 
97
89
  Use components directly inside your own `<head>`. Useful when you only need specific pieces, or want full control over the structure.
98
-
99
90
  ```astro
100
91
  ---
101
-
102
92
  import { Title, Description, OpenGraph, Favicon } from "@mannisto/astro-metadata"
103
-
104
93
  ---
105
94
 
106
95
  <html>
@@ -120,12 +109,10 @@ import { Title, Description, OpenGraph, Favicon } from "@mannisto/astro-metadata
120
109
  }}
121
110
  />
122
111
  <Favicon
123
- icons={{
124
- default: {
125
- ico: { path: "/favicon.ico" },
126
- svg: { path: "/favicon.svg" },
127
- },
128
- }}
112
+ icons={[
113
+ { path: "/favicon.ico" },
114
+ { path: "/favicon.svg" },
115
+ ]}
129
116
  />
130
117
  </head>
131
118
  <body>
@@ -136,14 +123,11 @@ import { Title, Description, OpenGraph, Favicon } from "@mannisto/astro-metadata
136
123
 
137
124
  Best for when you want to compose only what you need, or when `Head` is too opinionated for your setup.
138
125
 
139
-
140
126
  ### 3. Metadata utility
141
127
 
142
128
  Set metadata in your page, resolve it in your layout. Eliminates prop drilling through nested layout layers.
143
-
144
129
  ```astro
145
130
  ---
146
-
147
131
  // pages/about.astro
148
132
  import { Metadata } from "@mannisto/astro-metadata"
149
133
  import Layout from "../layouts/Layout.astro"
@@ -158,17 +142,14 @@ Metadata.set({
158
142
  },
159
143
  },
160
144
  })
161
-
162
145
  ---
163
146
 
164
147
  <Layout>
165
148
  <h1>About</h1>
166
149
  </Layout>
167
150
  ```
168
-
169
151
  ```astro
170
152
  ---
171
-
172
153
  // layouts/Layout.astro
173
154
  import { Head, Metadata } from "@mannisto/astro-metadata"
174
155
 
@@ -177,7 +158,6 @@ const meta = Metadata.resolve({
177
158
  description: "Default description",
178
159
  titleTemplate: "%s | My Site",
179
160
  })
180
-
181
161
  ---
182
162
 
183
163
  <html>
@@ -192,14 +172,12 @@ const meta = Metadata.resolve({
192
172
 
193
173
  Best for sites with deeply nested layouts, or when you want to keep metadata co-located with page content.
194
174
 
195
-
196
175
  ## Components
197
176
 
198
177
  <details>
199
178
  <summary><strong>Canonical</strong></summary>
200
179
 
201
180
  Renders a canonical link tag. Falls back to `Astro.url.href` when no value is provided, so every page gets a canonical tag with zero configuration.
202
-
203
181
  ```astro
204
182
  <Canonical value="https://example.com/page" />
205
183
  ```
@@ -210,10 +188,8 @@ Renders a canonical link tag. Falls back to `Astro.url.href` when no value is pr
210
188
 
211
189
  </details>
212
190
 
213
-
214
191
  <details>
215
192
  <summary><strong>Description</strong></summary>
216
-
217
193
  ```astro
218
194
  <Description value="Welcome to my site" />
219
195
  ```
@@ -224,65 +200,44 @@ Renders a canonical link tag. Falls back to `Astro.url.href` when no value is pr
224
200
 
225
201
  </details>
226
202
 
227
-
228
203
  <details>
229
204
  <summary><strong>Favicon</strong></summary>
230
205
 
231
- Favicon support with dark and light mode variants, multiple formats, and optional cache busting.
232
-
206
+ Favicon support with light and dark mode variants and automatic MIME type detection.
233
207
  ```astro
234
208
  <Favicon
235
- icons={{
236
- default: {
237
- ico: { path: "/favicon.ico" },
238
- svg: { path: "/favicon.svg" },
239
- png: [{ path: "/favicon-96x96.png", size: 96 }],
240
- apple: { path: "/apple-touch-icon.png", size: 180 },
241
- },
242
- lightMode: {
243
- svg: { path: "/favicon-light.svg" },
244
- },
245
- darkMode: {
246
- svg: { path: "/favicon-dark.svg" },
247
- },
248
- }}
209
+ icons={[
210
+ { path: "/favicon.ico" },
211
+ { path: "/favicon.svg" },
212
+ { path: "/favicon-96x96.png", size: 96 },
213
+ { path: "/apple-touch-icon.png", size: 180, apple: true },
214
+ { path: "/favicon-dark.svg", theme: "dark" },
215
+ { path: "/favicon-light.svg", theme: "light" },
216
+ ]}
249
217
  manifest="/site.webmanifest"
250
- cacheBust
251
218
  />
252
219
  ```
253
220
 
254
221
  | Prop | Type | Description |
255
222
  |------|------|-------------|
256
- | `icons.default` | `FaviconIcons` | Default favicon set |
257
- | `icons.lightMode` | `FaviconIcons` | Favicons shown in light color scheme |
258
- | `icons.darkMode` | `FaviconIcons` | Favicons shown in dark color scheme |
223
+ | `icons` | `FaviconFile[]` | List of favicon files |
259
224
  | `manifest` | `string` | Path to web app manifest |
260
- | `cacheBust` | `boolean` | Append `?v={timestamp}` to favicon URLs |
261
-
262
- #### FaviconIcons
263
-
264
- | Prop | Type | Description |
265
- |------|------|-------------|
266
- | `ico` | `FaviconFile` | `.ico` favicon |
267
- | `svg` | `FaviconFile` | `.svg` favicon |
268
- | `png` | `FaviconFile \| FaviconFile[]` | One or more `.png` favicons |
269
- | `apple` | `FaviconFile` | Apple touch icon |
270
225
 
271
226
  #### FaviconFile
272
227
 
273
228
  | Prop | Type | Description |
274
229
  |------|------|-------------|
275
- | `path` | `string` | Path to the file |
230
+ | `path` | `string` | Path to the file. MIME type is detected automatically. |
276
231
  | `size` | `number` | Size in pixels. Rendered as `NxN` in the `sizes` attribute. |
232
+ | `theme` | `"light" \| "dark"` | Adds a `prefers-color-scheme` media query |
233
+ | `apple` | `boolean` | Renders as `<link rel="apple-touch-icon">` |
277
234
 
278
235
  </details>
279
236
 
280
-
281
237
  <details>
282
238
  <summary><strong>Head</strong></summary>
283
239
 
284
240
  Wraps the entire page head and composes all sub-components internally. Charset and viewport are always included and can be overridden if needed.
285
-
286
241
  ```astro
287
242
  <Head
288
243
  title="Home"
@@ -297,11 +252,10 @@ Wraps the entire page head and composes all sub-components internally. Charset a
297
252
  },
298
253
  }}
299
254
  favicon={{
300
- icons: {
301
- default: {
302
- ico: { path: "/favicon.ico" },
303
- },
304
- },
255
+ icons: [
256
+ { path: "/favicon.ico" },
257
+ { path: "/favicon.svg" },
258
+ ],
305
259
  }}
306
260
  />
307
261
  ```
@@ -323,7 +277,6 @@ Wraps the entire page head and composes all sub-components internally. Charset a
323
277
  | `languageAlternates` | `LanguageAlternate[]` | — | Hreflang alternate links |
324
278
 
325
279
  #### Slots
326
-
327
280
  ```astro
328
281
  <Head title="My Site">
329
282
  <!-- Renders before charset and viewport -->
@@ -336,10 +289,8 @@ Wraps the entire page head and composes all sub-components internally. Charset a
336
289
 
337
290
  </details>
338
291
 
339
-
340
292
  <details>
341
293
  <summary><strong>Keywords</strong></summary>
342
-
343
294
  ```astro
344
295
  <Keywords value={["astro", "seo", "metadata"]} />
345
296
  ```
@@ -350,12 +301,10 @@ Wraps the entire page head and composes all sub-components internally. Charset a
350
301
 
351
302
  </details>
352
303
 
353
-
354
304
  <details>
355
305
  <summary><strong>LanguageAlternates</strong></summary>
356
306
 
357
307
  Renders `<link rel="alternate" hreflang>` tags for multilingual sites. Tells search engines which language version to serve for a given region.
358
-
359
308
  ```astro
360
309
  <LanguageAlternates
361
310
  alternates={[
@@ -374,7 +323,6 @@ Renders `<link rel="alternate" hreflang>` tags for multilingual sites. Tells sea
374
323
 
375
324
  </details>
376
325
 
377
-
378
326
  <details>
379
327
  <summary><strong>OpenGraph</strong></summary>
380
328
 
@@ -410,7 +358,6 @@ Renders Open Graph meta tags for rich previews when your pages are shared on soc
410
358
 
411
359
  </details>
412
360
 
413
-
414
361
  <details>
415
362
  <summary><strong>Robots</strong></summary>
416
363
 
@@ -432,7 +379,6 @@ Controls how search engines crawl and index your page. Defaults to `index, follo
432
379
 
433
380
  </details>
434
381
 
435
-
436
382
  <details>
437
383
  <summary><strong>Schema</strong></summary>
438
384
 
@@ -454,12 +400,10 @@ Outputs a `<script type="application/ld+json">` tag for structured data. Use it
454
400
 
455
401
  </details>
456
402
 
457
-
458
403
  <details>
459
404
  <summary><strong>Title</strong></summary>
460
405
 
461
406
  Renders the `<title>` tag. The template must contain `%s`, which is replaced with the page title — TypeScript enforces this at the type level.
462
-
463
407
  ```astro
464
408
  <Title value="My Page" template="%s | My Site" />
465
409
  ```
@@ -471,12 +415,10 @@ Renders the `<title>` tag. The template must contain `%s`, which is replaced wit
471
415
 
472
416
  </details>
473
417
 
474
-
475
418
  <details>
476
419
  <summary><strong>Twitter</strong></summary>
477
420
 
478
421
  Renders Twitter card meta tags for rich previews on X. When used inside `Head`, `title` and `description` fall back to the page values automatically.
479
-
480
422
  ```astro
481
423
  <Twitter
482
424
  card="summary_large_image"
package/index.ts CHANGED
@@ -20,8 +20,7 @@ export type { Props as RobotsProps } from "./src/components/Robots.a
20
20
  export type { Props as OpenGraphProps } from "./src/components/OpenGraph.astro"
21
21
  export type { Props as TwitterProps } from "./src/components/Twitter.astro"
22
22
  export type { Props as FaviconProps,
23
- FaviconFile,
24
- FaviconIcons } from "./src/components/Favicon.astro"
23
+ FaviconFile } from "./src/components/Favicon.astro"
25
24
  export type { Props as SchemaProps } from "./src/components/Schema.astro"
26
25
  export type { Props as LanguageAlternatesProps,
27
26
  LanguageAlternate } from "./src/components/LanguageAlternates.astro"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mannisto/astro-metadata",
3
- "version": "1.0.0-alpha.3",
3
+ "version": "1.0.0-alpha.5",
4
4
  "type": "module",
5
5
  "description": "Astro components for managing your page head — metadata, social sharing, favicons, and SEO.",
6
6
  "license": "MIT",
@@ -1,177 +1,80 @@
1
1
  ---
2
2
 
3
3
  export type FaviconFile = {
4
- path : string
5
- size? : number
6
- }
7
-
8
- export type FaviconIcons = {
9
- ico? : FaviconFile
10
- png? : FaviconFile | FaviconFile[]
11
- svg? : FaviconFile
12
- apple? : FaviconFile
13
- }
14
-
15
- type PreparedFile = {
16
- path : string
17
- size? : string
18
- }
19
-
20
- type PreparedIcons = {
21
- ico? : PreparedFile
22
- png? : PreparedFile[]
23
- svg? : PreparedFile
24
- apple? : PreparedFile
4
+ path : string
5
+ size? : number
6
+ theme? : "light" | "dark"
7
+ apple? : boolean
25
8
  }
26
9
 
27
10
  export type Props = {
28
- icons: {
29
- default? : FaviconIcons
30
- lightMode? : FaviconIcons
31
- darkMode? : FaviconIcons
32
- }
33
- manifest? : string
34
- cacheBust? : boolean
11
+ icons : FaviconFile[]
12
+ manifest? : string
35
13
  }
36
14
 
37
- const {
38
- icons,
39
- manifest,
40
- cacheBust = false
15
+ const {
16
+ icons,
17
+ manifest
41
18
  } = Astro.props
42
19
 
43
- const cacheBustString = cacheBust
44
- ? `?v=${Date.now()}`
45
- : ""
46
-
47
20
  /**
48
- * Prepares a single favicon file by appending the cache bust string
49
- * and converting the numeric size to the "NxN" format browsers expect.
21
+ * Detects the MIME type of a favicon file from its extension.
50
22
  *
51
- * @param file - The favicon file to prepare.
52
- * @returns The prepared favicon file, or undefined if no file was provided.
23
+ * @param path - The path to the favicon file.
24
+ * @returns The MIME type of the file.
53
25
  */
54
- function prepareFile(file?: FaviconFile): PreparedFile | undefined {
55
- if (!file) return undefined
56
- return {
57
- path: `${file.path}${cacheBustString}`,
58
- size: file.size ? `${file.size}x${file.size}` : undefined,
59
- }
26
+ function getMimeType(path: string): string {
27
+ if (path.endsWith(".svg")) return "image/svg+xml"
28
+ if (path.endsWith(".png")) return "image/png"
29
+ if (path.endsWith(".webp")) return "image/webp"
30
+ if (path.endsWith(".jpg") ||
31
+ path.endsWith(".jpeg")) return "image/jpeg"
32
+ if (path.endsWith(".ico")) return "image/x-icon"
33
+ return "image/x-icon"
60
34
  }
61
35
 
62
36
  /**
63
- * Prepares a full set of favicon icons by running each format through prepareFile.
64
- * Normalizes the png field to always be an array for consistent rendering.
37
+ * Builds the media query string for a given theme.
65
38
  *
66
- * @param set - The favicon icon set to prepare.
67
- * @returns The prepared favicon icon set, or undefined if no set was provided.
39
+ * @param theme - The theme to build the media query for.
40
+ * @returns The media query string, or undefined if no theme was provided.
68
41
  */
69
- function prepareIcons(set?: FaviconIcons): PreparedIcons | undefined {
70
- if (!set) return undefined
71
- return {
72
- ico: prepareFile(set.ico),
73
- svg: prepareFile(set.svg),
74
- apple: prepareFile(set.apple),
75
- png: set.png
76
- ? (Array.isArray(set.png) ? set.png : [set.png]).map((file) => prepareFile(file)!)
77
- : undefined,
78
- }
42
+ function getMedia(theme?: "light" | "dark"): string | undefined {
43
+ if (theme === "light") return "(prefers-color-scheme: light)"
44
+ if (theme === "dark") return "(prefers-color-scheme: dark)"
45
+ return undefined
79
46
  }
80
47
 
81
- const prepared = {
82
- default: prepareIcons(icons.default),
83
- lightMode: prepareIcons(icons.lightMode),
84
- darkMode: prepareIcons(icons.darkMode),
85
- }
48
+ const prepared = icons.map((file) => ({
49
+ path : file.path,
50
+ size : file.size ? `${file.size}x${file.size}` : undefined,
51
+ type : getMimeType(file.path),
52
+ media : getMedia(file.theme),
53
+ apple : file.apple ?? false,
54
+ }))
86
55
 
87
56
  ---
88
57
 
89
58
  {manifest && <link rel="manifest" href={manifest} />}
90
59
 
91
- {prepared.default?.ico && (
92
- <link
93
- rel="icon"
94
- type="image/x-icon"
95
- href={prepared.default.ico.path}
96
- sizes={prepared.default.ico.size}
97
- />
98
- )}
99
- {prepared.default?.svg && (
100
- <link
101
- rel="icon"
102
- type="image/svg+xml"
103
- href={prepared.default.svg.path}
104
- sizes={prepared.default.svg.size}
105
- />
106
- )}
107
- {prepared.default?.apple && (
108
- <link
109
- rel="apple-touch-icon"
110
- href={prepared.default.apple.path}
111
- sizes={prepared.default.apple.size}
112
- />
113
- )}
114
- {prepared.default?.png?.map((png) => (
115
- <link
116
- rel="icon"
117
- type="image/png"
118
- href={png.path}
119
- sizes={png.size}
120
- />
121
- ))}
122
-
123
- {prepared.lightMode?.ico && (
124
- <link
125
- rel="icon"
126
- type="image/x-icon"
127
- href={prepared.lightMode.ico.path}
128
- sizes={prepared.lightMode.ico.size}
129
- media="(prefers-color-scheme: light)"
130
- />
131
- )}
132
- {prepared.lightMode?.svg && (
133
- <link
134
- rel="icon"
135
- type="image/svg+xml"
136
- href={prepared.lightMode.svg.path}
137
- sizes={prepared.lightMode.svg.size}
138
- media="(prefers-color-scheme: light)"
139
- />
140
- )}
141
- {prepared.lightMode?.png?.map((png) => (
142
- <link
143
- rel="icon"
144
- type="image/png"
145
- href={png.path}
146
- sizes={png.size}
147
- media="(prefers-color-scheme: light)"
148
- />
149
- ))}
60
+ {prepared.map((icon) => {
61
+ if (icon.apple) {
62
+ return (
63
+ <link
64
+ rel="apple-touch-icon"
65
+ href={icon.path}
66
+ sizes={icon.size}
67
+ />
68
+ )
69
+ }
150
70
 
151
- {prepared.darkMode?.ico && (
152
- <link
153
- rel="icon"
154
- type="image/x-icon"
155
- href={prepared.darkMode.ico.path}
156
- sizes={prepared.darkMode.ico.size}
157
- media="(prefers-color-scheme: dark)"
158
- />
159
- )}
160
- {prepared.darkMode?.svg && (
161
- <link
162
- rel="icon"
163
- type="image/svg+xml"
164
- href={prepared.darkMode.svg.path}
165
- sizes={prepared.darkMode.svg.size}
166
- media="(prefers-color-scheme: dark)"
167
- />
168
- )}
169
- {prepared.darkMode?.png?.map((png) => (
170
- <link
171
- rel="icon"
172
- type="image/png"
173
- href={png.path}
174
- sizes={png.size}
175
- media="(prefers-color-scheme: dark)"
176
- />
177
- ))}
71
+ return (
72
+ <link
73
+ rel="icon"
74
+ type={icon.type}
75
+ href={icon.path}
76
+ sizes={icon.size}
77
+ media={icon.media}
78
+ />
79
+ )
80
+ })}
@@ -93,7 +93,6 @@ const {
93
93
  <Favicon
94
94
  icons={favicon.icons}
95
95
  manifest={favicon.manifest}
96
- cacheBust={favicon.cacheBust}
97
96
  />
98
97
  )}
99
98