@mannisto/astro-metadata 1.0.1 → 2.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/README.md +427 -82
- package/package.json +35 -22
- package/src/assets/icons/LICENSE.md +35 -0
- package/src/assets/icons/binoculars.svg +1 -0
- package/src/assets/icons/caret-right.svg +1 -0
- package/src/assets/icons/check.svg +1 -0
- package/src/assets/icons/copy.svg +1 -0
- package/src/assets/icons/seal-check-duotone.svg +1 -0
- package/src/assets/icons/seal-question-duotone.svg +1 -0
- package/src/assets/icons/warning-circle-duotone.svg +1 -0
- package/src/assets/icons/warning-duotone.svg +1 -0
- package/src/assets/icons/x.svg +1 -0
- package/src/components/Metadata.astro +145 -0
- package/src/components/index.ts +2 -0
- package/src/integration.d.ts +112 -0
- package/src/integration.js +508 -0
- package/src/lib/checks.d.ts +70 -0
- package/src/lib/checks.js +193 -0
- package/src/lib/config.ts +111 -0
- package/src/lib/favicon-files.d.ts +29 -0
- package/src/lib/favicon-files.js +103 -0
- package/src/lib/favicon-image.ts +124 -0
- package/src/lib/favicon.ts +167 -0
- package/src/lib/head.d.ts +25 -0
- package/src/lib/head.js +141 -0
- package/src/lib/manifest.ts +126 -0
- package/src/lib/rules.d.ts +73 -0
- package/src/lib/rules.js +902 -0
- package/src/lib/schema.ts +208 -0
- package/src/lib/site.ts +72 -0
- package/src/lib/url.ts +79 -0
- package/src/routes/favicon.ts +14 -0
- package/src/routes/manifest.ts +18 -0
- package/src/runtime.ts +16 -0
- package/src/toolbar/app.ts +72 -0
- package/src/toolbar/assets.ts +81 -0
- package/src/toolbar/data.ts +282 -0
- package/src/toolbar/define.ts +7 -0
- package/src/toolbar/env.d.ts +10 -0
- package/src/toolbar/highlight.ts +83 -0
- package/src/toolbar/styles/shared.css +18 -0
- package/src/toolbar/styles/tokens.css +182 -0
- package/src/toolbar/toolbar-badge.ts +78 -0
- package/src/toolbar/ui/alert/alert.css +126 -0
- package/src/toolbar/ui/alert/alert.ts +102 -0
- package/src/toolbar/ui/badge/badge.css +41 -0
- package/src/toolbar/ui/badge/badge.ts +24 -0
- package/src/toolbar/ui/code-block/code-block.css +90 -0
- package/src/toolbar/ui/code-block/code-block.ts +68 -0
- package/src/toolbar/ui/color-swatch/color-swatch.css +15 -0
- package/src/toolbar/ui/color-swatch/color-swatch.ts +31 -0
- package/src/toolbar/ui/icon/icon.css +11 -0
- package/src/toolbar/ui/icon/icon.ts +46 -0
- package/src/toolbar/ui/image/image.css +18 -0
- package/src/toolbar/ui/image/image.ts +58 -0
- package/src/toolbar/ui/inline-code/inline-code.css +16 -0
- package/src/toolbar/ui/inline-code/inline-code.ts +41 -0
- package/src/toolbar/ui/section/section.css +88 -0
- package/src/toolbar/ui/section/section.ts +78 -0
- package/src/toolbar/ui/tab/tab.css +7 -0
- package/src/toolbar/ui/tab/tab.ts +47 -0
- package/src/toolbar/ui/tab-group/tab-group.css +42 -0
- package/src/toolbar/ui/tab-group/tab-group.ts +101 -0
- package/src/toolbar/ui/table/table.css +124 -0
- package/src/toolbar/ui/table/table.ts +155 -0
- package/src/toolbar/views/code-blocks.ts +35 -0
- package/src/toolbar/views/metadata-panel.css +101 -0
- package/src/toolbar/views/metadata-panel.ts +125 -0
- package/src/toolbar/views/overview-view.css +27 -0
- package/src/toolbar/views/overview-view.ts +401 -0
- package/src/toolbar/views/previews/discord.css +70 -0
- package/src/toolbar/views/previews/discord.ts +60 -0
- package/src/toolbar/views/previews/facebook.css +65 -0
- package/src/toolbar/views/previews/facebook.ts +36 -0
- package/src/toolbar/views/previews/google.css +75 -0
- package/src/toolbar/views/previews/google.ts +36 -0
- package/src/toolbar/views/previews/linkedin.css +58 -0
- package/src/toolbar/views/previews/linkedin.ts +34 -0
- package/src/toolbar/views/previews/page.ts +129 -0
- package/src/toolbar/views/previews/platform.ts +14 -0
- package/src/toolbar/views/previews/previews-view.css +37 -0
- package/src/toolbar/views/previews/previews-view.ts +52 -0
- package/src/toolbar/views/previews/slack.css +61 -0
- package/src/toolbar/views/previews/slack.ts +47 -0
- package/src/toolbar/views/previews/twitter.css +89 -0
- package/src/toolbar/views/previews/twitter.ts +57 -0
- package/src/toolbar/views/previews/whatsapp.css +88 -0
- package/src/toolbar/views/previews/whatsapp.ts +65 -0
- package/src/toolbar/views/raw-view.ts +41 -0
- package/index.ts +0 -31
- package/src/components/Canonical.astro +0 -11
- package/src/components/Description.astro +0 -9
- package/src/components/Favicon.astro +0 -88
- package/src/components/Head.astro +0 -96
- package/src/components/Keywords.astro +0 -9
- package/src/components/LanguageAlternates.astro +0 -18
- package/src/components/OpenGraph.astro +0 -75
- package/src/components/Robots.astro +0 -25
- package/src/components/Schema.astro +0 -9
- package/src/components/Title.astro +0 -10
- package/src/components/Twitter.astro +0 -25
- package/src/lib/metadata.ts +0 -77
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Article,
|
|
3
|
+
BreadcrumbList,
|
|
4
|
+
Graph,
|
|
5
|
+
Organization,
|
|
6
|
+
Person,
|
|
7
|
+
Thing,
|
|
8
|
+
WebSite,
|
|
9
|
+
WithContext,
|
|
10
|
+
} from "schema-dts"
|
|
11
|
+
|
|
12
|
+
import { getConfig, localize } from "./config.ts"
|
|
13
|
+
import { url } from "./url.ts"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The data of a JSON-LD script. The schema.org types give the editor completion,
|
|
17
|
+
* and other objects are also accepted.
|
|
18
|
+
*/
|
|
19
|
+
export type SchemaData = WithContext<Thing> | Graph | Record<string, unknown>
|
|
20
|
+
|
|
21
|
+
export type SchemaWebsite = {
|
|
22
|
+
/** Defaults to `siteName` in the config. */
|
|
23
|
+
name?: string
|
|
24
|
+
/** Defaults to `description` in the config. */
|
|
25
|
+
description?: string
|
|
26
|
+
/** Defaults to the site root. */
|
|
27
|
+
url?: string
|
|
28
|
+
/** The locale of the localized config values, e.g. `Astro.currentLocale`. */
|
|
29
|
+
locale?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type SchemaOrganization = {
|
|
33
|
+
/** Defaults to `siteName` in the config. */
|
|
34
|
+
name?: string
|
|
35
|
+
/** Defaults to the site root. */
|
|
36
|
+
url?: string
|
|
37
|
+
/** The path or URL of the logo, e.g. "/logo.png". */
|
|
38
|
+
logo?: string
|
|
39
|
+
/** The profiles of the organization on other sites. */
|
|
40
|
+
sameAs?: string[]
|
|
41
|
+
/** The locale of the localized config values, e.g. `Astro.currentLocale`. */
|
|
42
|
+
locale?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type SchemaAuthor = string | { name: string; url?: string }
|
|
46
|
+
|
|
47
|
+
export type SchemaArticle = {
|
|
48
|
+
/** Defaults to "Article". */
|
|
49
|
+
type?: "Article" | "BlogPosting" | "NewsArticle"
|
|
50
|
+
title: string
|
|
51
|
+
description?: string
|
|
52
|
+
/** The paths or URLs of the images. */
|
|
53
|
+
image?: string | string[]
|
|
54
|
+
published?: Date | string
|
|
55
|
+
modified?: Date | string
|
|
56
|
+
author?: SchemaAuthor | SchemaAuthor[]
|
|
57
|
+
/** The URL of the article page, e.g. `Astro.url.pathname`. */
|
|
58
|
+
url?: string
|
|
59
|
+
/** The locale of the article. The publisher name uses it too. */
|
|
60
|
+
locale?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type SchemaBreadcrumb = {
|
|
64
|
+
name: string
|
|
65
|
+
/** The path or URL of the page, e.g. "/exhibitions". */
|
|
66
|
+
url: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Returns a date as an ISO 8601 text. A text stays unchanged.
|
|
71
|
+
*/
|
|
72
|
+
function isoDate(value: Date | string | undefined): string | undefined {
|
|
73
|
+
if (value === undefined || typeof value === "string") return value
|
|
74
|
+
return value.toISOString()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Returns an author as a schema.org person.
|
|
79
|
+
*/
|
|
80
|
+
function person(author: SchemaAuthor): Person {
|
|
81
|
+
if (typeof author === "string") {
|
|
82
|
+
return { "@type": "Person", name: author }
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
"@type": "Person",
|
|
86
|
+
name: author.name,
|
|
87
|
+
url: author.url ? url(author.url) : undefined,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Returns the `WebSite` schema, with the name and the URL of the site.
|
|
93
|
+
*
|
|
94
|
+
* @example Schema.website({ locale: Astro.currentLocale })
|
|
95
|
+
*/
|
|
96
|
+
function website(options: SchemaWebsite = {}): WithContext<WebSite> {
|
|
97
|
+
const config = getConfig()
|
|
98
|
+
return {
|
|
99
|
+
"@context": "https://schema.org",
|
|
100
|
+
"@type": "WebSite",
|
|
101
|
+
name: options.name ?? localize(config.siteName, options.locale),
|
|
102
|
+
description: options.description ?? localize(config.description, options.locale),
|
|
103
|
+
url: url(options.url ?? "/"),
|
|
104
|
+
inLanguage: options.locale,
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Returns the `Organization` schema of the site owner.
|
|
110
|
+
*
|
|
111
|
+
* @example Schema.organization({ logo: "/logo.png" })
|
|
112
|
+
*/
|
|
113
|
+
function organization(options: SchemaOrganization = {}): WithContext<Organization> {
|
|
114
|
+
const config = getConfig()
|
|
115
|
+
return {
|
|
116
|
+
"@context": "https://schema.org",
|
|
117
|
+
"@type": "Organization",
|
|
118
|
+
name: options.name ?? localize(config.siteName, options.locale),
|
|
119
|
+
url: url(options.url ?? "/"),
|
|
120
|
+
logo: options.logo ? url(options.logo) : undefined,
|
|
121
|
+
sameAs: options.sameAs,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Returns the `Article` schema of a page. The publisher is the site from the config.
|
|
127
|
+
*
|
|
128
|
+
* @example Schema.article({ title, published: post.date, author: "Acme Writer" })
|
|
129
|
+
*/
|
|
130
|
+
function article(options: SchemaArticle): WithContext<Article> {
|
|
131
|
+
const config = getConfig()
|
|
132
|
+
const siteName = localize(config.siteName, options.locale)
|
|
133
|
+
const images = typeof options.image === "string" ? [options.image] : options.image
|
|
134
|
+
const authors = Array.isArray(options.author) ? options.author : [options.author]
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
"@context": "https://schema.org",
|
|
138
|
+
"@type": options.type ?? "Article",
|
|
139
|
+
headline: options.title,
|
|
140
|
+
description: options.description,
|
|
141
|
+
image: images?.map((image) => {
|
|
142
|
+
return url(image)
|
|
143
|
+
}),
|
|
144
|
+
datePublished: isoDate(options.published),
|
|
145
|
+
dateModified: isoDate(options.modified),
|
|
146
|
+
author: options.author
|
|
147
|
+
? authors.flatMap((author) => {
|
|
148
|
+
return author ? [person(author)] : []
|
|
149
|
+
})
|
|
150
|
+
: undefined,
|
|
151
|
+
publisher: siteName
|
|
152
|
+
? {
|
|
153
|
+
"@type": "Organization",
|
|
154
|
+
name: siteName,
|
|
155
|
+
url: url("/"),
|
|
156
|
+
}
|
|
157
|
+
: undefined,
|
|
158
|
+
mainEntityOfPage: options.url ? url(options.url) : undefined,
|
|
159
|
+
inLanguage: options.locale,
|
|
160
|
+
} as WithContext<Article>
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Returns the `BreadcrumbList` schema: the path from the home page to the current page.
|
|
165
|
+
*
|
|
166
|
+
* @example Schema.breadcrumbs([{ name: "Home", url: "/" }, { name: "Blog", url: "/blog" }])
|
|
167
|
+
*/
|
|
168
|
+
function breadcrumbs(items: SchemaBreadcrumb[]): WithContext<BreadcrumbList> {
|
|
169
|
+
return {
|
|
170
|
+
"@context": "https://schema.org",
|
|
171
|
+
"@type": "BreadcrumbList",
|
|
172
|
+
itemListElement: items.map((item, index) => {
|
|
173
|
+
return {
|
|
174
|
+
"@type": "ListItem",
|
|
175
|
+
position: index + 1,
|
|
176
|
+
name: item.name,
|
|
177
|
+
item: url(item.url),
|
|
178
|
+
}
|
|
179
|
+
}),
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Builders for common JSON-LD schemas. They fill in values from the config of the
|
|
185
|
+
* integration and make all URLs absolute. Give the result to `<Metadata schema>`.
|
|
186
|
+
*/
|
|
187
|
+
export const Schema = {
|
|
188
|
+
website,
|
|
189
|
+
organization,
|
|
190
|
+
article,
|
|
191
|
+
breadcrumbs,
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Returns the data as JSON that is safe inside a script tag.
|
|
195
|
+
*
|
|
196
|
+
* @example <script type="application/ld+json" set:html={Schema.stringify(data)} />
|
|
197
|
+
*/
|
|
198
|
+
stringify(data: SchemaData | SchemaData[]): string {
|
|
199
|
+
// Escape the characters that can end the script tag or break the JavaScript parser.
|
|
200
|
+
// A value such as "</script>" in the schema then stays inside the JSON.
|
|
201
|
+
return JSON.stringify(data)
|
|
202
|
+
.replace(/</g, "\\u003c")
|
|
203
|
+
.replace(/>/g, "\\u003e")
|
|
204
|
+
.replace(/&/g, "\\u0026")
|
|
205
|
+
.replace(/\u2028/g, "\\u2028")
|
|
206
|
+
.replace(/\u2029/g, "\\u2029")
|
|
207
|
+
},
|
|
208
|
+
}
|
package/src/lib/site.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { getConfig, localize } from "./config.ts"
|
|
2
|
+
import type { MetadataConfig } from "./config.ts"
|
|
3
|
+
import { pageUrl, url } from "./url.ts"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The site values of the integration, and absolute URLs with the Astro `site` and `base`.
|
|
7
|
+
*/
|
|
8
|
+
export const Site = {
|
|
9
|
+
/**
|
|
10
|
+
* Returns the site values of the integration. Without the integration, all values are undefined.
|
|
11
|
+
*/
|
|
12
|
+
get config(): MetadataConfig {
|
|
13
|
+
const config = getConfig()
|
|
14
|
+
return {
|
|
15
|
+
siteName: config.siteName,
|
|
16
|
+
titleTemplate: config.titleTemplate,
|
|
17
|
+
description: config.description,
|
|
18
|
+
image: config.image,
|
|
19
|
+
twitter: config.twitter,
|
|
20
|
+
themeColor: config.themeColor,
|
|
21
|
+
feeds: config.feeds,
|
|
22
|
+
robots: config.robots,
|
|
23
|
+
manifest: config.manifest,
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns the name of the site for a locale.
|
|
29
|
+
*
|
|
30
|
+
* @param locale - e.g. "fi". A missing locale uses the first value.
|
|
31
|
+
*/
|
|
32
|
+
name(locale?: string): string | undefined {
|
|
33
|
+
return localize(getConfig().siteName, locale)
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns the full title of a page: the title in the title template, or the site name
|
|
38
|
+
* for a page without a title.
|
|
39
|
+
*
|
|
40
|
+
* @example Site.title("About") // "About | My Site"
|
|
41
|
+
*/
|
|
42
|
+
title(title?: string, locale?: string): string | undefined {
|
|
43
|
+
const template = localize(getConfig().titleTemplate, locale)
|
|
44
|
+
if (!title) return Site.name(locale)
|
|
45
|
+
if (!template?.includes("%s")) return title
|
|
46
|
+
|
|
47
|
+
// A replacer function keeps "$&" and other "$" patterns in the title as text
|
|
48
|
+
return template.replace("%s", () => {
|
|
49
|
+
return title
|
|
50
|
+
})
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns the absolute URL of a path, with the Astro `site` and `base`.
|
|
55
|
+
* Absolute URLs stay unchanged.
|
|
56
|
+
*
|
|
57
|
+
* @example Site.url("/og.jpg") // "https://example.com/og.jpg"
|
|
58
|
+
*/
|
|
59
|
+
url(path: string): string {
|
|
60
|
+
return url(path)
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Returns the absolute URL of a page, with or without the slash at the end,
|
|
65
|
+
* the same as the Astro `trailingSlash` and `build.format` settings.
|
|
66
|
+
*
|
|
67
|
+
* @example Site.pageUrl("/blog") // "https://example.com/blog/"
|
|
68
|
+
*/
|
|
69
|
+
pageUrl(path: string): string {
|
|
70
|
+
return pageUrl(path)
|
|
71
|
+
},
|
|
72
|
+
}
|
package/src/lib/url.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { getConfig } from "./config.ts"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns true for a URL with a scheme ("https:", "data:") or a protocol-relative URL ("//").
|
|
5
|
+
*/
|
|
6
|
+
function isAbsolute(value: string): boolean {
|
|
7
|
+
return /^[a-z][a-z\d+.-]*:/i.test(value) || value.startsWith("//")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Adds the Astro `base` to a path: "/og.jpg" becomes "/docs/og.jpg".
|
|
12
|
+
* A path that already starts with the base stays unchanged.
|
|
13
|
+
*
|
|
14
|
+
* @param path - A path in the site, e.g. "/og.jpg".
|
|
15
|
+
* @returns The path with the base.
|
|
16
|
+
*/
|
|
17
|
+
export function withBase(path: string): string {
|
|
18
|
+
if (isAbsolute(path)) return path
|
|
19
|
+
|
|
20
|
+
const base = (import.meta.env.BASE_URL ?? "/").replace(/\/$/, "")
|
|
21
|
+
const clean = path.startsWith("/") ? path : `/${path}`
|
|
22
|
+
if (!base || clean === base || clean.startsWith(`${base}/`)) return clean
|
|
23
|
+
return `${base}${clean}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Returns the absolute URL of a path in the site, with the Astro `site` and `base`:
|
|
28
|
+
* "/og.jpg" becomes "https://example.com/docs/og.jpg". Absolute URLs stay unchanged.
|
|
29
|
+
* Without `site` in the Astro config, the result is the path with the base.
|
|
30
|
+
*
|
|
31
|
+
* @param path - A path in the site, e.g. "/og.jpg".
|
|
32
|
+
* @returns The absolute URL.
|
|
33
|
+
*/
|
|
34
|
+
export function url(path: string): string {
|
|
35
|
+
if (isAbsolute(path)) return path
|
|
36
|
+
|
|
37
|
+
const site = import.meta.env.SITE
|
|
38
|
+
const pathname = withBase(path)
|
|
39
|
+
return site ? new URL(pathname, site).href : pathname
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Adds or removes the slash at the end of a page path, the same as Astro:
|
|
44
|
+
* - `trailingSlash: "always"` adds the slash, and `"never"` removes it.
|
|
45
|
+
* - With `"ignore"`, `build.format` decides: "directory" adds the slash, and "file" removes it.
|
|
46
|
+
* With "preserve", the path stays unchanged.
|
|
47
|
+
* Paths with a file extension, a query or a hash stay unchanged.
|
|
48
|
+
* Without the integration, the path stays unchanged.
|
|
49
|
+
*
|
|
50
|
+
* @param path - A page path, e.g. "/blog".
|
|
51
|
+
* @returns The path with or without the slash at the end.
|
|
52
|
+
*/
|
|
53
|
+
export function pagePath(path: string): string {
|
|
54
|
+
const { trailingSlash, buildFormat } = getConfig()
|
|
55
|
+
if (!trailingSlash || path === "/" || /[?#]/.test(path)) return path
|
|
56
|
+
|
|
57
|
+
const last = path.split("/").pop() ?? ""
|
|
58
|
+
if (last.includes(".")) return path
|
|
59
|
+
|
|
60
|
+
let slash = trailingSlash === "always"
|
|
61
|
+
if (trailingSlash === "ignore") {
|
|
62
|
+
if (buildFormat === "preserve") return path
|
|
63
|
+
slash = buildFormat !== "file"
|
|
64
|
+
}
|
|
65
|
+
const clean = path.replace(/\/+$/, "")
|
|
66
|
+
return slash ? `${clean}/` : clean || "/"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Returns the absolute URL of a page, with the slash at the end that Astro uses.
|
|
71
|
+
* Absolute URLs stay unchanged.
|
|
72
|
+
*
|
|
73
|
+
* @param path - A page path, e.g. "/blog".
|
|
74
|
+
* @returns The absolute URL, e.g. "https://example.com/blog/".
|
|
75
|
+
*/
|
|
76
|
+
export function pageUrl(path: string): string {
|
|
77
|
+
if (isAbsolute(path)) return path
|
|
78
|
+
return url(pagePath(path))
|
|
79
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { APIRoute } from "astro"
|
|
2
|
+
|
|
3
|
+
import { Favicon } from "../lib/favicon.ts"
|
|
4
|
+
|
|
5
|
+
export const prerender = true
|
|
6
|
+
|
|
7
|
+
// The integration adds this route one time for each favicon file.
|
|
8
|
+
// The path without the base tells which file to generate.
|
|
9
|
+
export const GET: APIRoute = ({ url }) => {
|
|
10
|
+
const base = import.meta.env.BASE_URL.replace(/\/$/, "")
|
|
11
|
+
const path =
|
|
12
|
+
base && url.pathname.startsWith(`${base}/`) ? url.pathname.slice(base.length) : url.pathname
|
|
13
|
+
return Favicon.generate(path)
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { APIRoute, GetStaticPaths } from "astro"
|
|
2
|
+
|
|
3
|
+
import { Manifest } from "../lib/manifest.ts"
|
|
4
|
+
|
|
5
|
+
export const prerender = true
|
|
6
|
+
|
|
7
|
+
export const getStaticPaths: GetStaticPaths = () => {
|
|
8
|
+
return Manifest.paths().map((path) => {
|
|
9
|
+
return { params: { lang: path.lang }, props: { locale: path.locale } }
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const GET: APIRoute = ({ props }) => {
|
|
14
|
+
const manifest = Manifest.build(props.locale)
|
|
15
|
+
return new Response(JSON.stringify(manifest, null, 2), {
|
|
16
|
+
headers: { "Content-Type": "application/manifest+json" },
|
|
17
|
+
})
|
|
18
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type { Feed, Localized, MetadataConfig, MetadataImage, ThemeColor } from "./lib/config.ts"
|
|
2
|
+
export type { FaviconFile, ResolvedFaviconFile } from "./lib/favicon-files.js"
|
|
3
|
+
export type { FaviconConfig, FaviconLink, ManifestIcon } from "./lib/favicon.ts"
|
|
4
|
+
export { Favicon } from "./lib/favicon.ts"
|
|
5
|
+
export type { ManifestConfig, ManifestPath } from "./lib/manifest.ts"
|
|
6
|
+
export { Manifest } from "./lib/manifest.ts"
|
|
7
|
+
export type {
|
|
8
|
+
SchemaArticle,
|
|
9
|
+
SchemaAuthor,
|
|
10
|
+
SchemaBreadcrumb,
|
|
11
|
+
SchemaData,
|
|
12
|
+
SchemaOrganization,
|
|
13
|
+
SchemaWebsite,
|
|
14
|
+
} from "./lib/schema.ts"
|
|
15
|
+
export { Schema } from "./lib/schema.ts"
|
|
16
|
+
export { Site } from "./lib/site.ts"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { defineToolbarApp } from "astro/toolbar"
|
|
2
|
+
|
|
3
|
+
import { styleBadge } from "./toolbar-badge.ts"
|
|
4
|
+
|
|
5
|
+
import "./views/metadata-panel.ts"
|
|
6
|
+
|
|
7
|
+
import { checkPage, collect, summarize } from "./data.ts"
|
|
8
|
+
import type { MetadataPanel } from "./views/metadata-panel.ts"
|
|
9
|
+
|
|
10
|
+
export default defineToolbarApp({
|
|
11
|
+
init(canvas, app) {
|
|
12
|
+
styleBadge()
|
|
13
|
+
|
|
14
|
+
const panel = document.createElement("astro-metadata-panel") as MetadataPanel
|
|
15
|
+
canvas.append(panel)
|
|
16
|
+
let open = false
|
|
17
|
+
|
|
18
|
+
// The badge shows the worst problem of the page
|
|
19
|
+
async function updateBadge() {
|
|
20
|
+
const { checks } = await checkPage()
|
|
21
|
+
const { level } = summarize(checks)
|
|
22
|
+
if (level) {
|
|
23
|
+
app.toggleNotification({ state: true, level })
|
|
24
|
+
} else {
|
|
25
|
+
app.toggleNotification({ state: false })
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function load() {
|
|
30
|
+
panel.data = undefined
|
|
31
|
+
panel.data = await collect()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function setOpen(state: boolean) {
|
|
35
|
+
app.toggleState({ state })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
app.onToggled(({ state }) => {
|
|
39
|
+
open = state
|
|
40
|
+
if (state) load()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
panel.addEventListener("close", () => {
|
|
44
|
+
setOpen(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// Alt+M (Option+M on a Mac) opens and closes the panel. Escape closes it.
|
|
48
|
+
// `code` is the key, not the character: Option+M types "µ" on a Mac.
|
|
49
|
+
document.addEventListener("keydown", (event) => {
|
|
50
|
+
const target = event.target as HTMLElement | null
|
|
51
|
+
if (target?.closest("input, textarea, select, [contenteditable]")) return
|
|
52
|
+
|
|
53
|
+
if (event.altKey && event.code === "KeyM") {
|
|
54
|
+
event.preventDefault()
|
|
55
|
+
setOpen(!open)
|
|
56
|
+
} else if (event.key === "Escape" && open) {
|
|
57
|
+
setOpen(false)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// View transitions change the page without a full load
|
|
62
|
+
document.addEventListener("astro:after-swap", () => {
|
|
63
|
+
updateBadge()
|
|
64
|
+
if (open) load()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
updateBadge()
|
|
68
|
+
|
|
69
|
+
// ?metadata in the URL opens the panel when the page loads
|
|
70
|
+
if (new URLSearchParams(location.search).has("metadata")) setOpen(true)
|
|
71
|
+
},
|
|
72
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Asset, HeadData, PageAssets } from "../lib/checks.js"
|
|
2
|
+
import { getConfig } from "../lib/config.ts"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns the URL to load a file from, or undefined for a file on another site.
|
|
6
|
+
* A file of the site loads from the dev server, because the `site` URL is the
|
|
7
|
+
* production site, where a new file does not exist yet.
|
|
8
|
+
*/
|
|
9
|
+
function devUrl(url: string): URL | undefined {
|
|
10
|
+
let parsed: URL
|
|
11
|
+
try {
|
|
12
|
+
parsed = new URL(url, location.href)
|
|
13
|
+
} catch {
|
|
14
|
+
return undefined
|
|
15
|
+
}
|
|
16
|
+
if (parsed.origin === location.origin) return parsed
|
|
17
|
+
|
|
18
|
+
const site = getConfig().site
|
|
19
|
+
if (!site || parsed.origin !== new URL(site).origin) return undefined
|
|
20
|
+
return new URL(`${parsed.pathname}${parsed.search}`, location.origin)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns the real size of an image, or undefined for a format that the browser
|
|
25
|
+
* can not measure, e.g. SVG.
|
|
26
|
+
*/
|
|
27
|
+
async function imageSize(blob: Blob): Promise<{ width: number; height: number } | undefined> {
|
|
28
|
+
try {
|
|
29
|
+
const bitmap = await createImageBitmap(blob)
|
|
30
|
+
const size = { width: bitmap.width, height: bitmap.height }
|
|
31
|
+
bitmap.close()
|
|
32
|
+
return size
|
|
33
|
+
} catch {
|
|
34
|
+
return undefined
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Loads a file of the page: whether it loads, its type, its size in bytes,
|
|
40
|
+
* and the size of an image.
|
|
41
|
+
*/
|
|
42
|
+
async function loadAsset(url: string, measure: boolean): Promise<Asset | undefined> {
|
|
43
|
+
const source = devUrl(url)
|
|
44
|
+
if (!source) return undefined
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch(source)
|
|
48
|
+
if (!response.ok) return { url, ok: false, status: response.status }
|
|
49
|
+
|
|
50
|
+
const blob = await response.blob()
|
|
51
|
+
const type = blob.type || response.headers.get("content-type")?.split(";")[0]
|
|
52
|
+
const size = measure ? await imageSize(blob) : undefined
|
|
53
|
+
return { url, ok: true, type, bytes: blob.size, ...size }
|
|
54
|
+
} catch {
|
|
55
|
+
return { url, ok: false }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Loads the `og:image` and the favicons of the page from the dev server.
|
|
61
|
+
* The build reads the same files from the output folder.
|
|
62
|
+
*/
|
|
63
|
+
export async function loadAssets(head: HeadData): Promise<PageAssets> {
|
|
64
|
+
const imageUrl = head.meta["og:image"]
|
|
65
|
+
const links = head.links.filter((link) => {
|
|
66
|
+
return /(^|\s)(icon|apple-touch-icon)(\s|$)/i.test(link.rel ?? "") && Boolean(link.href)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const [image, ...icons] = await Promise.all([
|
|
70
|
+
imageUrl ? loadAsset(imageUrl, true) : Promise.resolve(undefined),
|
|
71
|
+
...links.map((link) => {
|
|
72
|
+
return loadAsset(link.href, false)
|
|
73
|
+
}),
|
|
74
|
+
])
|
|
75
|
+
return {
|
|
76
|
+
image,
|
|
77
|
+
icons: icons.filter((icon): icon is Asset => {
|
|
78
|
+
return icon !== undefined
|
|
79
|
+
}),
|
|
80
|
+
}
|
|
81
|
+
}
|