@mannisto/astro-metadata 1.0.0-alpha.4 → 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/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@mannisto/astro-metadata",
3
- "version": "1.0.0-alpha.4",
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
- "files": ["src", "index.ts"],
15
+ "keywords": [
16
+ "astro-component",
17
+ "astro",
18
+ "seo",
19
+ "metadata"
20
+ ],
16
21
  "exports": "./index.ts",
22
+ "files": [
23
+ "src",
24
+ "index.ts"
25
+ ],
26
+ "scripts": {
27
+ "init": "bash scripts/init.sh",
28
+ "lint": "biome lint ./src ./index.ts",
29
+ "format": "prettier --write .",
30
+ "check": "biome lint ./src ./index.ts && prettier --check .",
31
+ "test:unit": "vitest run",
32
+ "test:e2e": "playwright test",
33
+ "test:all": "pnpm run test:unit && pnpm run test:e2e"
34
+ },
17
35
  "peerDependencies": {
18
36
  "astro": "^4.3.0 || ^5.0.0"
19
37
  },
20
38
  "devDependencies": {
39
+ "@biomejs/biome": "^2.4.3",
40
+ "@playwright/test": "^1.58.2",
41
+ "@types/node": "^25.3.0",
21
42
  "astro": "^5.17.2",
22
- "typescript": "^5.9.3"
23
- },
24
- "keywords": ["astro-component", "astro", "head", "seo", "meta"]
25
- }
43
+ "prettier": "^3.8.1",
44
+ "prettier-plugin-astro": "^0.14.1",
45
+ "typescript": "^5.9.3",
46
+ "vitest": "^4.0.18"
47
+ }
48
+ }
@@ -1,15 +1,11 @@
1
1
  ---
2
-
3
2
  export type Props = {
4
3
  value?: string
5
4
  }
6
5
 
7
- const {
8
- value
9
- } = Astro.props
6
+ const { value } = Astro.props
10
7
 
11
8
  const canonical = value ?? Astro.url.href
12
-
13
9
  ---
14
10
 
15
- <link rel="canonical" href={canonical} />
11
+ <link rel="canonical" href={canonical} />
@@ -1,13 +1,9 @@
1
1
  ---
2
-
3
2
  export type Props = {
4
3
  value?: string
5
4
  }
6
5
 
7
- const {
8
- value
9
- } = Astro.props
10
-
6
+ const { value } = Astro.props
11
7
  ---
12
8
 
13
- {value && <meta name="description" content={value} />}
9
+ {value && <meta name="description" content={value} />}
@@ -1,21 +1,18 @@
1
1
  ---
2
-
3
2
  export type FaviconFile = {
4
- path : string
5
- size? : number
6
- theme? : "light" | "dark"
7
- apple? : boolean
3
+ path: string
4
+ size?: number
5
+ theme?: "light" | "dark"
6
+ apple?: boolean
8
7
  }
9
8
 
10
9
  export type Props = {
11
- icons : FaviconFile[]
12
- manifest? : string
10
+ icons: FaviconFile[]
11
+ manifest?: string
12
+ sort?: boolean
13
13
  }
14
14
 
15
- const {
16
- icons,
17
- manifest
18
- } = Astro.props
15
+ const { icons, manifest, sort = true } = Astro.props
19
16
 
20
17
  /**
21
18
  * Detects the MIME type of a favicon file from its extension.
@@ -24,12 +21,11 @@ const {
24
21
  * @returns The MIME type of the file.
25
22
  */
26
23
  function getMimeType(path: string): string {
27
- if (path.endsWith(".svg")) return "image/svg+xml"
28
- if (path.endsWith(".png")) return "image/png"
24
+ if (path.endsWith(".svg")) return "image/svg+xml"
25
+ if (path.endsWith(".png")) return "image/png"
29
26
  if (path.endsWith(".webp")) return "image/webp"
30
- if (path.endsWith(".jpg") ||
31
- path.endsWith(".jpeg")) return "image/jpeg"
32
- if (path.endsWith(".ico")) return "image/x-icon"
27
+ if (path.endsWith(".jpg") || path.endsWith(".jpeg")) return "image/jpeg"
28
+ if (path.endsWith(".ico")) return "image/x-icon"
33
29
  return "image/x-icon"
34
30
  }
35
31
 
@@ -41,40 +37,52 @@ function getMimeType(path: string): string {
41
37
  */
42
38
  function getMedia(theme?: "light" | "dark"): string | undefined {
43
39
  if (theme === "light") return "(prefers-color-scheme: light)"
44
- if (theme === "dark") return "(prefers-color-scheme: dark)"
40
+ if (theme === "dark") return "(prefers-color-scheme: dark)"
45
41
  return undefined
46
42
  }
47
43
 
48
- const prepared = icons.map((file) => ({
49
- path : file.path,
50
- size : file.size ? `${file.size}x${file.size}` : undefined,
51
- type : getMimeType(file.path),
52
- media : getMedia(file.theme),
53
- apple : file.apple ?? false,
54
- }))
44
+ /**
45
+ * Returns a sort priority for a given favicon file.
46
+ * Lower number = rendered first.
47
+ *
48
+ * Order: ico → png → svg → apple → themed variants
49
+ */
50
+ function getSortOrder(file: FaviconFile): number {
51
+ if (file.apple) return 4
52
+ if (file.theme) return 5
53
+ if (file.path.endsWith(".ico")) return 0
54
+ if (
55
+ file.path.endsWith(".png") ||
56
+ file.path.endsWith(".webp") ||
57
+ file.path.endsWith(".jpg") ||
58
+ file.path.endsWith(".jpeg")
59
+ )
60
+ return 1
61
+ if (file.path.endsWith(".svg")) return 2
62
+ return 3
63
+ }
55
64
 
65
+ const sorted = sort ? [...icons].sort((a, b) => getSortOrder(a) - getSortOrder(b)) : icons
66
+
67
+ const prepared = sorted.map((file) => ({
68
+ path: file.path,
69
+ size: file.size ? `${file.size}x${file.size}` : undefined,
70
+ type: getMimeType(file.path),
71
+ media: getMedia(file.theme),
72
+ apple: file.apple ?? false,
73
+ }))
56
74
  ---
57
75
 
58
76
  {manifest && <link rel="manifest" href={manifest} />}
59
77
 
60
- {prepared.map((icon) => {
61
- if (icon.apple) {
78
+ {
79
+ prepared.map((icon) => {
80
+ if (icon.apple) {
81
+ return <link rel="apple-touch-icon" href={icon.path} sizes={icon.size} />
82
+ }
83
+
62
84
  return (
63
- <link
64
- rel="apple-touch-icon"
65
- href={icon.path}
66
- sizes={icon.size}
67
- />
85
+ <link rel="icon" type={icon.type} href={icon.path} sizes={icon.size} media={icon.media} />
68
86
  )
69
- }
70
-
71
- return (
72
- <link
73
- rel="icon"
74
- type={icon.type}
75
- href={icon.path}
76
- sizes={icon.size}
77
- media={icon.media}
78
- />
79
- )
80
- })}
87
+ })
88
+ }
@@ -1,30 +1,30 @@
1
1
  ---
2
2
  import type { ComponentProps } from "astro/types"
3
- import Title from "./Title.astro"
4
- import Description from "./Description.astro"
5
- import Canonical from "./Canonical.astro"
6
- import Keywords from "./Keywords.astro"
7
- import Robots from "./Robots.astro"
8
- import OpenGraph from "./OpenGraph.astro"
9
- import Twitter from "./Twitter.astro"
10
- import Favicon from "./Favicon.astro"
11
- import Schema from "./Schema.astro"
3
+ import Title from "./Title.astro"
4
+ import Description from "./Description.astro"
5
+ import Canonical from "./Canonical.astro"
6
+ import Keywords from "./Keywords.astro"
7
+ import Robots from "./Robots.astro"
8
+ import OpenGraph from "./OpenGraph.astro"
9
+ import Twitter from "./Twitter.astro"
10
+ import Favicon from "./Favicon.astro"
11
+ import Schema from "./Schema.astro"
12
12
  import LanguageAlternates from "./LanguageAlternates.astro"
13
13
 
14
14
  export type Props = {
15
- title : string
16
- titleTemplate? : `${string}%s${string}`
17
- description? : string
18
- canonical? : string
19
- keywords? : string[]
20
- charset? : string
21
- viewport? : string
22
- robots? : ComponentProps<typeof Robots>
23
- openGraph? : ComponentProps<typeof OpenGraph>
24
- twitter? : ComponentProps<typeof Twitter>
25
- favicon? : ComponentProps<typeof Favicon>
26
- schema? : ComponentProps<typeof Schema>
27
- languageAlternates? : ComponentProps<typeof LanguageAlternates>["alternates"]
15
+ title: string
16
+ titleTemplate?: `${string}%s${string}`
17
+ description?: string
18
+ canonical?: string
19
+ keywords?: string[]
20
+ charset?: string
21
+ viewport?: string
22
+ robots?: ComponentProps<typeof Robots>
23
+ openGraph?: ComponentProps<typeof OpenGraph>
24
+ twitter?: ComponentProps<typeof Twitter>
25
+ favicon?: ComponentProps<typeof Favicon>
26
+ schema?: ComponentProps<typeof Schema>
27
+ languageAlternates?: ComponentProps<typeof LanguageAlternates>["alternates"]
28
28
  }
29
29
 
30
30
  const {
@@ -33,7 +33,7 @@ const {
33
33
  description,
34
34
  canonical,
35
35
  keywords,
36
- charset = "UTF-8",
36
+ charset = "UTF-8",
37
37
  viewport = "width=device-width, initial-scale=1.0",
38
38
  robots,
39
39
  openGraph,
@@ -57,46 +57,47 @@ const {
57
57
  <Robots
58
58
  index={robots?.index}
59
59
  follow={robots?.follow}
60
- noArchive={robots?.noArchive}
61
- noSnippet={robots?.noSnippet}
60
+ archive={robots?.archive}
61
+ snippet={robots?.snippet}
62
62
  extra={robots?.extra}
63
63
  />
64
64
 
65
- {languageAlternates && (
66
- <LanguageAlternates alternates={languageAlternates} />
67
- )}
65
+ {languageAlternates && <LanguageAlternates alternates={languageAlternates} />}
68
66
 
69
- {openGraph && (
70
- <OpenGraph
71
- title={openGraph.title ?? title}
72
- description={openGraph.description ?? description}
73
- image={openGraph.image}
74
- url={openGraph.url ?? canonical}
75
- type={openGraph.type}
76
- siteName={openGraph.siteName}
77
- locale={openGraph.locale}
78
- />
79
- )}
67
+ {
68
+ openGraph && (
69
+ <OpenGraph
70
+ title={openGraph.title ?? title}
71
+ description={openGraph.description ?? description}
72
+ url={openGraph.url ?? canonical}
73
+ type={openGraph.type}
74
+ siteName={openGraph.siteName}
75
+ locale={openGraph.locale}
76
+ localeAlternate={openGraph.localeAlternate}
77
+ image={openGraph.image}
78
+ video={openGraph.video}
79
+ audio={openGraph.audio}
80
+ />
81
+ )
82
+ }
80
83
 
81
- {twitter && (
82
- <Twitter
83
- title={twitter.title ?? title}
84
- description={twitter.description ?? description}
85
- image={twitter.image}
86
- card={twitter.card}
87
- site={twitter.site}
88
- creator={twitter.creator}
89
- />
90
- )}
84
+ {
85
+ twitter && (
86
+ <Twitter
87
+ title={twitter.title ?? title}
88
+ description={twitter.description ?? description}
89
+ image={twitter.image}
90
+ card={twitter.card}
91
+ site={twitter.site}
92
+ creator={twitter.creator}
93
+ url={twitter.url ?? canonical}
94
+ />
95
+ )
96
+ }
91
97
 
92
- {favicon && (
93
- <Favicon
94
- icons={favicon.icons}
95
- manifest={favicon.manifest}
96
- />
97
- )}
98
+ {favicon && <Favicon icons={favicon.icons} manifest={favicon.manifest} sort={favicon.sort} />}
98
99
 
99
100
  {schema && <Schema schema={schema.schema} />}
100
101
 
101
102
  <slot />
102
- </head>
103
+ </head>
@@ -1,13 +1,9 @@
1
1
  ---
2
-
3
2
  export type Props = {
4
3
  value?: string[]
5
4
  }
6
5
 
7
- const {
8
- value = []
9
- } = Astro.props
10
-
6
+ const { value = [] } = Astro.props
11
7
  ---
12
8
 
13
- {value.length > 0 && <meta name="keywords" content={value.join(", ")} />}
9
+ {value.length > 0 && <meta name="keywords" content={value.join(", ")} />}
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  export type LanguageAlternate = {
3
- href : string
4
- hreflang : string
3
+ href: string
4
+ hreflang: string
5
5
  }
6
6
 
7
7
  export type Props = {
@@ -11,10 +11,8 @@ export type Props = {
11
11
  const { alternates } = Astro.props
12
12
  ---
13
13
 
14
- {alternates.map((alternate) => (
15
- <link
16
- rel="alternate"
17
- href={alternate.href}
18
- hreflang={alternate.hreflang}
19
- />
20
- ))}
14
+ {
15
+ alternates.map((alternate) => (
16
+ <link rel="alternate" href={alternate.href} hreflang={alternate.hreflang} />
17
+ ))
18
+ }
@@ -1,39 +1,75 @@
1
1
  ---
2
+ export type OpenGraphImage = {
3
+ url: string
4
+ secureUrl?: string
5
+ type?: string
6
+ alt?: string
7
+ width?: number
8
+ height?: number
9
+ }
10
+
11
+ export type OpenGraphVideo = {
12
+ url: string
13
+ secureUrl?: string
14
+ type?: string
15
+ width?: number
16
+ height?: number
17
+ }
18
+
19
+ export type OpenGraphAudio = {
20
+ url: string
21
+ secureUrl?: string
22
+ type?: string
23
+ }
2
24
 
3
25
  export type Props = {
4
- title? : string
5
- description? : string
6
- url? : string
7
- type? : string
8
- siteName? : string
9
- locale? : string
10
- image? : {
11
- url : string
12
- alt? : string
13
- width? : number
14
- height? : number
15
- }
26
+ title?: string
27
+ description?: string
28
+ url?: string
29
+ type?: string
30
+ siteName?: string
31
+ locale?: string
32
+ localeAlternate?: string[]
33
+ image?: OpenGraphImage
34
+ video?: OpenGraphVideo
35
+ audio?: OpenGraphAudio
16
36
  }
17
37
 
18
38
  const {
19
39
  title,
20
40
  description,
21
- image,
22
41
  url,
23
42
  type = "website",
24
43
  siteName,
25
44
  locale,
45
+ localeAlternate,
46
+ image,
47
+ video,
48
+ audio,
26
49
  } = Astro.props
27
-
28
50
  ---
29
51
 
30
- {title && <meta property="og:title" content={title} />}
31
- {description && <meta property="og:description" content={description} />}
32
- {image?.url && <meta property="og:image" content={image.url} />}
33
- {image?.alt && <meta property="og:image:alt" content={image.alt} />}
34
- {image?.width && <meta property="og:image:width" content={String(image.width)} />}
52
+ {title && <meta property="og:title" content={title} />}
53
+ {description && <meta property="og:description" content={description} />}
54
+ {url && <meta property="og:url" content={url} />}
55
+ {type && <meta property="og:type" content={type} />}
56
+ {siteName && <meta property="og:site_name" content={siteName} />}
57
+ {locale && <meta property="og:locale" content={locale} />}
58
+ {localeAlternate?.map((loc: string) => <meta property="og:locale:alternate" content={loc} />)}
59
+
60
+ {image?.url && <meta property="og:image" content={image.url} />}
61
+ {image?.secureUrl && <meta property="og:image:secure_url" content={image.secureUrl} />}
62
+ {image?.type && <meta property="og:image:type" content={image.type} />}
63
+ {image?.alt && <meta property="og:image:alt" content={image.alt} />}
64
+ {image?.width && <meta property="og:image:width" content={String(image.width)} />}
35
65
  {image?.height && <meta property="og:image:height" content={String(image.height)} />}
36
- {url && <meta property="og:url" content={url} />}
37
- {type && <meta property="og:type" content={type} />}
38
- {siteName && <meta property="og:site_name" content={siteName} />}
39
- {locale && <meta property="og:locale" content={locale} />}
66
+
67
+ {video?.url && <meta property="og:video" content={video.url} />}
68
+ {video?.secureUrl && <meta property="og:video:secure_url" content={video.secureUrl} />}
69
+ {video?.type && <meta property="og:video:type" content={video.type} />}
70
+ {video?.width && <meta property="og:video:width" content={String(video.width)} />}
71
+ {video?.height && <meta property="og:video:height" content={String(video.height)} />}
72
+
73
+ {audio?.url && <meta property="og:audio" content={audio.url} />}
74
+ {audio?.secureUrl && <meta property="og:audio:secure_url" content={audio.secureUrl} />}
75
+ {audio?.type && <meta property="og:audio:type" content={audio.type} />}
@@ -1,29 +1,25 @@
1
1
  ---
2
2
  export type Props = {
3
- index? : boolean
4
- follow? : boolean
5
- noArchive? : boolean
6
- noSnippet? : boolean
7
- extra? : string
3
+ index?: boolean
4
+ follow?: boolean
5
+ archive?: boolean
6
+ snippet?: boolean
7
+ extra?: string
8
8
  }
9
9
 
10
- const {
11
- index = true,
12
- follow = true,
13
- noArchive,
14
- noSnippet,
15
- extra,
16
- } = Astro.props
10
+ const { index = true, follow = true, archive = true, snippet = true, extra } = Astro.props
17
11
 
18
12
  // Combine the directives into a comma separated string,
19
13
  // for example: "index, follow, noarchive, nosnippet"
20
14
  const directives = [
21
15
  index ? "index" : "noindex",
22
16
  follow ? "follow" : "nofollow",
23
- noArchive && "noarchive",
24
- noSnippet && "nosnippet",
17
+ !archive ? "noarchive" : null,
18
+ !snippet ? "nosnippet" : null,
25
19
  extra,
26
- ].filter(Boolean).join(", ")
20
+ ]
21
+ .filter(Boolean)
22
+ .join(", ")
27
23
  ---
28
24
 
29
- <meta name="robots" content={directives} />
25
+ <meta name="robots" content={directives} />
@@ -1,13 +1,9 @@
1
1
  ---
2
-
3
2
  export type Props = {
4
3
  schema: Record<string, unknown>
5
4
  }
6
5
 
7
- const {
8
- schema
9
- } = Astro.props
10
-
6
+ const { schema } = Astro.props
11
7
  ---
12
8
 
13
- <script is:inline type="application/ld+json" set:html={JSON.stringify(schema)} />
9
+ <script is:inline type="application/ld+json" set:html={JSON.stringify(schema)} />
@@ -1,15 +1,10 @@
1
1
  ---
2
-
3
2
  export type Props = {
4
- value : string
5
- template? : `${string}%s${string}`
3
+ value: string
4
+ template?: `${string}%s${string}`
6
5
  }
7
6
 
8
- const {
9
- value,
10
- template = "%s"
11
- } = Astro.props
12
-
7
+ const { value, template = "%s" } = Astro.props
13
8
  ---
14
9
 
15
- <title>{template.replace("%s", value)}</title>
10
+ <title>{template.replace("%s", value)}</title>
@@ -1,32 +1,25 @@
1
1
  ---
2
-
3
2
  export type Props = {
4
- title? : string
5
- description? : string
6
- card? : "summary" | "summary_large_image"
7
- site? : string
8
- creator? : string
9
- image? : {
10
- url : string
11
- alt? : string
3
+ title?: string
4
+ description?: string
5
+ url?: string
6
+ card?: "summary" | "summary_large_image" | "player" | "app"
7
+ site?: string
8
+ creator?: string
9
+ image?: {
10
+ url: string
11
+ alt?: string
12
12
  }
13
13
  }
14
14
 
15
- const {
16
- title,
17
- description,
18
- image,
19
- card = "summary_large_image",
20
- site,
21
- creator,
22
- } = Astro.props
23
-
15
+ const { title, description, url, card = "summary_large_image", site, creator, image } = Astro.props
24
16
  ---
25
17
 
26
- {card && <meta name="twitter:card" content={card} />}
27
- {title && <meta name="twitter:title" content={title} />}
18
+ {card && <meta name="twitter:card" content={card} />}
19
+ {title && <meta name="twitter:title" content={title} />}
28
20
  {description && <meta name="twitter:description" content={description} />}
29
- {image?.url && <meta name="twitter:image" content={image.url} />}
30
- {image?.alt && <meta name="twitter:image:alt" content={image.alt} />}
31
- {site && <meta name="twitter:site" content={site} />}
32
- {creator && <meta name="twitter:creator" content={creator} />}
21
+ {url && <meta name="twitter:url" content={url} />}
22
+ {image?.url && <meta name="twitter:image" content={image.url} />}
23
+ {image?.alt && <meta name="twitter:image:alt" content={image.alt} />}
24
+ {site && <meta name="twitter:site" content={site} />}
25
+ {creator && <meta name="twitter:creator" content={creator} />}