@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 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
+ ![banner](./assets/default/banner.png)
4
+
5
+ ![npm version](https://img.shields.io/npm/v/@mannisto/astro-metadata)
6
+ ![license](https://img.shields.io/npm/l/@mannisto/astro-metadata)
7
+ ![astro peer dependency](https://img.shields.io/npm/dependency-version/@mannisto/astro-metadata/peer/astro)
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,15 @@
1
+ ---
2
+
3
+ export type Props = {
4
+ value?: string
5
+ }
6
+
7
+ const {
8
+ value
9
+ } = Astro.props
10
+
11
+ const canonical = value ?? Astro.url.href
12
+
13
+ ---
14
+
15
+ <link rel="canonical" href={canonical} />
@@ -0,0 +1,13 @@
1
+ ---
2
+
3
+ export type Props = {
4
+ value?: string
5
+ }
6
+
7
+ const {
8
+ value
9
+ } = Astro.props
10
+
11
+ ---
12
+
13
+ {value && <meta name="description" content={value} />}
@@ -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,13 @@
1
+ ---
2
+
3
+ export type Props = {
4
+ value?: string[]
5
+ }
6
+
7
+ const {
8
+ value = []
9
+ } = Astro.props
10
+
11
+ ---
12
+
13
+ {value.length > 0 && <meta name="keywords" content={value.join(", ")} />}
@@ -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,13 @@
1
+ ---
2
+
3
+ export type Props = {
4
+ schema: Record<string, unknown>
5
+ }
6
+
7
+ const {
8
+ schema
9
+ } = Astro.props
10
+
11
+ ---
12
+
13
+ <script is:inline type="application/ld+json" set:html={JSON.stringify(schema)} />
@@ -0,0 +1,15 @@
1
+ ---
2
+
3
+ export type Props = {
4
+ value : string
5
+ template? : `${string}%s${string}`
6
+ }
7
+
8
+ const {
9
+ value,
10
+ template = "%s"
11
+ } = Astro.props
12
+
13
+ ---
14
+
15
+ <title>{template.replace("%s", value)}</title>
@@ -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
+ }