@mannisto/astro-metadata 1.0.0-alpha.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/LICENSE +21 -0
- package/README.md +432 -0
- package/index.ts +27 -0
- package/package.json +25 -0
- package/src/components/Canonical.astro +15 -0
- package/src/components/Description.astro +13 -0
- package/src/components/Favicon.astro +177 -0
- package/src/components/Head.astro +101 -0
- package/src/components/Keywords.astro +13 -0
- package/src/components/LanguageAlternates.astro +20 -0
- package/src/components/OpenGraph.astro +39 -0
- package/src/components/Robots.astro +29 -0
- package/src/components/Schema.astro +13 -0
- package/src/components/Title.astro +15 -0
- package/src/components/Twitter.astro +32 -0
- package/src/lib/metadata.ts +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ere Männistö
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
# Astro Metadata
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
Astro components for managing your page head — metadata, social sharing, favicons, and SEO.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Table of contents
|
|
14
|
+
|
|
15
|
+
- [Installation](#installation)
|
|
16
|
+
- [Patterns](#patterns)
|
|
17
|
+
- [1. Head component](#1-head-component)
|
|
18
|
+
- [2. Individual components](#2-individual-components)
|
|
19
|
+
- [3. Metadata utility](#3-metadata-utility)
|
|
20
|
+
- [Components](#components)
|
|
21
|
+
- [Canonical](#canonical)
|
|
22
|
+
- [Description](#description)
|
|
23
|
+
- [Favicon](#favicon)
|
|
24
|
+
- [Head](#head)
|
|
25
|
+
- [Keywords](#keywords)
|
|
26
|
+
- [LanguageAlternates](#languagealternates)
|
|
27
|
+
- [OpenGraph](#opengraph)
|
|
28
|
+
- [Robots](#robots)
|
|
29
|
+
- [Schema](#schema)
|
|
30
|
+
- [Title](#title)
|
|
31
|
+
- [Twitter](#twitter)
|
|
32
|
+
- [License](#license)
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
```bash
|
|
38
|
+
pnpm add @mannisto/astro-metadata
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Patterns
|
|
44
|
+
|
|
45
|
+
There are three ways to use this package. Pick what suits your project, or combine them freely.
|
|
46
|
+
|
|
47
|
+
### 1. Head component
|
|
48
|
+
|
|
49
|
+
The simplest approach. Use `Head` in your layout and pass props down from your pages. Charset and viewport are included automatically.
|
|
50
|
+
```astro
|
|
51
|
+
---
|
|
52
|
+
// layouts/Layout.astro
|
|
53
|
+
import { Head } from "@mannisto/astro-metadata"
|
|
54
|
+
import type { HeadProps } from "@mannisto/astro-metadata"
|
|
55
|
+
|
|
56
|
+
interface Props extends HeadProps {}
|
|
57
|
+
|
|
58
|
+
const { title, description, ...rest } = Astro.props
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
<html>
|
|
62
|
+
<Head
|
|
63
|
+
title={title}
|
|
64
|
+
description={description}
|
|
65
|
+
titleTemplate="%s | My Site"
|
|
66
|
+
{...rest}
|
|
67
|
+
/>
|
|
68
|
+
<body>
|
|
69
|
+
<slot />
|
|
70
|
+
</body>
|
|
71
|
+
</html>
|
|
72
|
+
```
|
|
73
|
+
```astro
|
|
74
|
+
---
|
|
75
|
+
// pages/index.astro
|
|
76
|
+
import Layout from "../layouts/Layout.astro"
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
<Layout title="Home" description="Welcome to my site">
|
|
80
|
+
<h1>Hello</h1>
|
|
81
|
+
</Layout>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Best for simple sites where pages pass metadata as props to their layout.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### 2. Individual components
|
|
89
|
+
|
|
90
|
+
Use components directly inside your own `<head>`. Useful when you only need specific pieces, or want full control over the structure.
|
|
91
|
+
```astro
|
|
92
|
+
---
|
|
93
|
+
import { Title, Description, OpenGraph, Favicon } from "@mannisto/astro-metadata"
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
<html>
|
|
97
|
+
<head>
|
|
98
|
+
<meta charset="UTF-8" />
|
|
99
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
100
|
+
<Title value="My Page" template="%s | My Site" />
|
|
101
|
+
<Description value="Welcome to my site" />
|
|
102
|
+
<OpenGraph
|
|
103
|
+
title="My Page"
|
|
104
|
+
description="Welcome to my site"
|
|
105
|
+
image={{ url: "/og.jpg", alt: "My Site", width: 1200, height: 630 }}
|
|
106
|
+
/>
|
|
107
|
+
<Favicon
|
|
108
|
+
icons={{
|
|
109
|
+
default: {
|
|
110
|
+
ico: { path: "/favicon.ico" },
|
|
111
|
+
svg: { path: "/favicon.svg" },
|
|
112
|
+
}
|
|
113
|
+
}}
|
|
114
|
+
/>
|
|
115
|
+
</head>
|
|
116
|
+
<body>
|
|
117
|
+
<slot />
|
|
118
|
+
</body>
|
|
119
|
+
</html>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Best for when you want to compose only what you need, or when `Head` is too opinionated for your setup.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
### 3. Metadata utility
|
|
127
|
+
|
|
128
|
+
Set metadata in your page, resolve it in your layout. Eliminates prop drilling through nested layout layers.
|
|
129
|
+
```astro
|
|
130
|
+
---
|
|
131
|
+
// pages/about.astro
|
|
132
|
+
import { Metadata } from "@mannisto/astro-metadata"
|
|
133
|
+
import Layout from "../layouts/Layout.astro"
|
|
134
|
+
|
|
135
|
+
Metadata.set({
|
|
136
|
+
title: "About",
|
|
137
|
+
description: "Learn more about us",
|
|
138
|
+
openGraph: {
|
|
139
|
+
image: { url: "/og/about.jpg", alt: "About" }
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
<Layout>
|
|
145
|
+
<h1>About</h1>
|
|
146
|
+
</Layout>
|
|
147
|
+
```
|
|
148
|
+
```astro
|
|
149
|
+
---
|
|
150
|
+
// layouts/Layout.astro
|
|
151
|
+
import { Head, Metadata } from "@mannisto/astro-metadata"
|
|
152
|
+
|
|
153
|
+
const meta = Metadata.resolve({
|
|
154
|
+
title: "My Site",
|
|
155
|
+
description: "Default description",
|
|
156
|
+
titleTemplate: "%s | My Site",
|
|
157
|
+
})
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
<html>
|
|
161
|
+
<Head {...meta} />
|
|
162
|
+
<body>
|
|
163
|
+
<slot />
|
|
164
|
+
</body>
|
|
165
|
+
</html>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`Metadata.resolve()` merges page values over your layout defaults — whatever the page sets wins, everything else falls back gracefully.
|
|
169
|
+
|
|
170
|
+
Best for sites with deeply nested layouts, or when you want to keep metadata co-located with page content.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Components
|
|
175
|
+
|
|
176
|
+
### Canonical
|
|
177
|
+
|
|
178
|
+
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.
|
|
179
|
+
```astro
|
|
180
|
+
<Canonical value="https://example.com/page" />
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
| Prop | Type | Description |
|
|
184
|
+
|------|------|-------------|
|
|
185
|
+
| `value` | `string` | Canonical URL. Defaults to `Astro.url.href`. |
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
### Description
|
|
190
|
+
```astro
|
|
191
|
+
<Description value="Welcome to my site" />
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
| Prop | Type | Description |
|
|
195
|
+
|------|------|-------------|
|
|
196
|
+
| `value` | `string` | Page description |
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
### Favicon
|
|
201
|
+
|
|
202
|
+
Favicon support with dark and light mode variants, multiple formats, and optional cache busting.
|
|
203
|
+
```astro
|
|
204
|
+
<Favicon
|
|
205
|
+
icons={{
|
|
206
|
+
default: {
|
|
207
|
+
ico: { path: "/favicon.ico" },
|
|
208
|
+
svg: { path: "/favicon.svg" },
|
|
209
|
+
png: [{ path: "/favicon-96x96.png", size: 96 }],
|
|
210
|
+
apple: { path: "/apple-touch-icon.png", size: 180 },
|
|
211
|
+
},
|
|
212
|
+
lightMode: {
|
|
213
|
+
svg: { path: "/favicon-light.svg" },
|
|
214
|
+
},
|
|
215
|
+
darkMode: {
|
|
216
|
+
svg: { path: "/favicon-dark.svg" },
|
|
217
|
+
},
|
|
218
|
+
}}
|
|
219
|
+
manifest="/site.webmanifest"
|
|
220
|
+
cacheBust
|
|
221
|
+
/>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
| Prop | Type | Description |
|
|
225
|
+
|------|------|-------------|
|
|
226
|
+
| `icons.default` | `FaviconIcons` | Default favicon set |
|
|
227
|
+
| `icons.lightMode` | `FaviconIcons` | Favicons shown in light color scheme |
|
|
228
|
+
| `icons.darkMode` | `FaviconIcons` | Favicons shown in dark color scheme |
|
|
229
|
+
| `manifest` | `string` | Path to web app manifest |
|
|
230
|
+
| `cacheBust` | `boolean` | Append `?v={timestamp}` to favicon URLs |
|
|
231
|
+
|
|
232
|
+
#### FaviconIcons
|
|
233
|
+
|
|
234
|
+
| Prop | Type | Description |
|
|
235
|
+
|------|------|-------------|
|
|
236
|
+
| `ico` | `FaviconFile` | `.ico` favicon |
|
|
237
|
+
| `svg` | `FaviconFile` | `.svg` favicon |
|
|
238
|
+
| `png` | `FaviconFile \| FaviconFile[]` | One or more `.png` favicons |
|
|
239
|
+
| `apple` | `FaviconFile` | Apple touch icon |
|
|
240
|
+
|
|
241
|
+
#### FaviconFile
|
|
242
|
+
|
|
243
|
+
| Prop | Type | Description |
|
|
244
|
+
|------|------|-------------|
|
|
245
|
+
| `path` | `string` | Path to the file |
|
|
246
|
+
| `size` | `number` | Size in pixels. Rendered as `NxN` in the `sizes` attribute. |
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
### Head
|
|
251
|
+
|
|
252
|
+
Wraps the entire page head and composes all sub-components internally. Charset and viewport are always included.
|
|
253
|
+
```astro
|
|
254
|
+
<Head
|
|
255
|
+
title="Home"
|
|
256
|
+
titleTemplate="%s | My Site"
|
|
257
|
+
description="Welcome to my site"
|
|
258
|
+
openGraph={{ image: { url: "/og.jpg", alt: "My Site", width: 1200, height: 630 } }}
|
|
259
|
+
favicon={{ icons: { default: { ico: { path: "/favicon.ico" } } } }}
|
|
260
|
+
/>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
| Prop | Type | 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` | Canonical URL. Defaults to `Astro.url.href` |
|
|
269
|
+
| `keywords` | `string[]` | List of keywords |
|
|
270
|
+
| `robots` | `RobotsProps` | Robots directives |
|
|
271
|
+
| `openGraph` | `OpenGraphProps` | Open Graph tags |
|
|
272
|
+
| `twitter` | `TwitterProps` | Twitter card tags |
|
|
273
|
+
| `favicon` | `FaviconProps` | Favicon configuration |
|
|
274
|
+
| `schema` | `SchemaProps` | JSON-LD structured data |
|
|
275
|
+
| `languageAlternates` | `LanguageAlternate[]` | Hreflang alternate links |
|
|
276
|
+
|
|
277
|
+
#### Slots
|
|
278
|
+
```astro
|
|
279
|
+
<Head title="My Site">
|
|
280
|
+
<!-- Renders before charset and viewport -->
|
|
281
|
+
<meta slot="top" http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
282
|
+
|
|
283
|
+
<!-- Renders at the end of <head> -->
|
|
284
|
+
<script src={analyticsUrl} />
|
|
285
|
+
</Head>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
### Keywords
|
|
291
|
+
```astro
|
|
292
|
+
<Keywords value={["astro", "seo", "metadata"]} />
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
| Prop | Type | Description |
|
|
296
|
+
|------|------|-------------|
|
|
297
|
+
| `value` | `string[]` | List of keywords |
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
### LanguageAlternates
|
|
302
|
+
|
|
303
|
+
Renders `<link rel="alternate" hreflang>` tags for multilingual sites. Tells search engines which language version to serve for a given region.
|
|
304
|
+
```astro
|
|
305
|
+
<LanguageAlternates
|
|
306
|
+
alternates={[
|
|
307
|
+
{ href: "https://example.com/en", hreflang: "en" },
|
|
308
|
+
{ href: "https://example.com/fi", hreflang: "fi" },
|
|
309
|
+
{ href: "https://example.com", hreflang: "x-default" },
|
|
310
|
+
]}
|
|
311
|
+
/>
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
| Prop | Type | Description |
|
|
315
|
+
|------|------|-------------|
|
|
316
|
+
| `alternates` | `LanguageAlternate[]` | List of alternate language pages |
|
|
317
|
+
| `alternates[].href` | `string` | Full URL of the alternate page |
|
|
318
|
+
| `alternates[].hreflang` | `string` | Language or region code, e.g. `en`, `fi`, `en-US`, `x-default` |
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
### OpenGraph
|
|
323
|
+
|
|
324
|
+
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.
|
|
325
|
+
```astro
|
|
326
|
+
<OpenGraph
|
|
327
|
+
title="My Page"
|
|
328
|
+
description="Welcome to my site"
|
|
329
|
+
image={{ url: "/og.jpg", alt: "My Site", width: 1200, height: 630 }}
|
|
330
|
+
url="https://example.com"
|
|
331
|
+
siteName="My Site"
|
|
332
|
+
locale="en_US"
|
|
333
|
+
/>
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
| Prop | Type | Default | Description |
|
|
337
|
+
|------|------|---------|-------------|
|
|
338
|
+
| `title` | `string` | — | OG title |
|
|
339
|
+
| `description` | `string` | — | OG description |
|
|
340
|
+
| `image.url` | `string` | — | Image URL. Required if image is set. |
|
|
341
|
+
| `image.alt` | `string` | — | Image alt text |
|
|
342
|
+
| `image.width` | `number` | — | Image width in pixels. Recommended: `1200` |
|
|
343
|
+
| `image.height` | `number` | — | Image height in pixels. Recommended: `630` |
|
|
344
|
+
| `url` | `string` | — | Canonical URL for the OG object |
|
|
345
|
+
| `type` | `string` | `"website"` | OG type |
|
|
346
|
+
| `siteName` | `string` | — | Name of the site |
|
|
347
|
+
| `locale` | `string` | — | Locale, e.g. `en_US` |
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
### Robots
|
|
352
|
+
|
|
353
|
+
Controls how search engines crawl and index your page. Defaults to `index, follow`.
|
|
354
|
+
```astro
|
|
355
|
+
<Robots
|
|
356
|
+
noArchive
|
|
357
|
+
extra="max-snippet:-1, max-image-preview:large, max-video-preview:-1"
|
|
358
|
+
/>
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
| Prop | Type | Default | Description |
|
|
362
|
+
|------|------|---------|-------------|
|
|
363
|
+
| `index` | `boolean` | `true` | Allow indexing |
|
|
364
|
+
| `follow` | `boolean` | `true` | Allow following links |
|
|
365
|
+
| `noArchive` | `boolean` | — | Prevent search engines from caching the page |
|
|
366
|
+
| `noSnippet` | `boolean` | — | Prevent text snippets in search results |
|
|
367
|
+
| `extra` | `string` | — | Additional directives, e.g. `"max-snippet:-1, max-image-preview:large"` |
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
### Schema
|
|
372
|
+
|
|
373
|
+
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.
|
|
374
|
+
```astro
|
|
375
|
+
<Schema
|
|
376
|
+
schema={{
|
|
377
|
+
"@context": "https://schema.org",
|
|
378
|
+
"@type": "Person",
|
|
379
|
+
"name": "Ere Männistö",
|
|
380
|
+
"url": "https://example.com",
|
|
381
|
+
}}
|
|
382
|
+
/>
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
| Prop | Type | Description |
|
|
386
|
+
|------|------|-------------|
|
|
387
|
+
| `schema` | `Record<string, unknown>` | JSON-LD object |
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
### Title
|
|
392
|
+
|
|
393
|
+
Renders the `<title>` tag. The template must contain `%s`, which is replaced with the page title — TypeScript enforces this at the type level.
|
|
394
|
+
```astro
|
|
395
|
+
<Title value="My Page" template="%s | My Site" />
|
|
396
|
+
<!-- <title>My Page | My Site</title> -->
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
| Prop | Type | Description |
|
|
400
|
+
|------|------|-------------|
|
|
401
|
+
| `value` | `string` | Page title. Required. |
|
|
402
|
+
| `template` | `` `${string}%s${string}` `` | Template string. Must contain `%s`. |
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
### Twitter
|
|
407
|
+
|
|
408
|
+
Renders Twitter card meta tags for rich previews on X. When used inside `Head`, `title` and `description` fall back to the page values automatically.
|
|
409
|
+
```astro
|
|
410
|
+
<Twitter
|
|
411
|
+
card="summary_large_image"
|
|
412
|
+
site="@mysite"
|
|
413
|
+
creator="@myhandle"
|
|
414
|
+
image={{ url: "/og.jpg", alt: "My Site" }}
|
|
415
|
+
/>
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
| Prop | Type | Default | Description |
|
|
419
|
+
|------|------|---------|-------------|
|
|
420
|
+
| `title` | `string` | — | Card title |
|
|
421
|
+
| `description` | `string` | — | Card description |
|
|
422
|
+
| `image.url` | `string` | — | Image URL. Required if image is set. |
|
|
423
|
+
| `image.alt` | `string` | — | Image alt text |
|
|
424
|
+
| `card` | `"summary" \| "summary_large_image"` | `"summary_large_image"` | Card type |
|
|
425
|
+
| `site` | `string` | — | Twitter handle of the site, e.g. `@mysite` |
|
|
426
|
+
| `creator` | `string` | — | Twitter handle of the content author |
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## License
|
|
431
|
+
|
|
432
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
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"
|
|
11
|
+
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,
|
|
24
|
+
FaviconIcons } from "./src/components/Favicon.astro"
|
|
25
|
+
export type { Props as SchemaProps } from "./src/components/Schema.astro"
|
|
26
|
+
export type { Props as LanguageAlternatesProps,
|
|
27
|
+
LanguageAlternate } from "./src/components/LanguageAlternates.astro"
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mannisto/astro-metadata",
|
|
3
|
+
"version": "1.0.0-alpha.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Astro components for managing your page head — metadata, social sharing, favicons, and SEO.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/eremannisto/astro-metadata#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/eremannisto/astro-metadata"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/eremannisto/astro-metadata/issues"
|
|
14
|
+
},
|
|
15
|
+
"files": ["src", "index.ts"],
|
|
16
|
+
"exports": "./index.ts",
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"astro": "^4.3.0 || ^5.0.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"astro": "^5.17.2",
|
|
22
|
+
"typescript": "^5.9.3"
|
|
23
|
+
},
|
|
24
|
+
"keywords": ["astro-component", "astro", "head", "seo", "meta"]
|
|
25
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
---
|
|
2
|
+
|
|
3
|
+
export type FaviconFile = {
|
|
4
|
+
path : string
|
|
5
|
+
size? : number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type FaviconIcons = {
|
|
9
|
+
ico? : FaviconFile
|
|
10
|
+
png? : FaviconFile | FaviconFile[]
|
|
11
|
+
svg? : FaviconFile
|
|
12
|
+
apple? : FaviconFile
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type PreparedFile = {
|
|
16
|
+
path : string
|
|
17
|
+
size? : string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type PreparedIcons = {
|
|
21
|
+
ico? : PreparedFile
|
|
22
|
+
png? : PreparedFile[]
|
|
23
|
+
svg? : PreparedFile
|
|
24
|
+
apple? : PreparedFile
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type Props = {
|
|
28
|
+
icons: {
|
|
29
|
+
default? : FaviconIcons
|
|
30
|
+
lightMode? : FaviconIcons
|
|
31
|
+
darkMode? : FaviconIcons
|
|
32
|
+
}
|
|
33
|
+
manifest? : string
|
|
34
|
+
cacheBust? : boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const {
|
|
38
|
+
icons,
|
|
39
|
+
manifest,
|
|
40
|
+
cacheBust = false
|
|
41
|
+
} = Astro.props
|
|
42
|
+
|
|
43
|
+
const cacheBustString = cacheBust
|
|
44
|
+
? `?v=${Date.now()}`
|
|
45
|
+
: ""
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Prepares a single favicon file by appending the cache bust string
|
|
49
|
+
* and converting the numeric size to the "NxN" format browsers expect.
|
|
50
|
+
*
|
|
51
|
+
* @param file - The favicon file to prepare.
|
|
52
|
+
* @returns The prepared favicon file, or undefined if no file was provided.
|
|
53
|
+
*/
|
|
54
|
+
function prepareFile(file?: FaviconFile): PreparedFile | undefined {
|
|
55
|
+
if (!file) return undefined
|
|
56
|
+
return {
|
|
57
|
+
path: `${file.path}${cacheBustString}`,
|
|
58
|
+
size: file.size ? `${file.size}x${file.size}` : undefined,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Prepares a full set of favicon icons by running each format through prepareFile.
|
|
64
|
+
* Normalizes the png field to always be an array for consistent rendering.
|
|
65
|
+
*
|
|
66
|
+
* @param set - The favicon icon set to prepare.
|
|
67
|
+
* @returns The prepared favicon icon set, or undefined if no set was provided.
|
|
68
|
+
*/
|
|
69
|
+
function prepareIcons(set?: FaviconIcons): PreparedIcons | undefined {
|
|
70
|
+
if (!set) return undefined
|
|
71
|
+
return {
|
|
72
|
+
ico: prepareFile(set.ico),
|
|
73
|
+
svg: prepareFile(set.svg),
|
|
74
|
+
apple: prepareFile(set.apple),
|
|
75
|
+
png: set.png
|
|
76
|
+
? (Array.isArray(set.png) ? set.png : [set.png]).map((file) => prepareFile(file)!)
|
|
77
|
+
: undefined,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const prepared = {
|
|
82
|
+
default: prepareIcons(icons.default),
|
|
83
|
+
lightMode: prepareIcons(icons.lightMode),
|
|
84
|
+
darkMode: prepareIcons(icons.darkMode),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
{manifest && <link rel="manifest" href={manifest} />}
|
|
90
|
+
|
|
91
|
+
{prepared.default?.ico && (
|
|
92
|
+
<link
|
|
93
|
+
rel="icon"
|
|
94
|
+
type="image/x-icon"
|
|
95
|
+
href={prepared.default.ico.path}
|
|
96
|
+
sizes={prepared.default.ico.size}
|
|
97
|
+
/>
|
|
98
|
+
)}
|
|
99
|
+
{prepared.default?.svg && (
|
|
100
|
+
<link
|
|
101
|
+
rel="icon"
|
|
102
|
+
type="image/svg+xml"
|
|
103
|
+
href={prepared.default.svg.path}
|
|
104
|
+
sizes={prepared.default.svg.size}
|
|
105
|
+
/>
|
|
106
|
+
)}
|
|
107
|
+
{prepared.default?.apple && (
|
|
108
|
+
<link
|
|
109
|
+
rel="apple-touch-icon"
|
|
110
|
+
href={prepared.default.apple.path}
|
|
111
|
+
sizes={prepared.default.apple.size}
|
|
112
|
+
/>
|
|
113
|
+
)}
|
|
114
|
+
{prepared.default?.png?.map((png) => (
|
|
115
|
+
<link
|
|
116
|
+
rel="icon"
|
|
117
|
+
type="image/png"
|
|
118
|
+
href={png.path}
|
|
119
|
+
sizes={png.size}
|
|
120
|
+
/>
|
|
121
|
+
))}
|
|
122
|
+
|
|
123
|
+
{prepared.lightMode?.ico && (
|
|
124
|
+
<link
|
|
125
|
+
rel="icon"
|
|
126
|
+
type="image/x-icon"
|
|
127
|
+
href={prepared.lightMode.ico.path}
|
|
128
|
+
sizes={prepared.lightMode.ico.size}
|
|
129
|
+
media="(prefers-color-scheme: light)"
|
|
130
|
+
/>
|
|
131
|
+
)}
|
|
132
|
+
{prepared.lightMode?.svg && (
|
|
133
|
+
<link
|
|
134
|
+
rel="icon"
|
|
135
|
+
type="image/svg+xml"
|
|
136
|
+
href={prepared.lightMode.svg.path}
|
|
137
|
+
sizes={prepared.lightMode.svg.size}
|
|
138
|
+
media="(prefers-color-scheme: light)"
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
141
|
+
{prepared.lightMode?.png?.map((png) => (
|
|
142
|
+
<link
|
|
143
|
+
rel="icon"
|
|
144
|
+
type="image/png"
|
|
145
|
+
href={png.path}
|
|
146
|
+
sizes={png.size}
|
|
147
|
+
media="(prefers-color-scheme: light)"
|
|
148
|
+
/>
|
|
149
|
+
))}
|
|
150
|
+
|
|
151
|
+
{prepared.darkMode?.ico && (
|
|
152
|
+
<link
|
|
153
|
+
rel="icon"
|
|
154
|
+
type="image/x-icon"
|
|
155
|
+
href={prepared.darkMode.ico.path}
|
|
156
|
+
sizes={prepared.darkMode.ico.size}
|
|
157
|
+
media="(prefers-color-scheme: dark)"
|
|
158
|
+
/>
|
|
159
|
+
)}
|
|
160
|
+
{prepared.darkMode?.svg && (
|
|
161
|
+
<link
|
|
162
|
+
rel="icon"
|
|
163
|
+
type="image/svg+xml"
|
|
164
|
+
href={prepared.darkMode.svg.path}
|
|
165
|
+
sizes={prepared.darkMode.svg.size}
|
|
166
|
+
media="(prefers-color-scheme: dark)"
|
|
167
|
+
/>
|
|
168
|
+
)}
|
|
169
|
+
{prepared.darkMode?.png?.map((png) => (
|
|
170
|
+
<link
|
|
171
|
+
rel="icon"
|
|
172
|
+
type="image/png"
|
|
173
|
+
href={png.path}
|
|
174
|
+
sizes={png.size}
|
|
175
|
+
media="(prefers-color-scheme: dark)"
|
|
176
|
+
/>
|
|
177
|
+
))}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
|
|
3
|
+
import type { ComponentProps } from "astro/types"
|
|
4
|
+
import Title from "./Title.astro"
|
|
5
|
+
import Description from "./Description.astro"
|
|
6
|
+
import Canonical from "./Canonical.astro"
|
|
7
|
+
import Keywords from "./Keywords.astro"
|
|
8
|
+
import Robots from "./Robots.astro"
|
|
9
|
+
import OpenGraph from "./OpenGraph.astro"
|
|
10
|
+
import Twitter from "./Twitter.astro"
|
|
11
|
+
import Favicon from "./Favicon.astro"
|
|
12
|
+
import Schema from "./Schema.astro"
|
|
13
|
+
import LanguageAlternates from "./LanguageAlternates.astro"
|
|
14
|
+
|
|
15
|
+
export type Props = {
|
|
16
|
+
title : string
|
|
17
|
+
titleTemplate? : `${string}%s${string}`
|
|
18
|
+
description? : string
|
|
19
|
+
canonical? : string
|
|
20
|
+
keywords? : string[]
|
|
21
|
+
robots? : ComponentProps<typeof Robots>
|
|
22
|
+
openGraph? : ComponentProps<typeof OpenGraph>
|
|
23
|
+
twitter? : ComponentProps<typeof Twitter>
|
|
24
|
+
favicon? : ComponentProps<typeof Favicon>
|
|
25
|
+
schema? : ComponentProps<typeof Schema>
|
|
26
|
+
languageAlternates? : ComponentProps<typeof LanguageAlternates>["alternates"]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
title,
|
|
31
|
+
titleTemplate,
|
|
32
|
+
description,
|
|
33
|
+
canonical,
|
|
34
|
+
keywords,
|
|
35
|
+
robots,
|
|
36
|
+
openGraph,
|
|
37
|
+
twitter,
|
|
38
|
+
favicon,
|
|
39
|
+
schema,
|
|
40
|
+
languageAlternates,
|
|
41
|
+
} = Astro.props
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
<head>
|
|
46
|
+
<slot name="top" />
|
|
47
|
+
|
|
48
|
+
<meta charset="UTF-8" />
|
|
49
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
50
|
+
|
|
51
|
+
<Title value={title} template={titleTemplate} />
|
|
52
|
+
<Description value={description} />
|
|
53
|
+
<Canonical value={canonical} />
|
|
54
|
+
<Keywords value={keywords} />
|
|
55
|
+
<Robots
|
|
56
|
+
index={robots?.index}
|
|
57
|
+
follow={robots?.follow}
|
|
58
|
+
noArchive={robots?.noArchive}
|
|
59
|
+
noSnippet={robots?.noSnippet}
|
|
60
|
+
extra={robots?.extra}
|
|
61
|
+
/>
|
|
62
|
+
|
|
63
|
+
{languageAlternates && (
|
|
64
|
+
<LanguageAlternates alternates={languageAlternates} />
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
{openGraph && (
|
|
68
|
+
<OpenGraph
|
|
69
|
+
title={openGraph.title ?? title}
|
|
70
|
+
description={openGraph.description ?? description}
|
|
71
|
+
image={openGraph.image}
|
|
72
|
+
url={openGraph.url ?? canonical}
|
|
73
|
+
type={openGraph.type}
|
|
74
|
+
siteName={openGraph.siteName}
|
|
75
|
+
locale={openGraph.locale}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{twitter && (
|
|
80
|
+
<Twitter
|
|
81
|
+
title={twitter.title ?? title}
|
|
82
|
+
description={twitter.description ?? description}
|
|
83
|
+
image={twitter.image}
|
|
84
|
+
card={twitter.card}
|
|
85
|
+
site={twitter.site}
|
|
86
|
+
creator={twitter.creator}
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{favicon && (
|
|
91
|
+
<Favicon
|
|
92
|
+
icons={favicon.icons}
|
|
93
|
+
manifest={favicon.manifest}
|
|
94
|
+
cacheBust={favicon.cacheBust}
|
|
95
|
+
/>
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
{schema && <Schema schema={schema.schema} />}
|
|
99
|
+
|
|
100
|
+
<slot />
|
|
101
|
+
</head>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
export type LanguageAlternate = {
|
|
3
|
+
href : string
|
|
4
|
+
hreflang : string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type Props = {
|
|
8
|
+
alternates: LanguageAlternate[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { alternates } = Astro.props
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{alternates.map((alternate) => (
|
|
15
|
+
<link
|
|
16
|
+
rel="alternate"
|
|
17
|
+
href={alternate.href}
|
|
18
|
+
hreflang={alternate.hreflang}
|
|
19
|
+
/>
|
|
20
|
+
))}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
|
|
3
|
+
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
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
title,
|
|
20
|
+
description,
|
|
21
|
+
image,
|
|
22
|
+
url,
|
|
23
|
+
type = "website",
|
|
24
|
+
siteName,
|
|
25
|
+
locale,
|
|
26
|
+
} = Astro.props
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
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)} />}
|
|
35
|
+
{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} />}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
export type Props = {
|
|
3
|
+
index? : boolean
|
|
4
|
+
follow? : boolean
|
|
5
|
+
noArchive? : boolean
|
|
6
|
+
noSnippet? : boolean
|
|
7
|
+
extra? : string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
index = true,
|
|
12
|
+
follow = true,
|
|
13
|
+
noArchive,
|
|
14
|
+
noSnippet,
|
|
15
|
+
extra,
|
|
16
|
+
} = Astro.props
|
|
17
|
+
|
|
18
|
+
// Combine the directives into a comma separated string,
|
|
19
|
+
// for example: "index, follow, noarchive, nosnippet"
|
|
20
|
+
const directives = [
|
|
21
|
+
index ? "index" : "noindex",
|
|
22
|
+
follow ? "follow" : "nofollow",
|
|
23
|
+
noArchive && "noarchive",
|
|
24
|
+
noSnippet && "nosnippet",
|
|
25
|
+
extra,
|
|
26
|
+
].filter(Boolean).join(", ")
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
<meta name="robots" content={directives} />
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
|
|
3
|
+
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
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
title,
|
|
17
|
+
description,
|
|
18
|
+
image,
|
|
19
|
+
card = "summary_large_image",
|
|
20
|
+
site,
|
|
21
|
+
creator,
|
|
22
|
+
} = Astro.props
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
{card && <meta name="twitter:card" content={card} />}
|
|
27
|
+
{title && <meta name="twitter:title" content={title} />}
|
|
28
|
+
{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} />}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ComponentProps } from "astro/types"
|
|
2
|
+
import type Head from "../components/Head.astro"
|
|
3
|
+
|
|
4
|
+
let store: Partial<ComponentProps<typeof Head>> = {}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Metadata manages page-level head properties.
|
|
8
|
+
* Call set() in your page, then resolve() in your layout.
|
|
9
|
+
*/
|
|
10
|
+
export const Metadata = {
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Replace the current metadata store with new values.
|
|
14
|
+
*
|
|
15
|
+
* @param values - The head properties to store.
|
|
16
|
+
*/
|
|
17
|
+
set(values: Partial<ComponentProps<typeof Head>>) {
|
|
18
|
+
store = values
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Return the current metadata store.
|
|
23
|
+
*
|
|
24
|
+
* @returns The current head properties.
|
|
25
|
+
*/
|
|
26
|
+
get(): Partial<ComponentProps<typeof Head>> {
|
|
27
|
+
return store
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Merge stored values over the provided defaults.
|
|
32
|
+
* Use this in your layout to apply page-level overrides.
|
|
33
|
+
*
|
|
34
|
+
* @param defaults - The default head properties from your layout.
|
|
35
|
+
* @returns The merged head properties.
|
|
36
|
+
*/
|
|
37
|
+
resolve(defaults: Partial<ComponentProps<typeof Head>>): Partial<ComponentProps<typeof Head>> {
|
|
38
|
+
return { ...defaults, ...store }
|
|
39
|
+
},
|
|
40
|
+
|
|
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
|
+
}
|