@mannisto/astro-metadata 1.0.0-alpha.5 → 1.0.0-beta.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/README.md +225 -101
- package/index.ts +30 -25
- package/package.json +30 -7
- package/src/components/Canonical.astro +2 -6
- package/src/components/Description.astro +2 -6
- package/src/components/Favicon.astro +51 -43
- package/src/components/Head.astro +57 -56
- package/src/components/Keywords.astro +2 -6
- package/src/components/LanguageAlternates.astro +7 -9
- package/src/components/OpenGraph.astro +59 -23
- package/src/components/Robots.astro +12 -16
- package/src/components/Schema.astro +2 -6
- package/src/components/Title.astro +4 -9
- package/src/components/Twitter.astro +17 -24
- package/src/lib/metadata.ts +35 -13
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
|
|
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
|
|
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
|
|
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",
|
|
211
|
+
{ path: "/favicon-96x96.png", size: 96 },
|
|
213
212
|
{ path: "/apple-touch-icon.png", size: 180, apple: true },
|
|
214
|
-
{ path: "/favicon-dark.svg",
|
|
215
|
-
{ path: "/favicon-light.svg",
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
|
224
|
-
|
|
|
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
|
|
229
|
-
|
|
230
|
-
| `path`
|
|
231
|
-
| `size`
|
|
232
|
-
| `theme` | `"light" \| "dark"` | Adds a `prefers-color-scheme` media query
|
|
233
|
-
| `apple` | `boolean`
|
|
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
|
|
264
|
-
|
|
265
|
-
| `title`
|
|
266
|
-
| `titleTemplate`
|
|
267
|
-
| `description`
|
|
268
|
-
| `canonical`
|
|
269
|
-
| `keywords`
|
|
270
|
-
| `charset`
|
|
271
|
-
| `viewport`
|
|
272
|
-
| `robots`
|
|
273
|
-
| `openGraph`
|
|
274
|
-
| `twitter`
|
|
275
|
-
| `favicon`
|
|
276
|
-
| `schema`
|
|
277
|
-
| `languageAlternates` | `LanguageAlternate[]`
|
|
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
|
|
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",
|
|
315
|
+
{ href: "https://example.com", hreflang: "x-default" },
|
|
314
316
|
]}
|
|
315
317
|
/>
|
|
316
318
|
```
|
|
317
319
|
|
|
318
|
-
| Prop
|
|
319
|
-
|
|
320
|
-
| `alternates`
|
|
321
|
-
| `alternates[].href`
|
|
322
|
-
| `alternates[].hreflang` | `string`
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
347
|
-
|
|
348
|
-
| `title`
|
|
349
|
-
| `description`
|
|
350
|
-
| `
|
|
351
|
-
| `
|
|
352
|
-
| `
|
|
353
|
-
| `
|
|
354
|
-
| `
|
|
355
|
-
| `
|
|
356
|
-
| `
|
|
357
|
-
| `
|
|
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
|
|
373
|
-
|
|
374
|
-
| `index`
|
|
375
|
-
| `follow`
|
|
376
|
-
| `
|
|
377
|
-
| `
|
|
378
|
-
| `extra`
|
|
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
|
-
|
|
392
|
-
|
|
438
|
+
name: "Ere Männistö",
|
|
439
|
+
url: "https://example.com",
|
|
393
440
|
}}
|
|
394
441
|
/>
|
|
395
442
|
```
|
|
396
443
|
|
|
397
|
-
| Prop
|
|
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
|
|
412
|
-
|
|
413
|
-
| `value`
|
|
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,92 @@ Renders Twitter card meta tags for rich previews on X. When used inside `Head`,
|
|
|
431
481
|
/>
|
|
432
482
|
```
|
|
433
483
|
|
|
434
|
-
| Prop
|
|
435
|
-
|
|
436
|
-
| `title`
|
|
437
|
-
| `description` | `string`
|
|
438
|
-
| `
|
|
439
|
-
| `
|
|
440
|
-
| `
|
|
441
|
-
| `
|
|
442
|
-
| `
|
|
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 formatting.
|
|
534
|
+
|
|
535
|
+
Check for issues:
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
pnpm check
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Auto-fix issues:
|
|
542
|
+
|
|
543
|
+
```bash
|
|
544
|
+
pnpm check:fix
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
All pull requests must pass the Biome check and unit tests before merging. These are 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
|
+
```
|
|
569
|
+
|
|
446
570
|
## License
|
|
447
571
|
|
|
448
|
-
MIT © [Ere Männistö](https://github.com/eremannisto)
|
|
572
|
+
MIT © [Ere Männistö](https://github.com/eremannisto)
|
package/index.ts
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export { default as
|
|
3
|
-
export {
|
|
4
|
-
export { default as
|
|
5
|
-
export {
|
|
6
|
-
export { default as
|
|
7
|
-
export {
|
|
8
|
-
export { default as
|
|
9
|
-
export {
|
|
10
|
-
export { default as
|
|
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 {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export
|
|
19
|
-
export type { Props as RobotsProps
|
|
20
|
-
export
|
|
21
|
-
export type { Props as
|
|
22
|
-
export
|
|
23
|
-
|
|
24
|
-
export
|
|
25
|
-
export type { Props as
|
|
26
|
-
|
|
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-
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
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
|
-
"
|
|
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
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
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,21 +1,18 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
2
|
export type FaviconFile = {
|
|
4
|
-
path
|
|
5
|
-
size
|
|
6
|
-
theme
|
|
7
|
-
apple
|
|
3
|
+
path: string
|
|
4
|
+
size?: number
|
|
5
|
+
theme?: "light" | "dark"
|
|
6
|
+
apple?: boolean
|
|
8
7
|
}
|
|
9
8
|
|
|
10
9
|
export type Props = {
|
|
11
|
-
icons
|
|
12
|
-
manifest
|
|
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"))
|
|
28
|
-
if (path.endsWith(".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
|
-
|
|
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")
|
|
40
|
+
if (theme === "dark") return "(prefers-color-scheme: dark)"
|
|
45
41
|
return undefined
|
|
46
42
|
}
|
|
47
43
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
{
|
|
61
|
-
|
|
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
|
|
4
|
-
import Description
|
|
5
|
-
import Canonical
|
|
6
|
-
import Keywords
|
|
7
|
-
import Robots
|
|
8
|
-
import OpenGraph
|
|
9
|
-
import Twitter
|
|
10
|
-
import Favicon
|
|
11
|
-
import Schema
|
|
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
|
|
16
|
-
titleTemplate
|
|
17
|
-
description
|
|
18
|
-
canonical
|
|
19
|
-
keywords
|
|
20
|
-
charset
|
|
21
|
-
viewport
|
|
22
|
-
robots
|
|
23
|
-
openGraph
|
|
24
|
-
twitter
|
|
25
|
-
favicon
|
|
26
|
-
schema
|
|
27
|
-
languageAlternates
|
|
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
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
{
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
{
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
4
|
-
hreflang
|
|
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
|
-
{
|
|
15
|
-
|
|
16
|
-
rel="alternate"
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
5
|
-
description
|
|
6
|
-
url
|
|
7
|
-
type
|
|
8
|
-
siteName
|
|
9
|
-
locale
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
31
|
-
{description
|
|
32
|
-
{
|
|
33
|
-
{
|
|
34
|
-
{
|
|
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
|
-
|
|
37
|
-
{
|
|
38
|
-
{
|
|
39
|
-
{
|
|
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
|
|
4
|
-
follow
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
extra
|
|
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
|
-
|
|
24
|
-
|
|
17
|
+
!archive ? "noarchive" : null,
|
|
18
|
+
!snippet ? "nosnippet" : null,
|
|
25
19
|
extra,
|
|
26
|
-
]
|
|
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
|
|
5
|
-
template
|
|
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
|
|
5
|
-
description
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
27
|
-
{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
|
-
{
|
|
30
|
-
{image?.
|
|
31
|
-
{
|
|
32
|
-
{
|
|
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} />}
|
package/src/lib/metadata.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ComponentProps } from "astro/types"
|
|
2
|
-
import type Head
|
|
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()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
get
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
get
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
get
|
|
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
|
+
}
|