@mannisto/astro-metadata 1.0.2 → 2.0.0-alpha.2

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.
Files changed (102) hide show
  1. package/README.md +427 -82
  2. package/package.json +25 -14
  3. package/src/assets/icons/LICENSE.md +35 -0
  4. package/src/assets/icons/binoculars.svg +1 -0
  5. package/src/assets/icons/caret-right.svg +1 -0
  6. package/src/assets/icons/check.svg +1 -0
  7. package/src/assets/icons/copy.svg +1 -0
  8. package/src/assets/icons/seal-check-duotone.svg +1 -0
  9. package/src/assets/icons/seal-question-duotone.svg +1 -0
  10. package/src/assets/icons/warning-circle-duotone.svg +1 -0
  11. package/src/assets/icons/warning-duotone.svg +1 -0
  12. package/src/assets/icons/x.svg +1 -0
  13. package/src/components/Metadata.astro +145 -0
  14. package/src/components/index.ts +2 -0
  15. package/src/integration.d.ts +112 -0
  16. package/src/integration.js +508 -0
  17. package/src/lib/checks.d.ts +70 -0
  18. package/src/lib/checks.js +193 -0
  19. package/src/lib/config.ts +111 -0
  20. package/src/lib/favicon-files.d.ts +29 -0
  21. package/src/lib/favicon-files.js +103 -0
  22. package/src/lib/favicon-image.ts +124 -0
  23. package/src/lib/favicon.ts +167 -0
  24. package/src/lib/head.d.ts +25 -0
  25. package/src/lib/head.js +141 -0
  26. package/src/lib/manifest.ts +126 -0
  27. package/src/lib/rules.d.ts +73 -0
  28. package/src/lib/rules.js +929 -0
  29. package/src/lib/schema.ts +208 -0
  30. package/src/lib/site.ts +72 -0
  31. package/src/lib/url.ts +79 -0
  32. package/src/routes/favicon.ts +14 -0
  33. package/src/routes/manifest.ts +18 -0
  34. package/src/runtime.ts +16 -0
  35. package/src/toolbar/app.ts +72 -0
  36. package/src/toolbar/assets.ts +81 -0
  37. package/src/toolbar/data.ts +282 -0
  38. package/src/toolbar/define.ts +7 -0
  39. package/src/toolbar/env.d.ts +10 -0
  40. package/src/toolbar/highlight.ts +83 -0
  41. package/src/toolbar/styles/shared.css +18 -0
  42. package/src/toolbar/styles/tokens.css +182 -0
  43. package/src/toolbar/toolbar-badge.ts +78 -0
  44. package/src/toolbar/ui/alert/alert.css +126 -0
  45. package/src/toolbar/ui/alert/alert.ts +102 -0
  46. package/src/toolbar/ui/badge/badge.css +41 -0
  47. package/src/toolbar/ui/badge/badge.ts +24 -0
  48. package/src/toolbar/ui/code-block/code-block.css +90 -0
  49. package/src/toolbar/ui/code-block/code-block.ts +68 -0
  50. package/src/toolbar/ui/color-swatch/color-swatch.css +15 -0
  51. package/src/toolbar/ui/color-swatch/color-swatch.ts +31 -0
  52. package/src/toolbar/ui/icon/icon.css +11 -0
  53. package/src/toolbar/ui/icon/icon.ts +46 -0
  54. package/src/toolbar/ui/image/image.css +18 -0
  55. package/src/toolbar/ui/image/image.ts +58 -0
  56. package/src/toolbar/ui/inline-code/inline-code.css +16 -0
  57. package/src/toolbar/ui/inline-code/inline-code.ts +41 -0
  58. package/src/toolbar/ui/section/section.css +88 -0
  59. package/src/toolbar/ui/section/section.ts +78 -0
  60. package/src/toolbar/ui/tab/tab.css +7 -0
  61. package/src/toolbar/ui/tab/tab.ts +47 -0
  62. package/src/toolbar/ui/tab-group/tab-group.css +42 -0
  63. package/src/toolbar/ui/tab-group/tab-group.ts +101 -0
  64. package/src/toolbar/ui/table/table.css +124 -0
  65. package/src/toolbar/ui/table/table.ts +155 -0
  66. package/src/toolbar/views/code-blocks.ts +35 -0
  67. package/src/toolbar/views/metadata-panel.css +105 -0
  68. package/src/toolbar/views/metadata-panel.ts +125 -0
  69. package/src/toolbar/views/overview-view.css +27 -0
  70. package/src/toolbar/views/overview-view.ts +401 -0
  71. package/src/toolbar/views/previews/discord.css +70 -0
  72. package/src/toolbar/views/previews/discord.ts +60 -0
  73. package/src/toolbar/views/previews/facebook.css +65 -0
  74. package/src/toolbar/views/previews/facebook.ts +36 -0
  75. package/src/toolbar/views/previews/google.css +75 -0
  76. package/src/toolbar/views/previews/google.ts +36 -0
  77. package/src/toolbar/views/previews/linkedin.css +58 -0
  78. package/src/toolbar/views/previews/linkedin.ts +34 -0
  79. package/src/toolbar/views/previews/page.ts +129 -0
  80. package/src/toolbar/views/previews/platform.ts +14 -0
  81. package/src/toolbar/views/previews/previews-view.css +37 -0
  82. package/src/toolbar/views/previews/previews-view.ts +52 -0
  83. package/src/toolbar/views/previews/slack.css +61 -0
  84. package/src/toolbar/views/previews/slack.ts +47 -0
  85. package/src/toolbar/views/previews/twitter.css +89 -0
  86. package/src/toolbar/views/previews/twitter.ts +57 -0
  87. package/src/toolbar/views/previews/whatsapp.css +88 -0
  88. package/src/toolbar/views/previews/whatsapp.ts +65 -0
  89. package/src/toolbar/views/raw-view.ts +41 -0
  90. package/index.ts +0 -31
  91. package/src/components/Canonical.astro +0 -11
  92. package/src/components/Description.astro +0 -9
  93. package/src/components/Favicon.astro +0 -96
  94. package/src/components/Head.astro +0 -94
  95. package/src/components/Keywords.astro +0 -9
  96. package/src/components/LanguageAlternates.astro +0 -18
  97. package/src/components/OpenGraph.astro +0 -75
  98. package/src/components/Robots.astro +0 -25
  99. package/src/components/Schema.astro +0 -18
  100. package/src/components/Title.astro +0 -13
  101. package/src/components/Twitter.astro +0 -27
  102. package/src/lib/metadata.ts +0 -77
@@ -0,0 +1,61 @@
1
+ /* Slack: a link unfurl under a message */
2
+ .slack {
3
+ width: 100%;
4
+ max-width: 520px;
5
+ padding: 16px;
6
+ font-family: Lato, "Helvetica Neue", Helvetica, "Segoe UI", sans-serif;
7
+ border-radius: 8px;
8
+ background: #1a1d21;
9
+ }
10
+
11
+ .slack .unfurl {
12
+ display: flex;
13
+ gap: 12px;
14
+ padding-left: 12px;
15
+ border-left: 4px solid #35373b;
16
+
17
+ .body {
18
+ display: grid;
19
+ gap: 2px;
20
+ min-width: 0;
21
+ font-size: 15px;
22
+ line-height: 22px;
23
+ color: #d1d2d3;
24
+ }
25
+
26
+ .site {
27
+ display: flex;
28
+ align-items: center;
29
+ gap: 6px;
30
+ font-weight: 700;
31
+ }
32
+
33
+ .favicon {
34
+ width: 16px;
35
+ height: 16px;
36
+ border-radius: 2px;
37
+ object-fit: contain;
38
+ }
39
+
40
+ .title {
41
+ font-weight: 700;
42
+ color: #1d9bd1;
43
+ }
44
+
45
+ /* A large image: under the text, at most 360 px wide */
46
+ .image {
47
+ width: 100%;
48
+ max-width: 360px;
49
+ aspect-ratio: 1.91 / 1;
50
+ margin-top: 8px;
51
+ border-radius: 8px;
52
+ }
53
+
54
+ /* A small image: on the right */
55
+ .thumbnail {
56
+ flex: none;
57
+ width: 72px;
58
+ height: 72px;
59
+ border-radius: 4px;
60
+ }
61
+ }
@@ -0,0 +1,47 @@
1
+ import { html, nothing, unsafeCSS } from "lit"
2
+
3
+ import "../../ui/image/image.ts"
4
+
5
+ import type { Platform } from "./platform.ts"
6
+ import styles from "./slack.css?inline"
7
+
8
+ /**
9
+ * A link unfurl in a message, in dark mode.
10
+ *
11
+ * - Uses `og:site_name` with the favicon, `og:title`, `og:description` and `og:image`.
12
+ * - `twitter:card` with `summary_large_image` shows a large image under the text. Else the image is small, on the right.
13
+ * - An image smaller than 60 × 60 px does not show.
14
+ */
15
+ export const slack: Platform = {
16
+ name: "Slack",
17
+ styles: unsafeCSS(styles),
18
+ render(page) {
19
+ const large = page.twitter.card === "summary_large_image"
20
+ const image = page.og.image
21
+
22
+ return html`
23
+ <div class="slack">
24
+ <div class="unfurl">
25
+ <div class="body">
26
+ <div class="site">
27
+ ${page.favicon ? html`<img class="favicon" src=${page.favicon} alt="" />` : nothing}
28
+ ${page.og.siteName ?? page.url.host}
29
+ </div>
30
+ <div class="title">${page.og.title ?? page.title}</div>
31
+ <div class="text">${page.og.description ?? page.description}</div>
32
+ ${
33
+ large && image
34
+ ? html`<astro-metadata-image class="image" .src=${image}></astro-metadata-image>`
35
+ : nothing
36
+ }
37
+ </div>
38
+ ${
39
+ !large && image
40
+ ? html`<astro-metadata-image class="thumbnail" .src=${image}></astro-metadata-image>`
41
+ : nothing
42
+ }
43
+ </div>
44
+ </div>
45
+ `
46
+ },
47
+ }
@@ -0,0 +1,89 @@
1
+ /* Twitter (X): a link card in a post, 506 px wide */
2
+ .twitter {
3
+ width: 100%;
4
+ max-width: 506px;
5
+ padding: 12px;
6
+ font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
7
+ border-radius: 8px;
8
+ background: #000000;
9
+
10
+ .from {
11
+ margin-top: 4px;
12
+ font-size: 13px;
13
+ line-height: 16px;
14
+ color: #71767b;
15
+ }
16
+ }
17
+
18
+ .twitter .large {
19
+ position: relative;
20
+ overflow: hidden;
21
+ border: 1px solid #2f3336;
22
+ border-radius: 16px;
23
+
24
+ .image {
25
+ width: 100%;
26
+ aspect-ratio: 1.91 / 1;
27
+ }
28
+
29
+ /* The title on the image: one line */
30
+ .overlay {
31
+ position: absolute;
32
+ bottom: 12px;
33
+ left: 12px;
34
+ max-width: calc(100% - 24px);
35
+ overflow: hidden;
36
+ padding: 0 4px;
37
+ font-size: 13px;
38
+ line-height: 20px;
39
+ color: #ffffff;
40
+ text-overflow: ellipsis;
41
+ white-space: nowrap;
42
+ border-radius: 4px;
43
+ background: rgba(0, 0, 0, 0.77);
44
+ }
45
+ }
46
+
47
+ .twitter .small {
48
+ display: flex;
49
+ overflow: hidden;
50
+ border: 1px solid #2f3336;
51
+ border-radius: 16px;
52
+
53
+ .image {
54
+ flex: none;
55
+ width: 130px;
56
+ height: 130px;
57
+ border-right: 1px solid #2f3336;
58
+ }
59
+
60
+ .body {
61
+ display: flex;
62
+ flex-direction: column;
63
+ justify-content: center;
64
+ gap: 2px;
65
+ min-width: 0;
66
+ padding: 12px;
67
+ font-size: 15px;
68
+ line-height: 20px;
69
+ }
70
+
71
+ .host {
72
+ color: #71767b;
73
+ }
74
+
75
+ .title {
76
+ overflow: hidden;
77
+ color: #e7e9ea;
78
+ text-overflow: ellipsis;
79
+ white-space: nowrap;
80
+ }
81
+
82
+ .text {
83
+ display: -webkit-box;
84
+ overflow: hidden;
85
+ color: #71767b;
86
+ -webkit-box-orient: vertical;
87
+ -webkit-line-clamp: 2;
88
+ }
89
+ }
@@ -0,0 +1,57 @@
1
+ import { html, unsafeCSS } from "lit"
2
+
3
+ import "../../ui/image/image.ts"
4
+
5
+ import { withCode } from "../../ui/inline-code/inline-code.ts"
6
+ import type { Platform } from "./platform.ts"
7
+ import styles from "./twitter.css?inline"
8
+
9
+ /**
10
+ * A link card in a post in dark mode ("Lights out").
11
+ *
12
+ * - Twitter (X) shows a card only with `twitter:card`.
13
+ * - `summary_large_image` shows a large image, `summary` a small square image.
14
+ * - Uses `twitter:title`, `twitter:description` and `twitter:image`, or else the Open Graph values.
15
+ * - The large card shows only the title, on one line on the image, and the domain under it.
16
+ */
17
+ export const twitter: Platform = {
18
+ name: "Twitter (X)",
19
+ styles: unsafeCSS(styles),
20
+ render(page) {
21
+ const card = page.twitter.card
22
+ if (!card) {
23
+ return html`<p class="notice">
24
+ ${withCode("Twitter (X) shows no card: the page has no `twitter:card`.")}
25
+ </p>`
26
+ }
27
+
28
+ const title = page.twitter.title ?? page.og.title ?? page.title
29
+ const text = page.twitter.description ?? page.og.description ?? page.description
30
+ const image = page.twitter.image ?? page.og.image
31
+
32
+ if (card === "summary_large_image") {
33
+ return html`
34
+ <div class="twitter">
35
+ <div class="large">
36
+ <astro-metadata-image class="image" .src=${image}></astro-metadata-image>
37
+ <span class="overlay">${title}</span>
38
+ </div>
39
+ <div class="from">From ${page.url.host}</div>
40
+ </div>
41
+ `
42
+ }
43
+
44
+ return html`
45
+ <div class="twitter">
46
+ <div class="small">
47
+ <astro-metadata-image class="image" .src=${image}></astro-metadata-image>
48
+ <div class="body">
49
+ <div class="host">${page.url.host}</div>
50
+ <div class="title">${title}</div>
51
+ <div class="text">${text}</div>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ `
56
+ },
57
+ }
@@ -0,0 +1,88 @@
1
+ /* WhatsApp: a sent message with the link preview, at most 330 px wide */
2
+ .whatsapp {
3
+ width: 100%;
4
+ max-width: 330px;
5
+ font-family: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
6
+ }
7
+
8
+ .whatsapp .bubble {
9
+ width: 100%;
10
+ padding: 4px;
11
+ border-radius: 8px;
12
+ background: #005c4b;
13
+ }
14
+
15
+ .whatsapp .preview {
16
+ overflow: hidden;
17
+ border-radius: 6px;
18
+ background: #025144;
19
+
20
+ &.large .image {
21
+ width: 100%;
22
+ aspect-ratio: 1.91 / 1;
23
+ }
24
+
25
+ .row {
26
+ display: flex;
27
+ }
28
+
29
+ /* A small image: a square on the left of the text */
30
+ &.small .image {
31
+ flex: none;
32
+ width: 82px;
33
+ height: 82px;
34
+ }
35
+
36
+ .body {
37
+ min-width: 0;
38
+ padding: 6px 10px 8px;
39
+ }
40
+
41
+ .title {
42
+ display: -webkit-box;
43
+ overflow: hidden;
44
+ font-size: 14px;
45
+ font-weight: 600;
46
+ line-height: 19px;
47
+ color: #e9edef;
48
+ -webkit-box-orient: vertical;
49
+ -webkit-line-clamp: 2;
50
+ }
51
+
52
+ .text {
53
+ display: -webkit-box;
54
+ overflow: hidden;
55
+ margin-top: 2px;
56
+ font-size: 13px;
57
+ line-height: 17px;
58
+ color: rgba(233, 237, 239, 0.7);
59
+ -webkit-box-orient: vertical;
60
+ -webkit-line-clamp: 2;
61
+ }
62
+
63
+ .host {
64
+ margin-top: 4px;
65
+ font-size: 12px;
66
+ line-height: 16px;
67
+ color: rgba(233, 237, 239, 0.6);
68
+ }
69
+ }
70
+
71
+ /* The message text: the link, and the time */
72
+ .whatsapp .message {
73
+ padding: 6px 6px 2px;
74
+ font-size: 14px;
75
+ line-height: 19px;
76
+
77
+ .link {
78
+ overflow-wrap: anywhere;
79
+ color: #53bdeb;
80
+ }
81
+
82
+ .time {
83
+ float: right;
84
+ margin: 6px 0 0 8px;
85
+ font-size: 11px;
86
+ color: rgba(255, 255, 255, 0.6);
87
+ }
88
+ }
@@ -0,0 +1,65 @@
1
+ import { html, nothing, unsafeCSS } from "lit"
2
+
3
+ import "../../ui/image/image.ts"
4
+
5
+ import type { Platform } from "./platform.ts"
6
+ import styles from "./whatsapp.css?inline"
7
+
8
+ // The image sizes of WhatsApp: from 300 px a large image, from 100 px a small square
9
+ const LARGE_WIDTH = 300
10
+ const SMALL_WIDTH = 100
11
+ // A large image can be at most 4 times as wide as it is high
12
+ const MAX_RATIO = 4
13
+
14
+ /**
15
+ * A sent message with a link preview, in dark mode.
16
+ *
17
+ * - Uses `og:title`, `og:description` and `og:image`, or else the `<title>` and the description.
18
+ * - An image of 300 px or wider, with a ratio up to 4:1, shows large. An image of 100–299 px shows as a small square. A smaller image does not show.
19
+ * - The image must be smaller than about 600 KB, and the URL must be absolute.
20
+ * - The title and the description show on two lines at most.
21
+ */
22
+ export const whatsapp: Platform = {
23
+ name: "WhatsApp",
24
+ styles: unsafeCSS(styles),
25
+ render(page) {
26
+ const { image, imageWidth: width, imageHeight: height } = page.og
27
+ const ratio = width && height ? width / height : 1.91
28
+
29
+ let size: "large" | "small" | "none" = image ? "large" : "none"
30
+ if (image && width !== undefined) {
31
+ if (width < SMALL_WIDTH) size = "none"
32
+ else if (width < LARGE_WIDTH || ratio > MAX_RATIO) size = "small"
33
+ }
34
+
35
+ return html`
36
+ <div class="whatsapp">
37
+ <div class="bubble">
38
+ <div class="preview ${size}">
39
+ ${
40
+ size === "large"
41
+ ? html`<astro-metadata-image class="image" .src=${image}></astro-metadata-image>`
42
+ : nothing
43
+ }
44
+ <div class="row">
45
+ ${
46
+ size === "small"
47
+ ? html`<astro-metadata-image class="image" .src=${image}></astro-metadata-image>`
48
+ : nothing
49
+ }
50
+ <div class="body">
51
+ <div class="title">${page.og.title ?? page.title}</div>
52
+ <div class="text">${page.og.description ?? page.description}</div>
53
+ <div class="host">${page.url.host}</div>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ <div class="message">
58
+ <span class="link">${page.url.href}</span>
59
+ <span class="time">12:00 ✓✓</span>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ `
64
+ },
65
+ }
@@ -0,0 +1,41 @@
1
+ import { html, LitElement, unsafeCSS } from "lit"
2
+
3
+ import type { PageMetadata } from "../data.ts"
4
+ import shared from "../styles/shared.css?inline"
5
+
6
+ import "../ui/code-block/code-block.ts"
7
+
8
+ import { define } from "../define.ts"
9
+
10
+ import "../ui/section/section.ts"
11
+
12
+ import { manifestBlock, schemaBlocks } from "./code-blocks.ts"
13
+
14
+ /**
15
+ * All metadata as raw code: the head tags, the JSON-LD scripts and the web app manifest.
16
+ */
17
+ export class RawView extends LitElement {
18
+ static properties = { data: { attribute: false } }
19
+ static styles = unsafeCSS(shared)
20
+
21
+ declare data: PageMetadata
22
+
23
+ render() {
24
+ return html`
25
+ <astro-metadata-section heading="Head">
26
+ <astro-metadata-code .code=${this.data.source} language="html" label="HTML">
27
+ </astro-metadata-code>
28
+ </astro-metadata-section>
29
+
30
+ <astro-metadata-section heading="Structured data" ?empty=${this.data.schemas.length === 0}
31
+ >${schemaBlocks(this.data.schemas)}</astro-metadata-section
32
+ >
33
+
34
+ <astro-metadata-section heading="Web app manifest" ?empty=${!this.data.manifest}>
35
+ ${manifestBlock(this.data.manifest)}
36
+ </astro-metadata-section>
37
+ `
38
+ }
39
+ }
40
+
41
+ define("astro-metadata-raw", RawView)
package/index.ts DELETED
@@ -1,31 +0,0 @@
1
- export type { Props as CanonicalProps } from "./src/components/Canonical.astro"
2
- export { default as Canonical } from "./src/components/Canonical.astro"
3
- export type { Props as DescriptionProps } from "./src/components/Description.astro"
4
- export { default as Description } from "./src/components/Description.astro"
5
- export type { FaviconFile, Props as FaviconProps } from "./src/components/Favicon.astro"
6
- export { default as Favicon } from "./src/components/Favicon.astro"
7
- export type { Props as HeadProps } from "./src/components/Head.astro"
8
- export { default as Head } from "./src/components/Head.astro"
9
- export type { Props as KeywordsProps } from "./src/components/Keywords.astro"
10
- export { default as Keywords } from "./src/components/Keywords.astro"
11
- export type {
12
- LanguageAlternate,
13
- Props as LanguageAlternatesProps,
14
- } from "./src/components/LanguageAlternates.astro"
15
- export { default as LanguageAlternates } from "./src/components/LanguageAlternates.astro"
16
- export type {
17
- OpenGraphAudio,
18
- OpenGraphImage,
19
- OpenGraphVideo,
20
- Props as OpenGraphProps,
21
- } from "./src/components/OpenGraph.astro"
22
- export { default as OpenGraph } from "./src/components/OpenGraph.astro"
23
- export type { Props as RobotsProps } from "./src/components/Robots.astro"
24
- export { default as Robots } from "./src/components/Robots.astro"
25
- export type { Props as SchemaProps } from "./src/components/Schema.astro"
26
- export { default as Schema } from "./src/components/Schema.astro"
27
- export type { Props as TitleProps } from "./src/components/Title.astro"
28
- export { default as Title } from "./src/components/Title.astro"
29
- export type { Props as TwitterProps } from "./src/components/Twitter.astro"
30
- export { default as Twitter } from "./src/components/Twitter.astro"
31
- export { Metadata } from "./src/lib/metadata.ts"
@@ -1,11 +0,0 @@
1
- ---
2
- export type Props = {
3
- value?: string
4
- }
5
-
6
- const { value } = Astro.props
7
-
8
- const canonical = value ?? Astro.url.href
9
- ---
10
-
11
- <link rel="canonical" href={canonical} />
@@ -1,9 +0,0 @@
1
- ---
2
- export type Props = {
3
- value?: string
4
- }
5
-
6
- const { value } = Astro.props
7
- ---
8
-
9
- {value && <meta name="description" content={value} />}
@@ -1,96 +0,0 @@
1
- ---
2
- export type FaviconFile = {
3
- path: string
4
- size?: number
5
- theme?: "light" | "dark"
6
- apple?: boolean
7
- }
8
-
9
- export type Props = {
10
- icons: FaviconFile[]
11
- manifest?: string
12
- sort?: boolean
13
- }
14
-
15
- const { icons, manifest, sort = true } = Astro.props
16
-
17
- /**
18
- * Returns the file extension of a favicon path in lowercase, without the
19
- * query string or the hash. For example, "/icon.SVG?v=2" gives "svg".
20
- *
21
- * @param path - The path to the favicon file.
22
- * @returns The file extension, or an empty string if the path has none.
23
- */
24
- function getExtension(path: string): string {
25
- const file = path.split(/[?#]/)[0].toLowerCase()
26
- const dot = file.lastIndexOf(".")
27
- return dot === -1 ? "" : file.slice(dot + 1)
28
- }
29
-
30
- /**
31
- * Detects the MIME type of a favicon file from its extension.
32
- *
33
- * @param path - The path to the favicon file.
34
- * @returns The MIME type of the file.
35
- */
36
- function getMimeType(path: string): string {
37
- const extension = getExtension(path)
38
- if (extension === "svg") return "image/svg+xml"
39
- if (extension === "png") return "image/png"
40
- if (extension === "webp") return "image/webp"
41
- if (extension === "jpg" || extension === "jpeg") return "image/jpeg"
42
- return "image/x-icon"
43
- }
44
-
45
- /**
46
- * Builds the media query string for a given theme.
47
- *
48
- * @param theme - The theme to build the media query for.
49
- * @returns The media query string, or undefined if no theme was provided.
50
- */
51
- function getMedia(theme?: "light" | "dark"): string | undefined {
52
- if (theme === "light") return "(prefers-color-scheme: light)"
53
- if (theme === "dark") return "(prefers-color-scheme: dark)"
54
- return undefined
55
- }
56
-
57
- /**
58
- * Returns a sort priority for a given favicon file.
59
- * Lower number = rendered first.
60
- *
61
- * Order: ico → png → svg → apple → themed variants
62
- */
63
- function getSortOrder(file: FaviconFile): number {
64
- if (file.apple) return 4
65
- if (file.theme) return 5
66
- const extension = getExtension(file.path)
67
- if (extension === "ico") return 0
68
- if (["png", "webp", "jpg", "jpeg"].includes(extension)) return 1
69
- if (extension === "svg") return 2
70
- return 3
71
- }
72
-
73
- const sorted = sort ? [...icons].sort((a, b) => getSortOrder(a) - getSortOrder(b)) : icons
74
-
75
- const prepared = sorted.map((file) => ({
76
- path: file.path,
77
- size: file.size ? `${file.size}x${file.size}` : undefined,
78
- type: getMimeType(file.path),
79
- media: getMedia(file.theme),
80
- apple: file.apple ?? false,
81
- }))
82
- ---
83
-
84
- {manifest && <link rel="manifest" href={manifest} />}
85
-
86
- {
87
- prepared.map((icon) => {
88
- if (icon.apple) {
89
- return <link rel="apple-touch-icon" href={icon.path} sizes={icon.size} />
90
- }
91
-
92
- return (
93
- <link rel="icon" type={icon.type} href={icon.path} sizes={icon.size} media={icon.media} />
94
- )
95
- })
96
- }
@@ -1,94 +0,0 @@
1
- ---
2
- import type { ComponentProps } from "astro/types"
3
-
4
- import Canonical from "./Canonical.astro"
5
- import Description from "./Description.astro"
6
- import Favicon from "./Favicon.astro"
7
- import Keywords from "./Keywords.astro"
8
- import LanguageAlternates from "./LanguageAlternates.astro"
9
- import OpenGraph from "./OpenGraph.astro"
10
- import type { OpenGraphImage } from "./OpenGraph.astro"
11
- import Robots from "./Robots.astro"
12
- import Schema from "./Schema.astro"
13
- import Title from "./Title.astro"
14
- import Twitter from "./Twitter.astro"
15
-
16
- export type Props = {
17
- title: string
18
- titleTemplate?: `${string}%s${string}`
19
- description?: string | false
20
- canonical?: string | false
21
- keywords?: string[] | false
22
- charset?: string
23
- viewport?: string
24
- image?: OpenGraphImage | false
25
- robots?: ComponentProps<typeof Robots> | false
26
- openGraph?: ComponentProps<typeof OpenGraph> | false
27
- twitter?: ComponentProps<typeof Twitter> | false
28
- favicon?: ComponentProps<typeof Favicon> | false
29
- schema?: ComponentProps<typeof Schema> | false
30
- languageAlternates?: ComponentProps<typeof LanguageAlternates>["alternates"] | false
31
- }
32
-
33
- const {
34
- title,
35
- titleTemplate,
36
- description,
37
- canonical,
38
- keywords,
39
- charset = "UTF-8",
40
- viewport = "width=device-width, initial-scale=1.0",
41
- image,
42
- robots,
43
- openGraph,
44
- twitter,
45
- favicon,
46
- schema,
47
- languageAlternates,
48
- } = Astro.props
49
-
50
- const enabled = <T,>(value: T | false | undefined): value is T => {
51
- return value !== false && value !== undefined
52
- }
53
-
54
- const og =
55
- openGraph !== false
56
- ? {
57
- title,
58
- description: enabled(description) ? description : undefined,
59
- image: enabled(image) ? image : undefined,
60
- url: enabled(canonical) ? canonical : undefined,
61
- ...openGraph,
62
- }
63
- : false
64
-
65
- const tw =
66
- twitter !== false
67
- ? {
68
- title,
69
- description: enabled(description) ? description : undefined,
70
- image: enabled(image) ? image : undefined,
71
- ...twitter,
72
- }
73
- : false
74
- ---
75
-
76
- <head>
77
- <meta charset={charset} />
78
- <slot name="top" />
79
- <meta name="viewport" content={viewport} />
80
-
81
- <Title value={title} template={titleTemplate} />
82
-
83
- {enabled(description) && <Description value={description} />}
84
- {enabled(canonical) && <Canonical value={canonical} />}
85
- {enabled(keywords) && <Keywords value={keywords} />}
86
- {enabled(robots) && <Robots {...robots} />}
87
- {enabled(og) && <OpenGraph {...og} />}
88
- {enabled(tw) && <Twitter {...tw} />}
89
- {enabled(favicon) && <Favicon {...favicon} />}
90
- {enabled(schema) && <Schema {...schema} />}
91
- {enabled(languageAlternates) && <LanguageAlternates alternates={languageAlternates} />}
92
-
93
- <slot />
94
- </head>