@mannisto/astro-metadata 1.0.0-alpha.5 → 1.0.0-beta.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.md CHANGED
@@ -27,9 +27,11 @@ Astro components for managing your page head — metadata, social sharing, favic
27
27
  - [Schema](#schema)
28
28
  - [Title](#title)
29
29
  - [Twitter](#twitter)
30
+ - [Contributing](#contributing)
30
31
  - [License](#license)
31
32
 
32
33
  ## Installation
34
+
33
35
  ```bash
34
36
  # pnpm
35
37
  pnpm add @mannisto/astro-metadata
@@ -48,6 +50,7 @@ There are three ways to use this package. Pick what suits your project, or combi
48
50
  ### 1. Head component
49
51
 
50
52
  The simplest approach. Use `Head` in your layout and pass props down from your pages. Charset and viewport are included automatically.
53
+
51
54
  ```astro
52
55
  ---
53
56
  // layouts/Layout.astro
@@ -60,17 +63,13 @@ const { title, description, ...rest } = Astro.props
60
63
  ---
61
64
 
62
65
  <html>
63
- <Head
64
- title={title}
65
- description={description}
66
- titleTemplate="%s | My Site"
67
- {...rest}
68
- />
66
+ <Head title={title} description={description} titleTemplate="%s | My Site" {...rest} />
69
67
  <body>
70
68
  <slot />
71
69
  </body>
72
70
  </html>
73
71
  ```
72
+
74
73
  ```astro
75
74
  ---
76
75
  // pages/index.astro
@@ -87,6 +86,7 @@ Best for simple sites where pages pass metadata as props to their layout.
87
86
  ### 2. Individual components
88
87
 
89
88
  Use components directly inside your own `<head>`. Useful when you only need specific pieces, or want full control over the structure.
89
+
90
90
  ```astro
91
91
  ---
92
92
  import { Title, Description, OpenGraph, Favicon } from "@mannisto/astro-metadata"
@@ -108,12 +108,7 @@ import { Title, Description, OpenGraph, Favicon } from "@mannisto/astro-metadata
108
108
  height: 630,
109
109
  }}
110
110
  />
111
- <Favicon
112
- icons={[
113
- { path: "/favicon.ico" },
114
- { path: "/favicon.svg" },
115
- ]}
116
- />
111
+ <Favicon icons={[{ path: "/favicon.ico" }, { path: "/favicon.svg" }]} />
117
112
  </head>
118
113
  <body>
119
114
  <slot />
@@ -126,6 +121,7 @@ Best for when you want to compose only what you need, or when `Head` is too opin
126
121
  ### 3. Metadata utility
127
122
 
128
123
  Set metadata in your page, resolve it in your layout. Eliminates prop drilling through nested layout layers.
124
+
129
125
  ```astro
130
126
  ---
131
127
  // pages/about.astro
@@ -148,6 +144,7 @@ Metadata.set({
148
144
  <h1>About</h1>
149
145
  </Layout>
150
146
  ```
147
+
151
148
  ```astro
152
149
  ---
153
150
  // layouts/Layout.astro
@@ -178,12 +175,13 @@ Best for sites with deeply nested layouts, or when you want to keep metadata co-
178
175
  <summary><strong>Canonical</strong></summary>
179
176
 
180
177
  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.
178
+
181
179
  ```astro
182
180
  <Canonical value="https://example.com/page" />
183
181
  ```
184
182
 
185
- | Prop | Type | Description |
186
- |------|------|-------------|
183
+ | Prop | Type | Description |
184
+ | ------- | -------- | -------------------------------------------- |
187
185
  | `value` | `string` | Canonical URL. Defaults to `Astro.url.href`. |
188
186
 
189
187
  </details>
@@ -194,8 +192,8 @@ Renders a canonical link tag. Falls back to `Astro.url.href` when no value is pr
194
192
  <Description value="Welcome to my site" />
195
193
  ```
196
194
 
197
- | Prop | Type | Description |
198
- |------|------|-------------|
195
+ | Prop | Type | Description |
196
+ | ------- | -------- | ---------------- |
199
197
  | `value` | `string` | Page description |
200
198
 
201
199
  </details>
@@ -203,34 +201,38 @@ Renders a canonical link tag. Falls back to `Astro.url.href` when no value is pr
203
201
  <details>
204
202
  <summary><strong>Favicon</strong></summary>
205
203
 
206
- Favicon support with light and dark mode variants and automatic MIME type detection.
204
+ Favicon support with light and dark mode variants, automatic MIME type detection, and automatic sorting.
205
+
207
206
  ```astro
208
207
  <Favicon
209
208
  icons={[
210
209
  { path: "/favicon.ico" },
211
210
  { path: "/favicon.svg" },
212
- { path: "/favicon-96x96.png", size: 96 },
211
+ { path: "/favicon-96x96.png", size: 96 },
213
212
  { path: "/apple-touch-icon.png", size: 180, apple: true },
214
- { path: "/favicon-dark.svg", theme: "dark" },
215
- { path: "/favicon-light.svg", theme: "light" },
213
+ { path: "/favicon-dark.svg", theme: "dark" },
214
+ { path: "/favicon-light.svg", theme: "light" },
216
215
  ]}
217
216
  manifest="/site.webmanifest"
218
217
  />
219
218
  ```
220
219
 
221
- | Prop | Type | Description |
222
- |------|------|-------------|
223
- | `icons` | `FaviconFile[]` | List of favicon files |
224
- | `manifest` | `string` | Path to web app manifest |
220
+ Icons are automatically sorted in the recommended browser order: `ico` → `png` → `svg` → `apple` → themed variants. Pass `sort={false}` to preserve the original order.
221
+
222
+ | Prop | Type | Default | Description |
223
+ | ---------- | --------------- | ------- | --------------------------------------- |
224
+ | `icons` | `FaviconFile[]` | — | List of favicon files |
225
+ | `manifest` | `string` | — | Path to web app manifest |
226
+ | `sort` | `boolean` | `true` | Sort icons in recommended browser order |
225
227
 
226
228
  #### FaviconFile
227
229
 
228
- | Prop | Type | Description |
229
- |------|------|-------------|
230
- | `path` | `string` | Path to the file. MIME type is detected automatically. |
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">` |
230
+ | Prop | Type | Description |
231
+ | ------- | ------------------- | ----------------------------------------------------------- |
232
+ | `path` | `string` | Path to the file. MIME type is detected automatically. |
233
+ | `size` | `number` | Size in pixels. Rendered as `NxN` in the `sizes` attribute. |
234
+ | `theme` | `"light" \| "dark"` | Adds a `prefers-color-scheme` media query |
235
+ | `apple` | `boolean` | Renders as `<link rel="apple-touch-icon">` |
234
236
 
235
237
  </details>
236
238
 
@@ -238,6 +240,7 @@ Favicon support with light and dark mode variants and automatic MIME type detect
238
240
  <summary><strong>Head</strong></summary>
239
241
 
240
242
  Wraps the entire page head and composes all sub-components internally. Charset and viewport are always included and can be overridden if needed.
243
+
241
244
  ```astro
242
245
  <Head
243
246
  title="Home"
@@ -252,38 +255,36 @@ Wraps the entire page head and composes all sub-components internally. Charset a
252
255
  },
253
256
  }}
254
257
  favicon={{
255
- icons: [
256
- { path: "/favicon.ico" },
257
- { path: "/favicon.svg" },
258
- ],
258
+ icons: [{ path: "/favicon.ico" }, { path: "/favicon.svg" }],
259
259
  }}
260
260
  />
261
261
  ```
262
262
 
263
- | Prop | Type | Default | Description |
264
- |------|------|---------|-------------|
265
- | `title` | `string` | — | Page title. Required. |
266
- | `titleTemplate` | `` `${string}%s${string}` `` | — | Title template. Must contain `%s`, e.g. `"%s \| My Site"` |
267
- | `description` | `string` | — | Page description |
268
- | `canonical` | `string` | `Astro.url.href` | Canonical URL |
269
- | `keywords` | `string[]` | — | List of keywords |
270
- | `charset` | `string` | `"UTF-8"` | Document charset |
271
- | `viewport` | `string` | `"width=device-width, initial-scale=1.0"` | Viewport meta content |
272
- | `robots` | `RobotsProps` | — | Robots directives |
273
- | `openGraph` | `OpenGraphProps` | — | Open Graph tags |
274
- | `twitter` | `TwitterProps` | — | Twitter card tags |
275
- | `favicon` | `FaviconProps` | — | Favicon configuration |
276
- | `schema` | `SchemaProps` | — | JSON-LD structured data |
277
- | `languageAlternates` | `LanguageAlternate[]` | — | Hreflang alternate links |
263
+ | Prop | Type | Default | Description |
264
+ | -------------------- | ---------------------------- | ----------------------------------------- | --------------------------------------------------------- |
265
+ | `title` | `string` | — | Page title. Required. |
266
+ | `titleTemplate` | `` `${string}%s${string}` `` | — | Title template. Must contain `%s`, e.g. `"%s \| My Site"` |
267
+ | `description` | `string` | — | Page description |
268
+ | `canonical` | `string` | `Astro.url.href` | Canonical URL |
269
+ | `keywords` | `string[]` | — | List of keywords |
270
+ | `charset` | `string` | `"UTF-8"` | Document charset |
271
+ | `viewport` | `string` | `"width=device-width, initial-scale=1.0"` | Viewport meta content |
272
+ | `robots` | `RobotsProps` | — | Robots directives |
273
+ | `openGraph` | `OpenGraphProps` | — | Open Graph tags |
274
+ | `twitter` | `TwitterProps` | — | Twitter card tags |
275
+ | `favicon` | `FaviconProps` | — | Favicon configuration |
276
+ | `schema` | `SchemaProps` | — | JSON-LD structured data |
277
+ | `languageAlternates` | `LanguageAlternate[]` | — | Hreflang alternate links |
278
278
 
279
279
  #### Slots
280
+
280
281
  ```astro
281
282
  <Head title="My Site">
282
283
  <!-- Renders before charset and viewport -->
283
284
  <meta slot="top" http-equiv="X-UA-Compatible" content="IE=edge" />
284
285
 
285
286
  <!-- Renders at the end of <head> -->
286
- <script src={analyticsUrl} />
287
+ <script src={analyticsUrl}></script>
287
288
  </Head>
288
289
  ```
289
290
 
@@ -295,8 +296,8 @@ Wraps the entire page head and composes all sub-components internally. Charset a
295
296
  <Keywords value={["astro", "seo", "metadata"]} />
296
297
  ```
297
298
 
298
- | Prop | Type | Description |
299
- |------|------|-------------|
299
+ | Prop | Type | Description |
300
+ | ------- | ---------- | ---------------- |
300
301
  | `value` | `string[]` | List of keywords |
301
302
 
302
303
  </details>
@@ -305,21 +306,22 @@ Wraps the entire page head and composes all sub-components internally. Charset a
305
306
  <summary><strong>LanguageAlternates</strong></summary>
306
307
 
307
308
  Renders `<link rel="alternate" hreflang>` tags for multilingual sites. Tells search engines which language version to serve for a given region.
309
+
308
310
  ```astro
309
311
  <LanguageAlternates
310
312
  alternates={[
311
313
  { href: "https://example.com/en", hreflang: "en" },
312
314
  { href: "https://example.com/fi", hreflang: "fi" },
313
- { href: "https://example.com", hreflang: "x-default" },
315
+ { href: "https://example.com", hreflang: "x-default" },
314
316
  ]}
315
317
  />
316
318
  ```
317
319
 
318
- | Prop | Type | Description |
319
- |------|------|-------------|
320
- | `alternates` | `LanguageAlternate[]` | List of alternate language pages |
321
- | `alternates[].href` | `string` | Full URL of the alternate page |
322
- | `alternates[].hreflang` | `string` | Language or region code, e.g. `en`, `fi`, `en-US`, `x-default` |
320
+ | Prop | Type | Description |
321
+ | ----------------------- | --------------------- | -------------------------------------------------------------- |
322
+ | `alternates` | `LanguageAlternate[]` | List of alternate language pages |
323
+ | `alternates[].href` | `string` | Full URL of the alternate page |
324
+ | `alternates[].hreflang` | `string` | Language or region code, e.g. `en`, `fi`, `en-US`, `x-default` |
323
325
 
324
326
  </details>
325
327
 
@@ -327,34 +329,80 @@ Renders `<link rel="alternate" hreflang>` tags for multilingual sites. Tells sea
327
329
  <summary><strong>OpenGraph</strong></summary>
328
330
 
329
331
  Renders Open Graph meta tags for rich previews when your pages are shared on social platforms. When used inside `Head`, `title`, `description` and `url` fall back to the page values automatically.
332
+
330
333
  ```astro
331
334
  <OpenGraph
332
335
  title="My Page"
333
336
  description="Welcome to my site"
337
+ url="https://example.com"
338
+ type="website"
339
+ siteName="My Site"
340
+ locale="en_US"
341
+ localeAlternate={["fi_FI", "fr_FR"]}
334
342
  image={{
335
343
  url: "/og.jpg",
344
+ secureUrl: "https://example.com/og.jpg",
345
+ type: "image/jpeg",
336
346
  alt: "My Site",
337
347
  width: 1200,
338
348
  height: 630,
339
349
  }}
340
- url="https://example.com"
341
- siteName="My Site"
342
- locale="en_US"
350
+ video={{
351
+ url: "https://example.com/video.mp4",
352
+ secureUrl: "https://example.com/video.mp4",
353
+ type: "video/mp4",
354
+ width: 1280,
355
+ height: 720,
356
+ }}
357
+ audio={{
358
+ url: "https://example.com/audio.mp3",
359
+ secureUrl: "https://example.com/audio.mp3",
360
+ type: "audio/mpeg",
361
+ }}
343
362
  />
344
363
  ```
345
364
 
346
- | Prop | Type | Default | Description |
347
- |------|------|---------|-------------|
348
- | `title` | `string` | — | OG title |
349
- | `description` | `string` | — | OG description |
350
- | `image.url` | `string` | — | Image URL. Required if image is set. |
351
- | `image.alt` | `string` | | Image alt text |
352
- | `image.width` | `number` | — | Image width in pixels. Recommended: `1200` |
353
- | `image.height` | `number` | — | Image height in pixels. Recommended: `630` |
354
- | `url` | `string` | — | Canonical URL for the OG object |
355
- | `type` | `string` | `"website"` | OG type |
356
- | `siteName` | `string` | — | Name of the site |
357
- | `locale` | `string` | — | Locale, e.g. `en_US` |
365
+ | Prop | Type | Default | Description |
366
+ | ----------------- | ---------------- | ----------- | -------------------------------------------- |
367
+ | `title` | `string` | — | OG title |
368
+ | `description` | `string` | — | OG description |
369
+ | `url` | `string` | — | Canonical URL for the OG object |
370
+ | `type` | `string` | `"website"` | OG type |
371
+ | `siteName` | `string` | — | Name of the site |
372
+ | `locale` | `string` | — | Locale, e.g. `en_US` |
373
+ | `localeAlternate` | `string[]` | — | Alternate locales, e.g. `["fi_FI", "fr_FR"]` |
374
+ | `image` | `OpenGraphImage` | | Image metadata |
375
+ | `video` | `OpenGraphVideo` | — | Video metadata |
376
+ | `audio` | `OpenGraphAudio` | — | Audio metadata |
377
+
378
+ #### OpenGraphImage
379
+
380
+ | Prop | Type | Description |
381
+ | ----------- | -------- | ------------------------------------------ |
382
+ | `url` | `string` | Image URL. Required if image is set. |
383
+ | `secureUrl` | `string` | HTTPS image URL |
384
+ | `type` | `string` | MIME type, e.g. `"image/jpeg"` |
385
+ | `alt` | `string` | Image alt text |
386
+ | `width` | `number` | Image width in pixels. Recommended: `1200` |
387
+ | `height` | `number` | Image height in pixels. Recommended: `630` |
388
+
389
+ #### OpenGraphVideo
390
+
391
+ | Prop | Type | Description |
392
+ | ----------- | -------- | ------------------------------------ |
393
+ | `url` | `string` | Video URL. Required if video is set. |
394
+ | `secureUrl` | `string` | HTTPS video URL |
395
+ | `type` | `string` | MIME type, e.g. `"video/mp4"` |
396
+ | `width` | `number` | Video width in pixels |
397
+ | `height` | `number` | Video height in pixels |
398
+
399
+ #### OpenGraphAudio
400
+
401
+ | Prop | Type | Description |
402
+ | ----------- | -------- | ------------------------------------ |
403
+ | `url` | `string` | Audio URL. Required if audio is set. |
404
+ | `secureUrl` | `string` | HTTPS audio URL |
405
+ | `type` | `string` | MIME type, e.g. `"audio/mpeg"` |
358
406
 
359
407
  </details>
360
408
 
@@ -362,20 +410,18 @@ Renders Open Graph meta tags for rich previews when your pages are shared on soc
362
410
  <summary><strong>Robots</strong></summary>
363
411
 
364
412
  Controls how search engines crawl and index your page. Defaults to `index, follow`.
413
+
365
414
  ```astro
366
- <Robots
367
- noArchive
368
- extra="max-snippet:-1, max-image-preview:large, max-video-preview:-1"
369
- />
415
+ <Robots archive={false} extra="max-snippet:-1, max-image-preview:large, max-video-preview:-1" />
370
416
  ```
371
417
 
372
- | Prop | Type | Default | Description |
373
- |------|------|---------|-------------|
374
- | `index` | `boolean` | `true` | Allow indexing |
375
- | `follow` | `boolean` | `true` | Allow following links |
376
- | `noArchive` | `boolean` | | Prevent search engines from caching the page |
377
- | `noSnippet` | `boolean` | | Prevent text snippets in search results |
378
- | `extra` | `string` | — | Additional directives, e.g. `"max-snippet:-1, max-image-preview:large"` |
418
+ | Prop | Type | Default | Description |
419
+ | --------- | --------- | ------- | ----------------------------------------------------------------------- |
420
+ | `index` | `boolean` | `true` | Allow indexing |
421
+ | `follow` | `boolean` | `true` | Allow following links |
422
+ | `archive` | `boolean` | `true` | Allow search engines to cache the page |
423
+ | `snippet` | `boolean` | `true` | Allow text snippets in search results |
424
+ | `extra` | `string` | — | Additional directives, e.g. `"max-snippet:-1, max-image-preview:large"` |
379
425
 
380
426
  </details>
381
427
 
@@ -383,19 +429,20 @@ Controls how search engines crawl and index your page. Defaults to `index, follo
383
429
  <summary><strong>Schema</strong></summary>
384
430
 
385
431
  Outputs a `<script type="application/ld+json">` tag for structured data. Use it to help search engines understand your content and qualify for rich results.
432
+
386
433
  ```astro
387
434
  <Schema
388
435
  schema={{
389
436
  "@context": "https://schema.org",
390
437
  "@type": "Person",
391
- "name": "Ere Männistö",
392
- "url": "https://example.com",
438
+ name: "Ere Männistö",
439
+ url: "https://example.com",
393
440
  }}
394
441
  />
395
442
  ```
396
443
 
397
- | Prop | Type | Description |
398
- |------|------|-------------|
444
+ | Prop | Type | Description |
445
+ | -------- | ------------------------- | -------------- |
399
446
  | `schema` | `Record<string, unknown>` | JSON-LD object |
400
447
 
401
448
  </details>
@@ -404,13 +451,14 @@ Outputs a `<script type="application/ld+json">` tag for structured data. Use it
404
451
  <summary><strong>Title</strong></summary>
405
452
 
406
453
  Renders the `<title>` tag. The template must contain `%s`, which is replaced with the page title — TypeScript enforces this at the type level.
454
+
407
455
  ```astro
408
456
  <Title value="My Page" template="%s | My Site" />
409
457
  ```
410
458
 
411
- | Prop | Type | Description |
412
- |------|------|-------------|
413
- | `value` | `string` | Page title. Required. |
459
+ | Prop | Type | Description |
460
+ | ---------- | ---------------------------- | ----------------------------------- |
461
+ | `value` | `string` | Page title. Required. |
414
462
  | `template` | `` `${string}%s${string}` `` | Template string. Must contain `%s`. |
415
463
 
416
464
  </details>
@@ -419,11 +467,13 @@ Renders the `<title>` tag. The template must contain `%s`, which is replaced wit
419
467
  <summary><strong>Twitter</strong></summary>
420
468
 
421
469
  Renders Twitter card meta tags for rich previews on X. When used inside `Head`, `title` and `description` fall back to the page values automatically.
470
+
422
471
  ```astro
423
472
  <Twitter
424
473
  card="summary_large_image"
425
474
  site="@mysite"
426
475
  creator="@myhandle"
476
+ url="https://example.com"
427
477
  image={{
428
478
  url: "/og.jpg",
429
479
  alt: "My Site",
@@ -431,18 +481,93 @@ Renders Twitter card meta tags for rich previews on X. When used inside `Head`,
431
481
  />
432
482
  ```
433
483
 
434
- | Prop | Type | Default | Description |
435
- |------|------|---------|-------------|
436
- | `title` | `string` | — | Card title |
437
- | `description` | `string` | — | Card description |
438
- | `image.url` | `string` | — | Image URL. Required if image is set. |
439
- | `image.alt` | `string` | | Image alt text |
440
- | `card` | `"summary" \| "summary_large_image"` | `"summary_large_image"` | Card type |
441
- | `site` | `string` | — | Twitter handle of the site, e.g. `@mysite` |
442
- | `creator` | `string` | — | Twitter handle of the content author |
484
+ | Prop | Type | Default | Description |
485
+ | ------------- | --------------------------------------------------------- | ----------------------- | ------------------------------------------ |
486
+ | `title` | `string` | — | Card title |
487
+ | `description` | `string` | — | Card description |
488
+ | `url` | `string` | — | Canonical URL for the card |
489
+ | `card` | `"summary" \| "summary_large_image" \| "player" \| "app"` | `"summary_large_image"` | Card type |
490
+ | `site` | `string` | | Twitter handle of the site, e.g. `@mysite` |
491
+ | `creator` | `string` | — | Twitter handle of the content author |
492
+ | `image.url` | `string` | — | Image URL. Required if image is set. |
493
+ | `image.alt` | `string` | — | Image alt text |
443
494
 
444
495
  </details>
445
496
 
497
+ ## Contributing
498
+
499
+ ### Setup
500
+
501
+ Clone the repository and run the init script:
502
+
503
+ ```bash
504
+ git clone https://github.com/eremannisto/astro-metadata
505
+ cd astro-metadata
506
+ pnpm run init
507
+ ```
508
+
509
+ This installs all dependencies, links the local package to the fixture project, and installs Playwright browsers.
510
+
511
+ ### Running tests
512
+
513
+ Run unit tests only:
514
+
515
+ ```bash
516
+ pnpm test:unit
517
+ ```
518
+
519
+ Run end-to-end component tests:
520
+
521
+ ```bash
522
+ pnpm test:e2e
523
+ ```
524
+
525
+ Run all tests:
526
+
527
+ ```bash
528
+ pnpm test:all
529
+ ```
530
+
531
+ ### Linting and formatting
532
+
533
+ This project uses [Biome](https://biomejs.dev) for linting and [Prettier](https://prettier.io) for formatting.
534
+
535
+ Check for issues:
536
+
537
+ ```bash
538
+ pnpm check
539
+ ```
540
+
541
+ Format files:
542
+
543
+ ```bash
544
+ pnpm format
545
+ ```
546
+
547
+ All pull requests must pass `pnpm check` before merging. This is enforced automatically via GitHub Actions.
548
+
549
+ ### Project structure
550
+
551
+ ```
552
+ astro-metadata/
553
+ src/
554
+ components/ # Astro components
555
+ lib/ # Metadata utility
556
+ tests/
557
+ e2e/
558
+ components/ # Playwright component tests
559
+ fixtures/ # Astro test project
560
+ unit/
561
+ metadata.test.ts
562
+ scripts/
563
+ init.sh
564
+ index.ts
565
+ playwright.config.ts
566
+ vitest.config.ts
567
+ biome.json
568
+ prettier.config.ts
569
+ ```
570
+
446
571
  ## License
447
572
 
448
- MIT © [Ere Männistö](https://github.com/eremannisto)
573
+ MIT © [Ere Männistö](https://github.com/eremannisto)
package/index.ts CHANGED
@@ -1,26 +1,31 @@
1
- export { default as Head } from "./src/components/Head.astro"
2
- export { default as Title } from "./src/components/Title.astro"
3
- export { default as Description } from "./src/components/Description.astro"
4
- export { default as Canonical } from "./src/components/Canonical.astro"
5
- export { default as Keywords } from "./src/components/Keywords.astro"
6
- export { default as Robots } from "./src/components/Robots.astro"
7
- export { default as OpenGraph } from "./src/components/OpenGraph.astro"
8
- export { default as Twitter } from "./src/components/Twitter.astro"
9
- export { default as Favicon } from "./src/components/Favicon.astro"
10
- export { default as Schema } from "./src/components/Schema.astro"
1
+ export type { Props as CanonicalProps } from "./src/components/Canonical.astro"
2
+ export { default as Canonical } from "./src/components/Canonical.astro"
3
+ export type { Props as DescriptionProps } from "./src/components/Description.astro"
4
+ export { default as Description } from "./src/components/Description.astro"
5
+ export type { FaviconFile, Props as FaviconProps } from "./src/components/Favicon.astro"
6
+ export { default as Favicon } from "./src/components/Favicon.astro"
7
+ export type { Props as HeadProps } from "./src/components/Head.astro"
8
+ export { default as Head } from "./src/components/Head.astro"
9
+ export type { Props as KeywordsProps } from "./src/components/Keywords.astro"
10
+ export { default as Keywords } from "./src/components/Keywords.astro"
11
+ export type {
12
+ LanguageAlternate,
13
+ Props as LanguageAlternatesProps,
14
+ } from "./src/components/LanguageAlternates.astro"
11
15
  export { default as LanguageAlternates } from "./src/components/LanguageAlternates.astro"
12
- export { Metadata } from "./src/lib/metadata.ts"
13
-
14
- export type { Props as HeadProps } from "./src/components/Head.astro"
15
- export type { Props as TitleProps } from "./src/components/Title.astro"
16
- export type { Props as DescriptionProps } from "./src/components/Description.astro"
17
- export type { Props as CanonicalProps } from "./src/components/Canonical.astro"
18
- export type { Props as KeywordsProps } from "./src/components/Keywords.astro"
19
- export type { Props as RobotsProps } from "./src/components/Robots.astro"
20
- export type { Props as OpenGraphProps } from "./src/components/OpenGraph.astro"
21
- export type { Props as TwitterProps } from "./src/components/Twitter.astro"
22
- export type { Props as FaviconProps,
23
- FaviconFile } from "./src/components/Favicon.astro"
24
- export type { Props as SchemaProps } from "./src/components/Schema.astro"
25
- export type { Props as LanguageAlternatesProps,
26
- LanguageAlternate } from "./src/components/LanguageAlternates.astro"
16
+ export type {
17
+ OpenGraphAudio,
18
+ OpenGraphImage,
19
+ OpenGraphVideo,
20
+ Props as OpenGraphProps,
21
+ } from "./src/components/OpenGraph.astro"
22
+ export { default as OpenGraph } from "./src/components/OpenGraph.astro"
23
+ export type { Props as RobotsProps } from "./src/components/Robots.astro"
24
+ export { default as Robots } from "./src/components/Robots.astro"
25
+ export type { Props as SchemaProps } from "./src/components/Schema.astro"
26
+ export { default as Schema } from "./src/components/Schema.astro"
27
+ export type { Props as TitleProps } from "./src/components/Title.astro"
28
+ export { default as Title } from "./src/components/Title.astro"
29
+ export type { Props as TwitterProps } from "./src/components/Twitter.astro"
30
+ export { default as Twitter } from "./src/components/Twitter.astro"
31
+ export { Metadata } from "./src/lib/metadata.ts"
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@mannisto/astro-metadata",
3
- "version": "1.0.0-alpha.5",
4
- "type": "module",
3
+ "version": "1.0.0-beta.2",
5
4
  "description": "Astro components for managing your page head — metadata, social sharing, favicons, and SEO.",
6
5
  "license": "MIT",
6
+ "type": "module",
7
7
  "homepage": "https://github.com/eremannisto/astro-metadata#readme",
8
8
  "repository": {
9
9
  "type": "git",
@@ -12,14 +12,37 @@
12
12
  "bugs": {
13
13
  "url": "https://github.com/eremannisto/astro-metadata/issues"
14
14
  },
15
- "files": ["src", "index.ts"],
15
+ "keywords": [
16
+ "astro-component",
17
+ "astro",
18
+ "seo",
19
+ "metadata"
20
+ ],
16
21
  "exports": "./index.ts",
22
+ "files": [
23
+ "src",
24
+ "index.ts"
25
+ ],
26
+ "scripts": {
27
+ "init": "bash scripts/init.sh",
28
+ "lint": "biome lint ./src ./index.ts",
29
+ "format": "prettier --write .",
30
+ "check": "biome lint ./src ./index.ts && prettier --check .",
31
+ "test:unit": "vitest run",
32
+ "test:e2e": "playwright test",
33
+ "test:all": "pnpm run test:unit && pnpm run test:e2e"
34
+ },
17
35
  "peerDependencies": {
18
36
  "astro": "^4.3.0 || ^5.0.0"
19
37
  },
20
38
  "devDependencies": {
39
+ "@biomejs/biome": "^2.4.3",
40
+ "@playwright/test": "^1.58.2",
41
+ "@types/node": "^25.3.0",
21
42
  "astro": "^5.17.2",
22
- "typescript": "^5.9.3"
23
- },
24
- "keywords": ["astro-component", "astro", "head", "seo", "meta"]
25
- }
43
+ "prettier": "^3.8.1",
44
+ "prettier-plugin-astro": "^0.14.1",
45
+ "typescript": "^5.9.3",
46
+ "vitest": "^4.0.18"
47
+ }
48
+ }
@@ -1,15 +1,11 @@
1
1
  ---
2
-
3
2
  export type Props = {
4
3
  value?: string
5
4
  }
6
5
 
7
- const {
8
- value
9
- } = Astro.props
6
+ const { value } = Astro.props
10
7
 
11
8
  const canonical = value ?? Astro.url.href
12
-
13
9
  ---
14
10
 
15
- <link rel="canonical" href={canonical} />
11
+ <link rel="canonical" href={canonical} />
@@ -1,13 +1,9 @@
1
1
  ---
2
-
3
2
  export type Props = {
4
3
  value?: string
5
4
  }
6
5
 
7
- const {
8
- value
9
- } = Astro.props
10
-
6
+ const { value } = Astro.props
11
7
  ---
12
8
 
13
- {value && <meta name="description" content={value} />}
9
+ {value && <meta name="description" content={value} />}
@@ -1,21 +1,18 @@
1
1
  ---
2
-
3
2
  export type FaviconFile = {
4
- path : string
5
- size? : number
6
- theme? : "light" | "dark"
7
- apple? : boolean
3
+ path: string
4
+ size?: number
5
+ theme?: "light" | "dark"
6
+ apple?: boolean
8
7
  }
9
8
 
10
9
  export type Props = {
11
- icons : FaviconFile[]
12
- manifest? : string
10
+ icons: FaviconFile[]
11
+ manifest?: string
12
+ sort?: boolean
13
13
  }
14
14
 
15
- const {
16
- icons,
17
- manifest
18
- } = Astro.props
15
+ const { icons, manifest, sort = true } = Astro.props
19
16
 
20
17
  /**
21
18
  * Detects the MIME type of a favicon file from its extension.
@@ -24,12 +21,11 @@ const {
24
21
  * @returns The MIME type of the file.
25
22
  */
26
23
  function getMimeType(path: string): string {
27
- if (path.endsWith(".svg")) return "image/svg+xml"
28
- if (path.endsWith(".png")) return "image/png"
24
+ if (path.endsWith(".svg")) return "image/svg+xml"
25
+ if (path.endsWith(".png")) return "image/png"
29
26
  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"
27
+ if (path.endsWith(".jpg") || path.endsWith(".jpeg")) return "image/jpeg"
28
+ if (path.endsWith(".ico")) return "image/x-icon"
33
29
  return "image/x-icon"
34
30
  }
35
31
 
@@ -41,40 +37,52 @@ function getMimeType(path: string): string {
41
37
  */
42
38
  function getMedia(theme?: "light" | "dark"): string | undefined {
43
39
  if (theme === "light") return "(prefers-color-scheme: light)"
44
- if (theme === "dark") return "(prefers-color-scheme: dark)"
40
+ if (theme === "dark") return "(prefers-color-scheme: dark)"
45
41
  return undefined
46
42
  }
47
43
 
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
- }))
44
+ /**
45
+ * Returns a sort priority for a given favicon file.
46
+ * Lower number = rendered first.
47
+ *
48
+ * Order: ico → png → svg → apple → themed variants
49
+ */
50
+ function getSortOrder(file: FaviconFile): number {
51
+ if (file.apple) return 4
52
+ if (file.theme) return 5
53
+ if (file.path.endsWith(".ico")) return 0
54
+ if (
55
+ file.path.endsWith(".png") ||
56
+ file.path.endsWith(".webp") ||
57
+ file.path.endsWith(".jpg") ||
58
+ file.path.endsWith(".jpeg")
59
+ )
60
+ return 1
61
+ if (file.path.endsWith(".svg")) return 2
62
+ return 3
63
+ }
55
64
 
65
+ const sorted = sort ? [...icons].sort((a, b) => getSortOrder(a) - getSortOrder(b)) : icons
66
+
67
+ const prepared = sorted.map((file) => ({
68
+ path: file.path,
69
+ size: file.size ? `${file.size}x${file.size}` : undefined,
70
+ type: getMimeType(file.path),
71
+ media: getMedia(file.theme),
72
+ apple: file.apple ?? false,
73
+ }))
56
74
  ---
57
75
 
58
76
  {manifest && <link rel="manifest" href={manifest} />}
59
77
 
60
- {prepared.map((icon) => {
61
- if (icon.apple) {
78
+ {
79
+ prepared.map((icon) => {
80
+ if (icon.apple) {
81
+ return <link rel="apple-touch-icon" href={icon.path} sizes={icon.size} />
82
+ }
83
+
62
84
  return (
63
- <link
64
- rel="apple-touch-icon"
65
- href={icon.path}
66
- sizes={icon.size}
67
- />
85
+ <link rel="icon" type={icon.type} href={icon.path} sizes={icon.size} media={icon.media} />
68
86
  )
69
- }
70
-
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
- })}
87
+ })
88
+ }
@@ -1,30 +1,30 @@
1
1
  ---
2
2
  import type { ComponentProps } from "astro/types"
3
- import Title from "./Title.astro"
4
- import Description from "./Description.astro"
5
- import Canonical from "./Canonical.astro"
6
- import Keywords from "./Keywords.astro"
7
- import Robots from "./Robots.astro"
8
- import OpenGraph from "./OpenGraph.astro"
9
- import Twitter from "./Twitter.astro"
10
- import Favicon from "./Favicon.astro"
11
- import Schema from "./Schema.astro"
3
+ import Title from "./Title.astro"
4
+ import Description from "./Description.astro"
5
+ import Canonical from "./Canonical.astro"
6
+ import Keywords from "./Keywords.astro"
7
+ import Robots from "./Robots.astro"
8
+ import OpenGraph from "./OpenGraph.astro"
9
+ import Twitter from "./Twitter.astro"
10
+ import Favicon from "./Favicon.astro"
11
+ import Schema from "./Schema.astro"
12
12
  import LanguageAlternates from "./LanguageAlternates.astro"
13
13
 
14
14
  export type Props = {
15
- title : string
16
- titleTemplate? : `${string}%s${string}`
17
- description? : string
18
- canonical? : string
19
- keywords? : string[]
20
- charset? : string
21
- viewport? : string
22
- robots? : ComponentProps<typeof Robots>
23
- openGraph? : ComponentProps<typeof OpenGraph>
24
- twitter? : ComponentProps<typeof Twitter>
25
- favicon? : ComponentProps<typeof Favicon>
26
- schema? : ComponentProps<typeof Schema>
27
- languageAlternates? : ComponentProps<typeof LanguageAlternates>["alternates"]
15
+ title: string
16
+ titleTemplate?: `${string}%s${string}`
17
+ description?: string
18
+ canonical?: string
19
+ keywords?: string[]
20
+ charset?: string
21
+ viewport?: string
22
+ robots?: ComponentProps<typeof Robots>
23
+ openGraph?: ComponentProps<typeof OpenGraph>
24
+ twitter?: ComponentProps<typeof Twitter>
25
+ favicon?: ComponentProps<typeof Favicon>
26
+ schema?: ComponentProps<typeof Schema>
27
+ languageAlternates?: ComponentProps<typeof LanguageAlternates>["alternates"]
28
28
  }
29
29
 
30
30
  const {
@@ -33,7 +33,7 @@ const {
33
33
  description,
34
34
  canonical,
35
35
  keywords,
36
- charset = "UTF-8",
36
+ charset = "UTF-8",
37
37
  viewport = "width=device-width, initial-scale=1.0",
38
38
  robots,
39
39
  openGraph,
@@ -57,46 +57,47 @@ const {
57
57
  <Robots
58
58
  index={robots?.index}
59
59
  follow={robots?.follow}
60
- noArchive={robots?.noArchive}
61
- noSnippet={robots?.noSnippet}
60
+ archive={robots?.archive}
61
+ snippet={robots?.snippet}
62
62
  extra={robots?.extra}
63
63
  />
64
64
 
65
- {languageAlternates && (
66
- <LanguageAlternates alternates={languageAlternates} />
67
- )}
65
+ {languageAlternates && <LanguageAlternates alternates={languageAlternates} />}
68
66
 
69
- {openGraph && (
70
- <OpenGraph
71
- title={openGraph.title ?? title}
72
- description={openGraph.description ?? description}
73
- image={openGraph.image}
74
- url={openGraph.url ?? canonical}
75
- type={openGraph.type}
76
- siteName={openGraph.siteName}
77
- locale={openGraph.locale}
78
- />
79
- )}
67
+ {
68
+ openGraph && (
69
+ <OpenGraph
70
+ title={openGraph.title ?? title}
71
+ description={openGraph.description ?? description}
72
+ url={openGraph.url ?? canonical}
73
+ type={openGraph.type}
74
+ siteName={openGraph.siteName}
75
+ locale={openGraph.locale}
76
+ localeAlternate={openGraph.localeAlternate}
77
+ image={openGraph.image}
78
+ video={openGraph.video}
79
+ audio={openGraph.audio}
80
+ />
81
+ )
82
+ }
80
83
 
81
- {twitter && (
82
- <Twitter
83
- title={twitter.title ?? title}
84
- description={twitter.description ?? description}
85
- image={twitter.image}
86
- card={twitter.card}
87
- site={twitter.site}
88
- creator={twitter.creator}
89
- />
90
- )}
84
+ {
85
+ twitter && (
86
+ <Twitter
87
+ title={twitter.title ?? title}
88
+ description={twitter.description ?? description}
89
+ image={twitter.image}
90
+ card={twitter.card}
91
+ site={twitter.site}
92
+ creator={twitter.creator}
93
+ url={twitter.url ?? canonical}
94
+ />
95
+ )
96
+ }
91
97
 
92
- {favicon && (
93
- <Favicon
94
- icons={favicon.icons}
95
- manifest={favicon.manifest}
96
- />
97
- )}
98
+ {favicon && <Favicon icons={favicon.icons} manifest={favicon.manifest} sort={favicon.sort} />}
98
99
 
99
100
  {schema && <Schema schema={schema.schema} />}
100
101
 
101
102
  <slot />
102
- </head>
103
+ </head>
@@ -1,13 +1,9 @@
1
1
  ---
2
-
3
2
  export type Props = {
4
3
  value?: string[]
5
4
  }
6
5
 
7
- const {
8
- value = []
9
- } = Astro.props
10
-
6
+ const { value = [] } = Astro.props
11
7
  ---
12
8
 
13
- {value.length > 0 && <meta name="keywords" content={value.join(", ")} />}
9
+ {value.length > 0 && <meta name="keywords" content={value.join(", ")} />}
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  export type LanguageAlternate = {
3
- href : string
4
- hreflang : string
3
+ href: string
4
+ hreflang: string
5
5
  }
6
6
 
7
7
  export type Props = {
@@ -11,10 +11,8 @@ export type Props = {
11
11
  const { alternates } = Astro.props
12
12
  ---
13
13
 
14
- {alternates.map((alternate) => (
15
- <link
16
- rel="alternate"
17
- href={alternate.href}
18
- hreflang={alternate.hreflang}
19
- />
20
- ))}
14
+ {
15
+ alternates.map((alternate) => (
16
+ <link rel="alternate" href={alternate.href} hreflang={alternate.hreflang} />
17
+ ))
18
+ }
@@ -1,39 +1,75 @@
1
1
  ---
2
+ export type OpenGraphImage = {
3
+ url: string
4
+ secureUrl?: string
5
+ type?: string
6
+ alt?: string
7
+ width?: number
8
+ height?: number
9
+ }
10
+
11
+ export type OpenGraphVideo = {
12
+ url: string
13
+ secureUrl?: string
14
+ type?: string
15
+ width?: number
16
+ height?: number
17
+ }
18
+
19
+ export type OpenGraphAudio = {
20
+ url: string
21
+ secureUrl?: string
22
+ type?: string
23
+ }
2
24
 
3
25
  export type Props = {
4
- title? : string
5
- description? : string
6
- url? : string
7
- type? : string
8
- siteName? : string
9
- locale? : string
10
- image? : {
11
- url : string
12
- alt? : string
13
- width? : number
14
- height? : number
15
- }
26
+ title?: string
27
+ description?: string
28
+ url?: string
29
+ type?: string
30
+ siteName?: string
31
+ locale?: string
32
+ localeAlternate?: string[]
33
+ image?: OpenGraphImage
34
+ video?: OpenGraphVideo
35
+ audio?: OpenGraphAudio
16
36
  }
17
37
 
18
38
  const {
19
39
  title,
20
40
  description,
21
- image,
22
41
  url,
23
42
  type = "website",
24
43
  siteName,
25
44
  locale,
45
+ localeAlternate,
46
+ image,
47
+ video,
48
+ audio,
26
49
  } = Astro.props
27
-
28
50
  ---
29
51
 
30
- {title && <meta property="og:title" content={title} />}
31
- {description && <meta property="og:description" content={description} />}
32
- {image?.url && <meta property="og:image" content={image.url} />}
33
- {image?.alt && <meta property="og:image:alt" content={image.alt} />}
34
- {image?.width && <meta property="og:image:width" content={String(image.width)} />}
52
+ {title && <meta property="og:title" content={title} />}
53
+ {description && <meta property="og:description" content={description} />}
54
+ {url && <meta property="og:url" content={url} />}
55
+ {type && <meta property="og:type" content={type} />}
56
+ {siteName && <meta property="og:site_name" content={siteName} />}
57
+ {locale && <meta property="og:locale" content={locale} />}
58
+ {localeAlternate?.map((loc: string) => <meta property="og:locale:alternate" content={loc} />)}
59
+
60
+ {image?.url && <meta property="og:image" content={image.url} />}
61
+ {image?.secureUrl && <meta property="og:image:secure_url" content={image.secureUrl} />}
62
+ {image?.type && <meta property="og:image:type" content={image.type} />}
63
+ {image?.alt && <meta property="og:image:alt" content={image.alt} />}
64
+ {image?.width && <meta property="og:image:width" content={String(image.width)} />}
35
65
  {image?.height && <meta property="og:image:height" content={String(image.height)} />}
36
- {url && <meta property="og:url" content={url} />}
37
- {type && <meta property="og:type" content={type} />}
38
- {siteName && <meta property="og:site_name" content={siteName} />}
39
- {locale && <meta property="og:locale" content={locale} />}
66
+
67
+ {video?.url && <meta property="og:video" content={video.url} />}
68
+ {video?.secureUrl && <meta property="og:video:secure_url" content={video.secureUrl} />}
69
+ {video?.type && <meta property="og:video:type" content={video.type} />}
70
+ {video?.width && <meta property="og:video:width" content={String(video.width)} />}
71
+ {video?.height && <meta property="og:video:height" content={String(video.height)} />}
72
+
73
+ {audio?.url && <meta property="og:audio" content={audio.url} />}
74
+ {audio?.secureUrl && <meta property="og:audio:secure_url" content={audio.secureUrl} />}
75
+ {audio?.type && <meta property="og:audio:type" content={audio.type} />}
@@ -1,29 +1,25 @@
1
1
  ---
2
2
  export type Props = {
3
- index? : boolean
4
- follow? : boolean
5
- noArchive? : boolean
6
- noSnippet? : boolean
7
- extra? : string
3
+ index?: boolean
4
+ follow?: boolean
5
+ archive?: boolean
6
+ snippet?: boolean
7
+ extra?: string
8
8
  }
9
9
 
10
- const {
11
- index = true,
12
- follow = true,
13
- noArchive,
14
- noSnippet,
15
- extra,
16
- } = Astro.props
10
+ const { index = true, follow = true, archive = true, snippet = true, extra } = Astro.props
17
11
 
18
12
  // Combine the directives into a comma separated string,
19
13
  // for example: "index, follow, noarchive, nosnippet"
20
14
  const directives = [
21
15
  index ? "index" : "noindex",
22
16
  follow ? "follow" : "nofollow",
23
- noArchive && "noarchive",
24
- noSnippet && "nosnippet",
17
+ !archive ? "noarchive" : null,
18
+ !snippet ? "nosnippet" : null,
25
19
  extra,
26
- ].filter(Boolean).join(", ")
20
+ ]
21
+ .filter(Boolean)
22
+ .join(", ")
27
23
  ---
28
24
 
29
- <meta name="robots" content={directives} />
25
+ <meta name="robots" content={directives} />
@@ -1,13 +1,9 @@
1
1
  ---
2
-
3
2
  export type Props = {
4
3
  schema: Record<string, unknown>
5
4
  }
6
5
 
7
- const {
8
- schema
9
- } = Astro.props
10
-
6
+ const { schema } = Astro.props
11
7
  ---
12
8
 
13
- <script is:inline type="application/ld+json" set:html={JSON.stringify(schema)} />
9
+ <script is:inline type="application/ld+json" set:html={JSON.stringify(schema)} />
@@ -1,15 +1,10 @@
1
1
  ---
2
-
3
2
  export type Props = {
4
- value : string
5
- template? : `${string}%s${string}`
3
+ value: string
4
+ template?: `${string}%s${string}`
6
5
  }
7
6
 
8
- const {
9
- value,
10
- template = "%s"
11
- } = Astro.props
12
-
7
+ const { value, template = "%s" } = Astro.props
13
8
  ---
14
9
 
15
- <title>{template.replace("%s", value)}</title>
10
+ <title>{template.replace("%s", value)}</title>
@@ -1,32 +1,25 @@
1
1
  ---
2
-
3
2
  export type Props = {
4
- title? : string
5
- description? : string
6
- card? : "summary" | "summary_large_image"
7
- site? : string
8
- creator? : string
9
- image? : {
10
- url : string
11
- alt? : string
3
+ title?: string
4
+ description?: string
5
+ url?: string
6
+ card?: "summary" | "summary_large_image" | "player" | "app"
7
+ site?: string
8
+ creator?: string
9
+ image?: {
10
+ url: string
11
+ alt?: string
12
12
  }
13
13
  }
14
14
 
15
- const {
16
- title,
17
- description,
18
- image,
19
- card = "summary_large_image",
20
- site,
21
- creator,
22
- } = Astro.props
23
-
15
+ const { title, description, url, card = "summary_large_image", site, creator, image } = Astro.props
24
16
  ---
25
17
 
26
- {card && <meta name="twitter:card" content={card} />}
27
- {title && <meta name="twitter:title" content={title} />}
18
+ {card && <meta name="twitter:card" content={card} />}
19
+ {title && <meta name="twitter:title" content={title} />}
28
20
  {description && <meta name="twitter:description" content={description} />}
29
- {image?.url && <meta name="twitter:image" content={image.url} />}
30
- {image?.alt && <meta name="twitter:image:alt" content={image.alt} />}
31
- {site && <meta name="twitter:site" content={site} />}
32
- {creator && <meta name="twitter:creator" content={creator} />}
21
+ {url && <meta name="twitter:url" content={url} />}
22
+ {image?.url && <meta name="twitter:image" content={image.url} />}
23
+ {image?.alt && <meta name="twitter:image:alt" content={image.alt} />}
24
+ {site && <meta name="twitter:site" content={site} />}
25
+ {creator && <meta name="twitter:creator" content={creator} />}
@@ -1,5 +1,5 @@
1
1
  import type { ComponentProps } from "astro/types"
2
- import type Head from "../components/Head.astro"
2
+ import type Head from "../components/Head.astro"
3
3
 
4
4
  let store: Partial<ComponentProps<typeof Head>> = {}
5
5
 
@@ -8,7 +8,6 @@ let store: Partial<ComponentProps<typeof Head>> = {}
8
8
  * Call set() in your page, then resolve() in your layout.
9
9
  */
10
10
  export const Metadata = {
11
-
12
11
  /**
13
12
  * Replace the current metadata store with new values.
14
13
  *
@@ -38,14 +37,37 @@ export const Metadata = {
38
37
  return { ...defaults, ...store }
39
38
  },
40
39
 
41
- get title() { return store.title },
42
- get titleTemplate() { return store.titleTemplate },
43
- get description() { return store.description },
44
- get canonical() { return store.canonical },
45
- get keywords() { return store.keywords },
46
- get robots() { return store.robots },
47
- get openGraph() { return store.openGraph },
48
- get twitter() { return store.twitter },
49
- get favicon() { return store.favicon },
50
- get schema() { return store.schema },
51
- }
40
+ get title() {
41
+ return store.title
42
+ },
43
+ get titleTemplate() {
44
+ return store.titleTemplate
45
+ },
46
+ get description() {
47
+ return store.description
48
+ },
49
+ get canonical() {
50
+ return store.canonical
51
+ },
52
+ get keywords() {
53
+ return store.keywords
54
+ },
55
+ get robots() {
56
+ return store.robots
57
+ },
58
+ get openGraph() {
59
+ return store.openGraph
60
+ },
61
+ get twitter() {
62
+ return store.twitter
63
+ },
64
+ get favicon() {
65
+ return store.favicon
66
+ },
67
+ get schema() {
68
+ return store.schema
69
+ },
70
+ get languageAlternates() {
71
+ return store.languageAlternates
72
+ },
73
+ }