@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,167 @@
|
|
|
1
|
+
import { getConfig } from "./config.ts"
|
|
2
|
+
import type { ResolvedFaviconFile } from "./favicon-files.js"
|
|
3
|
+
import { withBase } from "./url.ts"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The favicon data that the integration gives to the components and the endpoints.
|
|
7
|
+
*/
|
|
8
|
+
export type FaviconInfo = {
|
|
9
|
+
/** The absolute path of the source file. */
|
|
10
|
+
source: string
|
|
11
|
+
/** The background color of the Apple icon. */
|
|
12
|
+
background: string
|
|
13
|
+
/** True for an SVG source. Only an SVG source can give an SVG file. */
|
|
14
|
+
svg: boolean
|
|
15
|
+
/** A short hash of the source file, for cache busting. */
|
|
16
|
+
hash: string
|
|
17
|
+
/** The absolute path of the sharp package. */
|
|
18
|
+
sharp: string
|
|
19
|
+
/** The files to generate, with their formats and uses. */
|
|
20
|
+
files: ResolvedFaviconFile[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The favicon config: the `favicon` option after the checks.
|
|
25
|
+
*/
|
|
26
|
+
export type FaviconConfig = Omit<FaviconInfo, "sharp">
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* An icon entry for the `icons` field of a web app manifest.
|
|
30
|
+
*/
|
|
31
|
+
export type ManifestIcon = {
|
|
32
|
+
src: string
|
|
33
|
+
sizes: string
|
|
34
|
+
type: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A link tag of a generated favicon file.
|
|
39
|
+
*/
|
|
40
|
+
export type FaviconLink = {
|
|
41
|
+
rel: "icon" | "apple-touch-icon"
|
|
42
|
+
href: string
|
|
43
|
+
type?: string
|
|
44
|
+
sizes?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The order of the link tags: ICO first, then the PNG files, the SVG and the Apple icon
|
|
48
|
+
const LINK_ORDER = { ico: 0, png: 1, svg: 2 }
|
|
49
|
+
|
|
50
|
+
const MIME_TYPES = {
|
|
51
|
+
ico: "image/x-icon",
|
|
52
|
+
png: "image/png",
|
|
53
|
+
svg: "image/svg+xml",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns the size of a file for its link tag: the largest image of an ICO file,
|
|
58
|
+
* or undefined for an SVG file.
|
|
59
|
+
*/
|
|
60
|
+
function largestSize(file: ResolvedFaviconFile): number | undefined {
|
|
61
|
+
return file.sizes.length > 0 ? Math.max(...file.sizes) : undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The favicons that the `favicon` option of the integration generates.
|
|
66
|
+
* Without the option, the lists are empty and `config` is undefined.
|
|
67
|
+
*/
|
|
68
|
+
export const Favicon = {
|
|
69
|
+
/**
|
|
70
|
+
* Returns the favicon config: the source, the background color and the files.
|
|
71
|
+
*/
|
|
72
|
+
get config(): FaviconConfig | undefined {
|
|
73
|
+
const favicon = getConfig().favicon
|
|
74
|
+
if (!favicon) return undefined
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
source: favicon.source,
|
|
78
|
+
background: favicon.background,
|
|
79
|
+
svg: favicon.svg,
|
|
80
|
+
hash: favicon.hash,
|
|
81
|
+
files: favicon.files,
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Returns the generated files, with their formats, sizes and uses.
|
|
87
|
+
*/
|
|
88
|
+
get files(): ResolvedFaviconFile[] {
|
|
89
|
+
return getConfig().favicon?.files ?? []
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Returns the URL of a generated file, with the base and a hash of the source.
|
|
94
|
+
* The hash changes when the source changes, so browsers load the new icon.
|
|
95
|
+
*
|
|
96
|
+
* @example Favicon.url("/favicon.ico") // "/favicon.ico?v=3f2a1c9e"
|
|
97
|
+
*/
|
|
98
|
+
url(path: string): string {
|
|
99
|
+
const hash = getConfig().favicon?.hash
|
|
100
|
+
return hash ? `${withBase(path)}?v=${hash}` : withBase(path)
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Returns the link tags of the generated files, in the recommended order:
|
|
105
|
+
* the ICO file, the PNG files, the SVG file and the Apple icon.
|
|
106
|
+
*/
|
|
107
|
+
links(): FaviconLink[] {
|
|
108
|
+
const order = (file: ResolvedFaviconFile) => {
|
|
109
|
+
return file.use === "apple" ? 3 : LINK_ORDER[file.type]
|
|
110
|
+
}
|
|
111
|
+
const files = Favicon.files
|
|
112
|
+
.filter((file) => {
|
|
113
|
+
return file.use !== "manifest"
|
|
114
|
+
})
|
|
115
|
+
.sort((a, b) => {
|
|
116
|
+
return order(a) - order(b) || (a.sizes[0] ?? 0) - (b.sizes[0] ?? 0)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
return files.map((file) => {
|
|
120
|
+
const size = largestSize(file)
|
|
121
|
+
if (file.use === "apple") {
|
|
122
|
+
return {
|
|
123
|
+
rel: "apple-touch-icon",
|
|
124
|
+
href: Favicon.url(file.path),
|
|
125
|
+
sizes: `${size}x${size}`,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
rel: "icon",
|
|
130
|
+
href: Favicon.url(file.path),
|
|
131
|
+
type: MIME_TYPES[file.type],
|
|
132
|
+
sizes: size ? `${size}x${size}` : undefined,
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Returns the icons for the `icons` field of a web app manifest: the PNG files
|
|
139
|
+
* of 192 px or larger.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* // src/pages/manifest.webmanifest.ts
|
|
143
|
+
* export const GET = () => Response.json({ name: "My Site", icons: Favicon.icons() })
|
|
144
|
+
*/
|
|
145
|
+
icons(): ManifestIcon[] {
|
|
146
|
+
return Favicon.files
|
|
147
|
+
.filter((file) => {
|
|
148
|
+
return file.use === "manifest"
|
|
149
|
+
})
|
|
150
|
+
.map((file) => {
|
|
151
|
+
const size = file.sizes[0]
|
|
152
|
+
return { src: Favicon.url(file.path), sizes: `${size}x${size}`, type: "image/png" }
|
|
153
|
+
})
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Generates a favicon file of the config. Returns a 404 response for a path that
|
|
158
|
+
* is not in the config.
|
|
159
|
+
*
|
|
160
|
+
* @param path - The path of the file, e.g. "/favicon-32.png".
|
|
161
|
+
*/
|
|
162
|
+
async generate(path: string): Promise<Response> {
|
|
163
|
+
// Loaded only here: the image code needs Node and sharp
|
|
164
|
+
const { renderFavicon } = await import("./favicon-image.ts")
|
|
165
|
+
return renderFavicon(path)
|
|
166
|
+
},
|
|
167
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** The metadata of a page, read from its HTML. */
|
|
2
|
+
export type HeadData = {
|
|
3
|
+
/** The `lang` attribute of the html tag. */
|
|
4
|
+
lang?: string
|
|
5
|
+
/** The value of `<meta charset>`. */
|
|
6
|
+
charset?: string
|
|
7
|
+
/** The texts of all title tags. */
|
|
8
|
+
titles: string[]
|
|
9
|
+
/** The first `content` of each meta `name`, `property` or `http-equiv`, in lowercase. */
|
|
10
|
+
meta: Record<string, string>
|
|
11
|
+
/** The number of meta tags with each `name`, `property` or `http-equiv`. */
|
|
12
|
+
metaCount: Record<string, number>
|
|
13
|
+
/** The attributes of each link tag. */
|
|
14
|
+
links: Record<string, string>[]
|
|
15
|
+
/** The texts of the JSON-LD scripts. */
|
|
16
|
+
schemas: string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export declare function parseHead(html: string): HeadData
|
|
20
|
+
|
|
21
|
+
export declare function findLinks(head: HeadData, rel: string): Record<string, string>[]
|
|
22
|
+
|
|
23
|
+
export declare function findLink(head: HeadData, rel: string): string | undefined
|
|
24
|
+
|
|
25
|
+
export declare function isAbsolute(value: string): boolean
|
package/src/lib/head.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Plain JavaScript: the integration runs in Node, which does not strip types in node_modules.
|
|
2
|
+
// The dev toolbar app uses the same functions, so the build and the toolbar give the same results.
|
|
3
|
+
|
|
4
|
+
const ENTITIES = {
|
|
5
|
+
amp: "&",
|
|
6
|
+
lt: "<",
|
|
7
|
+
gt: ">",
|
|
8
|
+
quot: '"',
|
|
9
|
+
apos: "'",
|
|
10
|
+
nbsp: " ",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// A tag with its attributes. Quoted values can contain ">".
|
|
14
|
+
const TAG = /<(meta|link)\b((?:[^>"']|"[^"]*"|'[^']*')*)>/gi
|
|
15
|
+
const ATTRIBUTE = /([^\s=/>"']+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g
|
|
16
|
+
const SCRIPT = /<script\b((?:[^>"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/script>/gi
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Replaces the HTML entities in a text with their characters.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} text
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
function decode(text) {
|
|
25
|
+
return text.replace(/&(#x[\da-f]+|#\d+|[a-z]+);/gi, (match, code) => {
|
|
26
|
+
if (code[0] !== "#") return ENTITIES[code.toLowerCase()] ?? match
|
|
27
|
+
|
|
28
|
+
const hex = code[1].toLowerCase() === "x"
|
|
29
|
+
const point = hex ? parseInt(code.slice(2), 16) : parseInt(code.slice(1), 10)
|
|
30
|
+
return point <= 0x10ffff ? String.fromCodePoint(point) : match
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns the attributes of a tag. The names are in lowercase, the values are decoded.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} source - The text between the tag name and ">".
|
|
38
|
+
* @returns {Record<string, string>}
|
|
39
|
+
*/
|
|
40
|
+
function parseAttributes(source) {
|
|
41
|
+
/** @type {Record<string, string>} */
|
|
42
|
+
const attributes = {}
|
|
43
|
+
for (const match of source.matchAll(ATTRIBUTE)) {
|
|
44
|
+
const value = match[2] ?? match[3] ?? match[4] ?? ""
|
|
45
|
+
attributes[match[1].toLowerCase()] = decode(value)
|
|
46
|
+
}
|
|
47
|
+
return attributes
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Reads the metadata of a page from its HTML.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} html - The HTML of the page.
|
|
54
|
+
* @returns {import("./head").HeadData}
|
|
55
|
+
*/
|
|
56
|
+
export function parseHead(html) {
|
|
57
|
+
const end = html.search(/<\/head>/i)
|
|
58
|
+
const head = end === -1 ? html : html.slice(0, end)
|
|
59
|
+
|
|
60
|
+
/** @type {Record<string, string>} */
|
|
61
|
+
const meta = {}
|
|
62
|
+
/** @type {Record<string, number>} */
|
|
63
|
+
const metaCount = {}
|
|
64
|
+
/** @type {Record<string, string>[]} */
|
|
65
|
+
const links = []
|
|
66
|
+
let charset
|
|
67
|
+
|
|
68
|
+
for (const match of head.matchAll(TAG)) {
|
|
69
|
+
const attributes = parseAttributes(match[2])
|
|
70
|
+
if (match[1].toLowerCase() === "link") {
|
|
71
|
+
links.push(attributes)
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
if (attributes.charset !== undefined) charset = attributes.charset
|
|
75
|
+
|
|
76
|
+
const key = (attributes.name ?? attributes.property ?? attributes["http-equiv"])?.toLowerCase()
|
|
77
|
+
if (!key || attributes.content === undefined) continue
|
|
78
|
+
|
|
79
|
+
metaCount[key] = (metaCount[key] ?? 0) + 1
|
|
80
|
+
// Keep the first value, as crawlers do
|
|
81
|
+
if (!(key in meta)) meta[key] = attributes.content
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const titles = [...head.matchAll(/<title\b[^>]*>([\s\S]*?)<\/title>/gi)].map((match) => {
|
|
85
|
+
return decode(match[1]).trim()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// JSON-LD can also be in the body
|
|
89
|
+
const schemas = [...html.matchAll(SCRIPT)]
|
|
90
|
+
.filter((match) => {
|
|
91
|
+
return /application\/ld\+json/i.test(parseAttributes(match[1]).type ?? "")
|
|
92
|
+
})
|
|
93
|
+
.map((match) => {
|
|
94
|
+
return match[2]
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const lang = /<html\b(?:[^>"']|"[^"]*"|'[^']*')*>/i.exec(html)
|
|
98
|
+
return {
|
|
99
|
+
lang: lang ? parseAttributes(lang[0].slice(5, -1)).lang : undefined,
|
|
100
|
+
charset,
|
|
101
|
+
titles,
|
|
102
|
+
meta,
|
|
103
|
+
metaCount,
|
|
104
|
+
links,
|
|
105
|
+
schemas,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Returns the link tags with a `rel` value, e.g. all "alternate" links.
|
|
111
|
+
*
|
|
112
|
+
* @param {import("./head").HeadData} head
|
|
113
|
+
* @param {string} rel - e.g. "canonical".
|
|
114
|
+
* @returns {Record<string, string>[]}
|
|
115
|
+
*/
|
|
116
|
+
export function findLinks(head, rel) {
|
|
117
|
+
return head.links.filter((attributes) => {
|
|
118
|
+
return (attributes.rel ?? "").toLowerCase().split(/\s+/).includes(rel)
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Returns the `href` of the first link with a `rel` value.
|
|
124
|
+
*
|
|
125
|
+
* @param {import("./head").HeadData} head
|
|
126
|
+
* @param {string} rel - e.g. "canonical".
|
|
127
|
+
* @returns {string | undefined}
|
|
128
|
+
*/
|
|
129
|
+
export function findLink(head, rel) {
|
|
130
|
+
return findLinks(head, rel)[0]?.href
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Returns true for a URL with "http:" or "https:".
|
|
135
|
+
*
|
|
136
|
+
* @param {string} value
|
|
137
|
+
* @returns {boolean}
|
|
138
|
+
*/
|
|
139
|
+
export function isAbsolute(value) {
|
|
140
|
+
return /^https?:\/\//i.test(value)
|
|
141
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { getConfig, localize } from "./config.ts"
|
|
2
|
+
import type { Localized } from "./config.ts"
|
|
3
|
+
import { Favicon } from "./favicon.ts"
|
|
4
|
+
import { withBase } from "./url.ts"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The web app manifest to generate. Text values can have one value for each locale:
|
|
8
|
+
* then each locale gets its own manifest file.
|
|
9
|
+
*/
|
|
10
|
+
export type ManifestConfig = {
|
|
11
|
+
name: Localized
|
|
12
|
+
shortName?: Localized
|
|
13
|
+
description?: Localized
|
|
14
|
+
/** How the installed app opens. Defaults to "standalone". */
|
|
15
|
+
display?: "standalone" | "fullscreen" | "minimal-ui" | "browser"
|
|
16
|
+
/** The page that the installed app opens. Defaults to the site root. */
|
|
17
|
+
startUrl?: Localized
|
|
18
|
+
/** Defaults to the `themeColor` of the config, or its light color. */
|
|
19
|
+
themeColor?: string
|
|
20
|
+
backgroundColor?: string
|
|
21
|
+
/** Other manifest fields, e.g. `shortcuts`, copied into the file unchanged. */
|
|
22
|
+
extra?: Record<string, unknown>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A generated manifest file: its locale, and its folder when it is not at the root.
|
|
27
|
+
*/
|
|
28
|
+
export type ManifestPath = {
|
|
29
|
+
/** The folder of the file, e.g. "fi" for /fi/manifest.webmanifest. */
|
|
30
|
+
lang?: string
|
|
31
|
+
/** The locale of the texts in the file. */
|
|
32
|
+
locale?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Returns the manifest to generate, or undefined for no manifest or your own manifest.
|
|
37
|
+
*/
|
|
38
|
+
function generated(): ManifestConfig | undefined {
|
|
39
|
+
const manifest = getConfig().manifest
|
|
40
|
+
return typeof manifest === "object" ? manifest : undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The web app manifest of the `manifest` option: an object generates the manifest,
|
|
45
|
+
* and a string is the path of your own manifest.
|
|
46
|
+
*/
|
|
47
|
+
export const Manifest = {
|
|
48
|
+
/**
|
|
49
|
+
* Returns the `manifest` option, or undefined without it.
|
|
50
|
+
*/
|
|
51
|
+
get config(): ManifestConfig | string | undefined {
|
|
52
|
+
return getConfig().manifest
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns the generated manifest files. The first locale of the texts uses
|
|
57
|
+
* /manifest.webmanifest, the other locales use /<locale>/manifest.webmanifest.
|
|
58
|
+
* Returns an empty list for no manifest or your own manifest.
|
|
59
|
+
*/
|
|
60
|
+
paths(): ManifestPath[] {
|
|
61
|
+
const manifest = generated()
|
|
62
|
+
if (!manifest) return []
|
|
63
|
+
|
|
64
|
+
// The locales are the keys of the first localized text
|
|
65
|
+
const texts = [manifest.name, manifest.shortName, manifest.description, manifest.startUrl]
|
|
66
|
+
const localized = texts.find((text) => {
|
|
67
|
+
return typeof text === "object"
|
|
68
|
+
})
|
|
69
|
+
if (!localized) return [{}]
|
|
70
|
+
|
|
71
|
+
return Object.keys(localized).map((locale, index) => {
|
|
72
|
+
return { lang: index === 0 ? undefined : locale, locale }
|
|
73
|
+
})
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Returns the URL of the manifest for a locale, with the base.
|
|
78
|
+
* An unknown locale gets the manifest at the root.
|
|
79
|
+
*
|
|
80
|
+
* @example Manifest.url("fi") // "/fi/manifest.webmanifest"
|
|
81
|
+
*/
|
|
82
|
+
url(locale?: string): string | undefined {
|
|
83
|
+
const manifest = getConfig().manifest
|
|
84
|
+
if (!manifest) return undefined
|
|
85
|
+
if (typeof manifest === "string") return withBase(manifest)
|
|
86
|
+
|
|
87
|
+
const path = Manifest.paths().find((entry) => {
|
|
88
|
+
return entry.locale === locale
|
|
89
|
+
})
|
|
90
|
+
return withBase(path?.lang ? `/${path.lang}/manifest.webmanifest` : "/manifest.webmanifest")
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Builds the manifest for a locale, with the generated favicons as its icons.
|
|
95
|
+
* Returns undefined for no manifest or your own manifest.
|
|
96
|
+
*/
|
|
97
|
+
build(locale?: string): Record<string, unknown> | undefined {
|
|
98
|
+
const manifest = generated()
|
|
99
|
+
if (!manifest) return undefined
|
|
100
|
+
|
|
101
|
+
const siteColor = getConfig().themeColor
|
|
102
|
+
const themeColor =
|
|
103
|
+
manifest.themeColor ?? (typeof siteColor === "string" ? siteColor : siteColor?.light)
|
|
104
|
+
const icons = Favicon.icons()
|
|
105
|
+
|
|
106
|
+
const json: Record<string, unknown> = {
|
|
107
|
+
name: localize(manifest.name, locale),
|
|
108
|
+
short_name: localize(manifest.shortName, locale),
|
|
109
|
+
description: localize(manifest.description, locale),
|
|
110
|
+
lang: locale,
|
|
111
|
+
start_url: withBase(localize(manifest.startUrl, locale) ?? "/"),
|
|
112
|
+
scope: withBase("/"),
|
|
113
|
+
display: manifest.display ?? "standalone",
|
|
114
|
+
theme_color: themeColor,
|
|
115
|
+
background_color: manifest.backgroundColor,
|
|
116
|
+
icons: icons.length > 0 ? icons : undefined,
|
|
117
|
+
...manifest.extra,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Remove empty fields, so the file has only the values that the config gives
|
|
121
|
+
for (const key of Object.keys(json)) {
|
|
122
|
+
if (json[key] === undefined) delete json[key]
|
|
123
|
+
}
|
|
124
|
+
return json
|
|
125
|
+
},
|
|
126
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { HeadData } from "./head"
|
|
2
|
+
|
|
3
|
+
type RuleBase = {
|
|
4
|
+
/** A short, unique name, e.g. "og-title-missing". The `rules` option turns a rule off by its id. */
|
|
5
|
+
id: string
|
|
6
|
+
/**
|
|
7
|
+
* The tag of the problem, e.g. "og:title", "title", "hreflang" or "json-ld".
|
|
8
|
+
* The dev toolbar app opens the row of this tag.
|
|
9
|
+
*/
|
|
10
|
+
field: string
|
|
11
|
+
level: "error" | "warning" | "info"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A rule that checks the head of one page. */
|
|
15
|
+
export type Rule = RuleBase & {
|
|
16
|
+
/** True for a search or sharing rule: it does not run on a noindex page. */
|
|
17
|
+
indexed?: boolean
|
|
18
|
+
/** Returns the message of a problem, or undefined. Text between backticks shows as code. */
|
|
19
|
+
check(head: HeadData): string | undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A built page: its path in the output folder, and its head. */
|
|
23
|
+
export type SitePage = {
|
|
24
|
+
path: string
|
|
25
|
+
head: HeadData
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A problem of one page, found by a site rule. */
|
|
29
|
+
export type SiteProblem = {
|
|
30
|
+
path: string
|
|
31
|
+
message: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A rule that compares the pages of the site. Only the build runs it. */
|
|
35
|
+
export type SiteRule = RuleBase & {
|
|
36
|
+
check(pages: SitePage[]): SiteProblem[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A loaded file of the page. */
|
|
40
|
+
export type Asset = {
|
|
41
|
+
/** The URL or path of the file. */
|
|
42
|
+
url: string
|
|
43
|
+
/** False when the file does not load. */
|
|
44
|
+
ok: boolean
|
|
45
|
+
/** The HTTP status of a file that does not load. */
|
|
46
|
+
status?: number
|
|
47
|
+
/** The MIME type, e.g. "image/png". */
|
|
48
|
+
type?: string
|
|
49
|
+
/** The file size in bytes. */
|
|
50
|
+
bytes?: number
|
|
51
|
+
/** The real size of an image, in px. */
|
|
52
|
+
width?: number
|
|
53
|
+
height?: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The loaded files of a page. A file that can not be checked, e.g. on another site, is not in the list. */
|
|
57
|
+
export type PageAssets = {
|
|
58
|
+
image?: Asset
|
|
59
|
+
icons: Asset[]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A rule that checks the files of the page. */
|
|
63
|
+
export type AssetRule = RuleBase & {
|
|
64
|
+
check(head: HeadData, assets: PageAssets): string | undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export declare const LIMITS: { title: number; description: number; shortDescription: number }
|
|
68
|
+
|
|
69
|
+
export declare const RULES: Rule[]
|
|
70
|
+
|
|
71
|
+
export declare const SITE_RULES: SiteRule[]
|
|
72
|
+
|
|
73
|
+
export declare const ASSET_RULES: AssetRule[]
|