@mannisto/astro-meta 0.0.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,124 @@
1
+ # Astro Meta
2
+
3
+ Easy to use metadata components for Astro.
4
+
5
+ ## Installation
6
+ ```bash
7
+ pnpm add @mannisto/astro-meta
8
+ ```
9
+
10
+ ## Usage
11
+
12
+ Use the `Head` component in your layout:
13
+ ```astro
14
+ ---
15
+ import { Head } from "@mannisto/astro-meta"
16
+ ---
17
+
18
+ <html>
19
+ <Head
20
+ title="My Site"
21
+ description="Welcome to my site"
22
+ canonical="https://example.com"
23
+ robots={{ index: true, follow: true }}
24
+ og={{
25
+ title: "My Site",
26
+ image: "/og.jpg",
27
+ siteName: "My Site",
28
+ locale: "en_US",
29
+ }}
30
+ twitter={{
31
+ card: "summary_large_image",
32
+ site: "@mysite",
33
+ }}
34
+ favicon={{
35
+ icons: {
36
+ default: {
37
+ ico: { path: "/favicon.ico" },
38
+ svg: { path: "/favicon.svg" },
39
+ png: [{ path: "/favicon-96x96.png", size: 96 }],
40
+ apple: { path: "/apple-touch-icon.png", size: 180 },
41
+ },
42
+ lightMode: {
43
+ svg: { path: "/favicon-light.svg" },
44
+ },
45
+ darkMode: {
46
+ svg: { path: "/favicon-dark.svg" },
47
+ },
48
+ },
49
+ manifest: "/site.webmanifest",
50
+ cacheBust: true,
51
+ }}
52
+ />
53
+ <body>
54
+ ...
55
+ </body>
56
+ </html>
57
+ ```
58
+
59
+ ## Props
60
+
61
+ | Prop | Type | Required | Description |
62
+ |------|------|----------|-------------|
63
+ | `title` | `string` | Yes | Page title |
64
+ | `description` | `string` | No | Page description |
65
+ | `canonical` | `string` | No | Canonical URL |
66
+ | `keywords` | `string[]` | No | List of keywords |
67
+ | `robots.index` | `boolean` | No | Allow indexing, defaults to `true` |
68
+ | `robots.follow` | `boolean` | No | Allow following links, defaults to `true` |
69
+ | `robots.noArchive` | `boolean` | No | Prevent caching |
70
+ | `robots.noSnippet` | `boolean` | No | Prevent text snippets in search results |
71
+ | `og.title` | `string` | No | Defaults to `title` |
72
+ | `og.description` | `string` | No | Defaults to `description` |
73
+ | `og.image` | `string` | No | Open Graph image URL |
74
+ | `og.url` | `string` | No | Defaults to `canonical` |
75
+ | `og.type` | `string` | No | Defaults to `website` |
76
+ | `og.siteName` | `string` | No | Site name |
77
+ | `og.locale` | `string` | No | Locale, e.g. `en_US` |
78
+ | `twitter.title` | `string` | No | Defaults to `title` |
79
+ | `twitter.description` | `string` | No | Defaults to `description` |
80
+ | `twitter.image` | `string` | No | Twitter card image URL |
81
+ | `twitter.card` | `"summary" \| "summary_large_image"` | No | Defaults to `summary_large_image` |
82
+ | `twitter.site` | `string` | No | Twitter handle of the site, e.g. `@mysite` |
83
+ | `twitter.creator` | `string` | No | Twitter handle of the author |
84
+ | `favicon.icons.default` | `FaviconSet` | No | Default favicon set |
85
+ | `favicon.icons.lightMode` | `FaviconSet` | No | Favicons for light mode |
86
+ | `favicon.icons.darkMode` | `FaviconSet` | No | Favicons for dark mode |
87
+ | `favicon.manifest` | `string` | No | Path to web manifest |
88
+ | `favicon.cacheBust` | `boolean` | No | Append timestamp to favicon URLs to prevent caching |
89
+
90
+ ### FaviconSet
91
+
92
+ | Prop | Type | Description |
93
+ |------|------|-------------|
94
+ | `ico` | `{ path: string, size?: number }` | `.ico` favicon |
95
+ | `svg` | `{ path: string, size?: number }` | `.svg` favicon |
96
+ | `png` | `{ path: string, size?: number } \| { path: string, size?: number }[]` | `.png` favicon(s) |
97
+ | `apple` | `{ path: string, size?: number }` | Apple touch icon |
98
+
99
+ ### Slots
100
+
101
+ Two slots are available for injecting additional tags:
102
+ ```astro
103
+ <Head title="My Site">
104
+ <meta slot="top" http-equiv="X-UA-Compatible" content="IE=edge" />
105
+ <script src={fontAwesomeUrl} />
106
+ </Head>
107
+ ```
108
+
109
+ `slot="top"` renders before charset and viewport. The default slot renders at the end.
110
+
111
+ ### Individual components
112
+
113
+ All components are available as named exports:
114
+ ```astro
115
+ ---
116
+ import { OpenGraph, Twitter } from "@mannisto/astro-meta"
117
+ ---
118
+ ```
119
+
120
+ ## License
121
+
122
+ MIT License
123
+
124
+ Copyright (c) 2026 Ere Männistö
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@mannisto/astro-meta",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "Reusable metadata components for Astro",
6
+ "license": "MIT",
7
+ "files": ["src"],
8
+ "exports": {
9
+ ".": "./src/index.ts"
10
+ },
11
+ "peerDependencies": {
12
+ "astro": "^5.0.0"
13
+ },
14
+ "devDependencies": {
15
+ "astro": "^5.17.2",
16
+ "typescript": "^5.9.3"
17
+ },
18
+ "keywords": ["astro", "meta", "seo", "head"]
19
+ }
@@ -0,0 +1,13 @@
1
+ ---
2
+
3
+ interface Props {
4
+ value?: string
5
+ }
6
+
7
+ const {
8
+ value
9
+ } = Astro.props
10
+
11
+ ---
12
+
13
+ {value && <link rel="canonical" href={value} />}
@@ -0,0 +1,13 @@
1
+ ---
2
+
3
+ interface 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,155 @@
1
+ ---
2
+ type Icon = {
3
+ path: string
4
+ size?: number
5
+ }
6
+
7
+ type FaviconSet = {
8
+ ico?: Icon
9
+ png?: Icon | Icon[]
10
+ svg?: Icon
11
+ apple?: Icon
12
+ }
13
+
14
+ interface Props {
15
+ icons: {
16
+ default?: FaviconSet
17
+ lightMode?: FaviconSet
18
+ darkMode?: FaviconSet
19
+ }
20
+ manifest?: string
21
+ cacheBust?: boolean
22
+ }
23
+
24
+ const { icons, manifest, cacheBust } = Astro.props
25
+
26
+ const bust = cacheBust ? `?v=${Date.now()}` : ""
27
+
28
+ type PreparedIcon = {
29
+ path: string
30
+ size?: string
31
+ }
32
+
33
+ type PreparedFaviconSet = {
34
+ ico?: PreparedIcon
35
+ png?: PreparedIcon[]
36
+ svg?: PreparedIcon
37
+ apple?: PreparedIcon
38
+ }
39
+
40
+ function prepareIcon(icon?: Icon): PreparedIcon | undefined {
41
+ if (!icon) return undefined
42
+ return {
43
+ path: `${icon.path}${bust}`,
44
+ size: icon.size ? `${icon.size}x${icon.size}` : undefined,
45
+ }
46
+ }
47
+
48
+ function prepareFaviconSet(set?: FaviconSet): PreparedFaviconSet | undefined {
49
+ if (!set) return undefined
50
+ return {
51
+ ico: prepareIcon(set.ico),
52
+ svg: prepareIcon(set.svg),
53
+ apple: prepareIcon(set.apple),
54
+ png: set.png
55
+ ? (Array.isArray(set.png) ? set.png : [set.png]).map((icon) => prepareIcon(icon)!)
56
+ : undefined,
57
+ }
58
+ }
59
+
60
+ const prepared = {
61
+ default: prepareFaviconSet(icons.default),
62
+ lightMode: prepareFaviconSet(icons.lightMode),
63
+ darkMode: prepareFaviconSet(icons.darkMode),
64
+ }
65
+ ---
66
+
67
+ {manifest && <link rel="manifest" href={manifest} />}
68
+
69
+ {prepared.default?.ico && (
70
+ <link
71
+ rel="icon"
72
+ type="image/x-icon"
73
+ href={prepared.default.ico.path}
74
+ sizes={prepared.default.ico.size}
75
+ />
76
+ )}
77
+ {prepared.default?.svg && (
78
+ <link
79
+ rel="icon"
80
+ type="image/svg+xml"
81
+ href={prepared.default.svg.path}
82
+ sizes={prepared.default.svg.size}
83
+ />
84
+ )}
85
+ {prepared.default?.apple && (
86
+ <link
87
+ rel="apple-touch-icon"
88
+ href={prepared.default.apple.path}
89
+ sizes={prepared.default.apple.size}
90
+ />
91
+ )}
92
+ {prepared.default?.png?.map((png) => (
93
+ <link
94
+ rel="icon"
95
+ type="image/png"
96
+ href={png.path}
97
+ sizes={png.size}
98
+ />
99
+ ))}
100
+
101
+ {prepared.lightMode?.ico && (
102
+ <link
103
+ rel="icon"
104
+ type="image/x-icon"
105
+ href={prepared.lightMode.ico.path}
106
+ sizes={prepared.lightMode.ico.size}
107
+ media="(prefers-color-scheme: light)"
108
+ />
109
+ )}
110
+ {prepared.lightMode?.svg && (
111
+ <link
112
+ rel="icon"
113
+ type="image/svg+xml"
114
+ href={prepared.lightMode.svg.path}
115
+ sizes={prepared.lightMode.svg.size}
116
+ media="(prefers-color-scheme: light)"
117
+ />
118
+ )}
119
+ {prepared.lightMode?.png?.map((png) => (
120
+ <link
121
+ rel="icon"
122
+ type="image/png"
123
+ href={png.path}
124
+ sizes={png.size}
125
+ media="(prefers-color-scheme: light)"
126
+ />
127
+ ))}
128
+
129
+ {prepared.darkMode?.ico && (
130
+ <link
131
+ rel="icon"
132
+ type="image/x-icon"
133
+ href={prepared.darkMode.ico.path}
134
+ sizes={prepared.darkMode.ico.size}
135
+ media="(prefers-color-scheme: dark)"
136
+ />
137
+ )}
138
+ {prepared.darkMode?.svg && (
139
+ <link
140
+ rel="icon"
141
+ type="image/svg+xml"
142
+ href={prepared.darkMode.svg.path}
143
+ sizes={prepared.darkMode.svg.size}
144
+ media="(prefers-color-scheme: dark)"
145
+ />
146
+ )}
147
+ {prepared.darkMode?.png?.map((png) => (
148
+ <link
149
+ rel="icon"
150
+ type="image/png"
151
+ href={png.path}
152
+ sizes={png.size}
153
+ media="(prefers-color-scheme: dark)"
154
+ />
155
+ ))}
@@ -0,0 +1,84 @@
1
+ ---
2
+ import type { ComponentProps } from "astro/types"
3
+ import Title from "./Title.astro"
4
+ import Description from "./Description.astro"
5
+ import Canonical from "./Canonical.astro"
6
+ import Keywords from "./Keywords.astro"
7
+ import Robots from "./Robots.astro"
8
+ import OpenGraph from "./OpenGraph.astro"
9
+ import Twitter from "./Twitter.astro"
10
+ import Favicon from "./Favicon.astro"
11
+
12
+ interface Props {
13
+ title: string
14
+ description?: string
15
+ canonical?: string
16
+ keywords?: string[]
17
+ robots?: ComponentProps<typeof Robots>
18
+ og?: ComponentProps<typeof OpenGraph>
19
+ twitter?: ComponentProps<typeof Twitter>
20
+ favicon?: ComponentProps<typeof Favicon>
21
+ }
22
+
23
+ const {
24
+ title,
25
+ description,
26
+ canonical,
27
+ keywords,
28
+ robots = { index: true, follow: true },
29
+ og,
30
+ twitter,
31
+ favicon,
32
+ } = Astro.props
33
+ ---
34
+
35
+ <head>
36
+ <slot name="top" />
37
+
38
+ <meta charset="UTF-8" />
39
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
40
+
41
+ <Title value={title} />
42
+ <Description value={description} />
43
+ <Canonical value={canonical} />
44
+ <Keywords value={keywords} />
45
+ <Robots
46
+ index={robots.index}
47
+ follow={robots.follow}
48
+ noArchive={robots.noArchive}
49
+ noSnippet={robots.noSnippet}
50
+ />
51
+
52
+ {og && (
53
+ <OpenGraph
54
+ title={og.title ?? title}
55
+ description={og.description ?? description}
56
+ image={og.image}
57
+ url={og.url ?? canonical}
58
+ type={og.type}
59
+ siteName={og.siteName}
60
+ locale={og.locale}
61
+ />
62
+ )}
63
+
64
+ {twitter && (
65
+ <Twitter
66
+ title={twitter.title ?? title}
67
+ description={twitter.description ?? description}
68
+ image={twitter.image}
69
+ card={twitter.card}
70
+ site={twitter.site}
71
+ creator={twitter.creator}
72
+ />
73
+ )}
74
+
75
+ {favicon && (
76
+ <Favicon
77
+ icons={favicon.icons}
78
+ manifest={favicon.manifest}
79
+ cacheBust={favicon.cacheBust}
80
+ />
81
+ )}
82
+
83
+ <slot />
84
+ </head>
@@ -0,0 +1,13 @@
1
+ ---
2
+
3
+ interface Props {
4
+ value?: string[]
5
+ }
6
+
7
+ const {
8
+ value
9
+ } = Astro.props
10
+
11
+ ---
12
+
13
+ {value && <meta name="keywords" content={value.join(", ")} />}
@@ -0,0 +1,31 @@
1
+ ---
2
+
3
+ interface Props {
4
+ title?: string
5
+ description?: string
6
+ image?: string
7
+ url?: string
8
+ type?: string
9
+ siteName?: string
10
+ locale?: string
11
+ }
12
+
13
+ const {
14
+ title,
15
+ description,
16
+ image,
17
+ url,
18
+ type = "website",
19
+ siteName,
20
+ locale
21
+ } = Astro.props
22
+
23
+ ---
24
+
25
+ {title && <meta property="og:title" content={title} />}
26
+ {description && <meta property="og:description" content={description} />}
27
+ {image && <meta property="og:image" content={image} />}
28
+ {url && <meta property="og:url" content={url} />}
29
+ {type && <meta property="og:type" content={type} />}
30
+ {siteName && <meta property="og:site_name" content={siteName} />}
31
+ {locale && <meta property="og:locale" content={locale} />}
@@ -0,0 +1,26 @@
1
+ ---
2
+
3
+ interface Props {
4
+ index: boolean
5
+ follow: boolean
6
+ noArchive?: boolean
7
+ noSnippet?: boolean
8
+ }
9
+
10
+ const {
11
+ index,
12
+ follow,
13
+ noArchive,
14
+ noSnippet
15
+ } = Astro.props
16
+
17
+ const directives = [
18
+ index ? "index" : "noindex",
19
+ follow ? "follow" : "nofollow",
20
+ noArchive && "noarchive",
21
+ noSnippet && "nosnippet",
22
+ ].filter(Boolean).join(", ")
23
+
24
+ ---
25
+
26
+ <meta name="robots" content={directives} />
@@ -0,0 +1,13 @@
1
+ ---
2
+
3
+ interface Props {
4
+ value: string
5
+ }
6
+
7
+ const {
8
+ value
9
+ } = Astro.props
10
+
11
+ ---
12
+
13
+ <title>{value}</title>
@@ -0,0 +1,28 @@
1
+ ---
2
+
3
+ interface Props {
4
+ title?: string
5
+ description?: string
6
+ image?: string
7
+ card?: "summary" | "summary_large_image"
8
+ site?: string
9
+ creator?: string
10
+ }
11
+
12
+ const {
13
+ title,
14
+ description,
15
+ image,
16
+ card = "summary_large_image",
17
+ site,
18
+ creator
19
+ } = Astro.props
20
+
21
+ ---
22
+
23
+ {card && <meta name="twitter:card" content={card} />}
24
+ {title && <meta name="twitter:title" content={title} />}
25
+ {description && <meta name="twitter:description" content={description} />}
26
+ {image && <meta name="twitter:image" content={image} />}
27
+ {site && <meta name="twitter:site" content={site} />}
28
+ {creator && <meta name="twitter:creator" content={creator} />}