@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.
Files changed (102) hide show
  1. package/README.md +427 -82
  2. package/package.json +35 -22
  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 +902 -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 +101 -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 -88
  94. package/src/components/Head.astro +0 -96
  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 -9
  100. package/src/components/Title.astro +0 -10
  101. package/src/components/Twitter.astro +0 -25
  102. package/src/lib/metadata.ts +0 -77
@@ -0,0 +1,101 @@
1
+ * {
2
+ box-sizing: border-box;
3
+ }
4
+
5
+ .panel {
6
+ position: fixed;
7
+ inset: 0 0 0 auto;
8
+ z-index: 1;
9
+ display: flex;
10
+ flex-direction: column;
11
+ width: var(--panel-width, 480px);
12
+ max-width: 100vw;
13
+ background: var(--zinc-950);
14
+ border-left: 1px solid var(--zinc-800);
15
+ box-shadow: var(--shadow);
16
+ animation: slide-in 160ms ease-out;
17
+ }
18
+
19
+ @keyframes slide-in {
20
+ from {
21
+ transform: translateX(24px);
22
+ opacity: 0;
23
+ }
24
+ }
25
+
26
+ /* The handle on the left edge: drag it to change the width */
27
+ .resize {
28
+ position: absolute;
29
+ inset: 0 auto 0 -3px;
30
+ width: 6px;
31
+ cursor: ew-resize;
32
+
33
+ &:hover,
34
+ &.active {
35
+ background: var(--zinc-700);
36
+ }
37
+ }
38
+
39
+ .header {
40
+ display: flex;
41
+ align-items: center;
42
+ gap: var(--space-300);
43
+ padding: var(--space-300) var(--space-400);
44
+ border-bottom: 1px solid var(--zinc-800);
45
+ }
46
+
47
+ .heading {
48
+ flex: 1;
49
+ min-width: 0;
50
+ }
51
+
52
+ /* The title and the Beta badge */
53
+ .title {
54
+ display: flex;
55
+ align-items: center;
56
+ gap: var(--space-200);
57
+ }
58
+
59
+ h1 {
60
+ margin: 0;
61
+ font-size: var(--font-size-body);
62
+ font-weight: 600;
63
+ }
64
+
65
+ .path {
66
+ margin: 0;
67
+ overflow: hidden;
68
+ font-size: var(--font-size-small);
69
+ color: var(--zinc-400);
70
+ text-overflow: ellipsis;
71
+ white-space: nowrap;
72
+ }
73
+
74
+ button {
75
+ font: inherit;
76
+ color: inherit;
77
+ cursor: pointer;
78
+ background: none;
79
+ border: 0;
80
+ }
81
+
82
+ .close {
83
+ display: grid;
84
+ place-items: center;
85
+ width: 28px;
86
+ height: 28px;
87
+ border-radius: var(--radius-small);
88
+
89
+ &:hover {
90
+ background: var(--zinc-800);
91
+ }
92
+ }
93
+
94
+ astro-metadata-tab-group {
95
+ flex: 1;
96
+ }
97
+
98
+ .loading {
99
+ padding: var(--space-400);
100
+ color: var(--zinc-400);
101
+ }
@@ -0,0 +1,125 @@
1
+ import { html, LitElement, nothing, unsafeCSS } from "lit"
2
+ import { styleMap } from "lit/directives/style-map.js"
3
+
4
+ import { summarize } from "../data.ts"
5
+ import type { PageMetadata } from "../data.ts"
6
+ import { define } from "../define.ts"
7
+ import tokens from "../styles/tokens.css?inline"
8
+
9
+ import "../ui/badge/badge.ts"
10
+ import "../ui/icon/icon.ts"
11
+ import "../ui/tab-group/tab-group.ts"
12
+
13
+ import styles from "./metadata-panel.css?inline"
14
+
15
+ import "./overview-view.ts"
16
+ import "./previews/previews-view.ts"
17
+ import "./raw-view.ts"
18
+
19
+ const TAB_KEY = "astro-metadata:tab"
20
+ const WIDTH_KEY = "astro-metadata:width"
21
+ const MIN_WIDTH = 360
22
+
23
+ /**
24
+ * The slide-out panel: a header, and the tabs with the views.
25
+ * It fires a `close` event when the user clicks the close button.
26
+ */
27
+ export class MetadataPanel extends LitElement {
28
+ static properties = {
29
+ data: { attribute: false },
30
+ width: { state: true },
31
+ resizing: { state: true },
32
+ }
33
+
34
+ static styles = [unsafeCSS(tokens), unsafeCSS(styles)]
35
+
36
+ declare data: PageMetadata | undefined
37
+ declare width: number
38
+ declare resizing: boolean
39
+
40
+ constructor() {
41
+ super()
42
+ this.data = undefined
43
+ this.width = Number(localStorage.getItem(WIDTH_KEY)) || 480
44
+ this.resizing = false
45
+ }
46
+
47
+ close(): void {
48
+ this.dispatchEvent(new CustomEvent("close"))
49
+ }
50
+
51
+ /**
52
+ * Changes the width while the user drags the left edge.
53
+ */
54
+ startResize(event: PointerEvent): void {
55
+ const handle = event.currentTarget as HTMLElement
56
+ handle.setPointerCapture(event.pointerId)
57
+ this.resizing = true
58
+
59
+ const move = (moveEvent: PointerEvent) => {
60
+ const max = window.innerWidth - 40
61
+ this.width = Math.min(max, Math.max(MIN_WIDTH, window.innerWidth - moveEvent.clientX))
62
+ }
63
+ const stop = () => {
64
+ handle.removeEventListener("pointermove", move)
65
+ this.resizing = false
66
+ localStorage.setItem(WIDTH_KEY, String(this.width))
67
+ }
68
+ handle.addEventListener("pointermove", move)
69
+ handle.addEventListener("pointerup", stop, { once: true })
70
+ }
71
+
72
+ renderTabs() {
73
+ if (!this.data) return html`<p class="loading">Reading the metadata…</p>`
74
+
75
+ const { errors, warnings, level } = summarize(this.data.checks)
76
+ return html`
77
+ <astro-metadata-tab-group
78
+ selected=${localStorage.getItem(TAB_KEY) ?? "overview"}
79
+ @change=${(event: CustomEvent<string>) => {
80
+ localStorage.setItem(TAB_KEY, event.detail)
81
+ }}
82
+ >
83
+ <astro-metadata-tab
84
+ name="overview"
85
+ label="Overview"
86
+ count=${errors + warnings}
87
+ level=${level ?? nothing}
88
+ >
89
+ <astro-metadata-overview .data=${this.data}></astro-metadata-overview>
90
+ </astro-metadata-tab>
91
+ <astro-metadata-tab name="previews" label="Previews">
92
+ <astro-metadata-previews .data=${this.data}></astro-metadata-previews>
93
+ </astro-metadata-tab>
94
+ <astro-metadata-tab name="raw" label="Raw">
95
+ <astro-metadata-raw .data=${this.data}></astro-metadata-raw>
96
+ </astro-metadata-tab>
97
+ </astro-metadata-tab-group>
98
+ `
99
+ }
100
+
101
+ render() {
102
+ return html`
103
+ <aside class="panel" style=${styleMap({ "--panel-width": `${this.width}px` })}>
104
+ <div class="resize ${this.resizing ? "active" : ""}" @pointerdown=${this.startResize}></div>
105
+
106
+ <header class="header">
107
+ <div class="heading">
108
+ <div class="title">
109
+ <h1>Metadata</h1>
110
+ <astro-metadata-badge level="info">Beta</astro-metadata-badge>
111
+ </div>
112
+ <p class="path">${location.pathname}</p>
113
+ </div>
114
+ <button class="close" @click=${this.close} aria-label="Close the panel">
115
+ <astro-metadata-icon name="close"></astro-metadata-icon>
116
+ </button>
117
+ </header>
118
+
119
+ ${this.renderTabs()}
120
+ </aside>
121
+ `
122
+ }
123
+ }
124
+
125
+ define("astro-metadata-panel", MetadataPanel)
@@ -0,0 +1,27 @@
1
+ /* The alerts of the page: one for each level */
2
+ .alerts {
3
+ display: grid;
4
+ gap: var(--space-200);
5
+ }
6
+
7
+ /* The Open Graph image at the full width of the panel */
8
+ .image {
9
+ width: 100%;
10
+ aspect-ratio: 1.91 / 1;
11
+ border-radius: var(--radius);
12
+ background: var(--zinc-900);
13
+
14
+ &[missing] {
15
+ font-size: var(--font-size-small);
16
+ color: var(--zinc-400);
17
+ border: 1px dashed var(--zinc-800);
18
+ }
19
+ }
20
+
21
+ /* The name of a table in a section, e.g. "Open Graph" in the Social section */
22
+ .subheading {
23
+ margin: var(--space-200) 0 0;
24
+ font-size: var(--font-size-small);
25
+ font-weight: 500;
26
+ color: var(--zinc-300);
27
+ }
@@ -0,0 +1,401 @@
1
+ import { html, LitElement, nothing, unsafeCSS } from "lit"
2
+
3
+ import { LIMITS } from "../../lib/checks.js"
4
+ import { findMeta } from "../data.ts"
5
+ import type { Check, PageMetadata } from "../data.ts"
6
+ import shared from "../styles/shared.css?inline"
7
+
8
+ import "../ui/alert/alert.ts"
9
+ import "../ui/color-swatch/color-swatch.ts"
10
+ import "../ui/image/image.ts"
11
+
12
+ import { define } from "../define.ts"
13
+ import type { AlertItem } from "../ui/alert/alert.ts"
14
+ import type { Level } from "../ui/badge/badge.ts"
15
+ import { code, withCode } from "../ui/inline-code/inline-code.ts"
16
+
17
+ import "../ui/section/section.ts"
18
+ import "../ui/table/table.ts"
19
+
20
+ import type { Section } from "../ui/section/section.ts"
21
+ import type { Table, TableRow } from "../ui/table/table.ts"
22
+ import { manifestBlock, schemaBlocks } from "./code-blocks.ts"
23
+ import styles from "./overview-view.css?inline"
24
+
25
+ /** An icon file of the page: a link tag or an icon of the web app manifest. */
26
+ type IconFile = {
27
+ /** The path of the file, without the query, e.g. "/favicon-32.png". */
28
+ path: string
29
+ /** The URL to load the image. */
30
+ src: string
31
+ sizes?: string
32
+ type?: string
33
+ /** "icon", "apple-touch-icon" or "manifest". */
34
+ use: string
35
+ }
36
+
37
+ /** The part of a web app manifest that the Overview reads. */
38
+ type ManifestJson = {
39
+ icons?: { src: string; sizes?: string; type?: string }[]
40
+ }
41
+
42
+ /**
43
+ * Returns the size of an icon from its `sizes` value, e.g. 32 for "32x32".
44
+ * An icon without a size, e.g. an SVG, scales to any size, so it sorts last.
45
+ */
46
+ function sizeOf(file: IconFile): number {
47
+ const size = Number.parseInt(file.sizes ?? "", 10)
48
+ return Number.isNaN(size) ? Number.POSITIVE_INFINITY : size
49
+ }
50
+
51
+ /**
52
+ * Returns all icon files of the page, from the smallest to the largest.
53
+ */
54
+ function iconFiles(data: PageMetadata): IconFile[] {
55
+ const files: IconFile[] = data.icons.map((icon) => {
56
+ return {
57
+ path: icon.href.split("?")[0],
58
+ src: icon.href,
59
+ sizes: icon.sizes,
60
+ type: icon.type,
61
+ use: icon.rel,
62
+ }
63
+ })
64
+
65
+ const manifest = data.manifest
66
+ const manifestIcons = (manifest?.data as ManifestJson | undefined)?.icons ?? []
67
+ // Manifest icon paths are relative to the manifest URL
68
+ const base = new URL(manifest?.url ?? "", location.href)
69
+ for (const icon of manifestIcons) {
70
+ const url = new URL(icon.src, base)
71
+ files.push({
72
+ path: url.pathname,
73
+ src: url.href,
74
+ sizes: icon.sizes,
75
+ type: icon.type,
76
+ use: "manifest",
77
+ })
78
+ }
79
+
80
+ return files.sort((a, b) => {
81
+ return sizeOf(a) - sizeOf(b)
82
+ })
83
+ }
84
+
85
+ // The section of each field of a check. A field without a row in the section,
86
+ // e.g. "charset", opens only the section.
87
+ const FIELD_SECTIONS: Record<string, string> = {
88
+ "json-ld": "Structured data",
89
+ hreflang: "Locales",
90
+ }
91
+
92
+ /**
93
+ * Returns the section of a field: "Social" for the Open Graph and Twitter (X) tags,
94
+ * else "General".
95
+ */
96
+ function sectionOf(field: string): string {
97
+ if (FIELD_SECTIONS[field]) return FIELD_SECTIONS[field]
98
+ if (field.startsWith("og:") || field.startsWith("twitter:")) return "Social"
99
+ return "General"
100
+ }
101
+
102
+ /**
103
+ * Returns the short name of a feed type, e.g. "rss" for "application/rss+xml".
104
+ */
105
+ function feedFormat(type: string | undefined): string | undefined {
106
+ if (!type) return undefined
107
+ if (type.includes("atom")) return "atom"
108
+ if (type.includes("json")) return "json"
109
+ if (type.includes("rss")) return "rss"
110
+ return type
111
+ }
112
+
113
+ /**
114
+ * Returns a row with the tag name in the note. A row without a value gets no tag name.
115
+ */
116
+ function tagRow(label: string, value: TableRow["value"], tag: string): TableRow {
117
+ return { label, key: tag, value, note: value === undefined ? undefined : tag }
118
+ }
119
+
120
+ /**
121
+ * Returns the heading of an alert, e.g. "Error: 1" or "Errors: 3".
122
+ */
123
+ function heading(word: string, amount: number): string {
124
+ return `${word}${amount === 1 ? "" : "s"}: ${amount}`
125
+ }
126
+
127
+ /**
128
+ * Returns "1 file" or "3 files".
129
+ */
130
+ function count(amount: number, word: string): string {
131
+ return `${amount} ${word}${amount === 1 ? "" : "s"}`
132
+ }
133
+
134
+ /**
135
+ * Returns a table row with the length of a text against a limit. The length is yellow
136
+ * from 90 % of the limit, and orange over the limit.
137
+ */
138
+ function lengthRow(label: string, key: string, text: string | undefined, limit: number): TableRow {
139
+ if (!text) return { label, key }
140
+
141
+ let noteLevel: TableRow["noteLevel"]
142
+ if (text.length > limit) noteLevel = "over"
143
+ else if (text.length >= limit * 0.9) noteLevel = "near"
144
+ return { label, key, value: text, note: `${text.length} / ${limit}`, noteLevel }
145
+ }
146
+
147
+ /**
148
+ * The values of the page at one look: the status, then the values by category,
149
+ * in the order of their importance.
150
+ */
151
+ export class OverviewView extends LitElement {
152
+ static properties = { data: { attribute: false } }
153
+ static styles = [unsafeCSS(shared), unsafeCSS(styles)]
154
+
155
+ declare data: PageMetadata
156
+
157
+ /**
158
+ * Returns the Twitter (X) rows. Twitter (X) uses the Open Graph title, description and image,
159
+ * so these rows show only when the page has its own Twitter (X) value.
160
+ */
161
+ twitterRows(): TableRow[] {
162
+ const x = this.data.twitter
163
+ const rows = [
164
+ tagRow("Card", code(findMeta(x, "twitter:card")), "twitter:card"),
165
+ tagRow("Site", code(findMeta(x, "twitter:site")), "twitter:site"),
166
+ tagRow("Creator", code(findMeta(x, "twitter:creator")), "twitter:creator"),
167
+ ]
168
+ const overrides = [
169
+ tagRow("Title", findMeta(x, "twitter:title"), "twitter:title"),
170
+ tagRow("Description", findMeta(x, "twitter:description"), "twitter:description"),
171
+ tagRow("Image", findMeta(x, "twitter:image"), "twitter:image"),
172
+ ].filter((row) => {
173
+ return row.value !== undefined
174
+ })
175
+ return [...rows, ...overrides]
176
+ }
177
+
178
+ /**
179
+ * Opens the section of a field, and marks its row, e.g. after a click on a problem.
180
+ */
181
+ async showField(field: string): Promise<void> {
182
+ const section = this.renderRoot.querySelector<Section>(
183
+ `astro-metadata-section[heading="${sectionOf(field)}"]`
184
+ )
185
+ if (!section || section.empty) return
186
+
187
+ section.closed = false
188
+ await section.updateComplete
189
+ for (const table of section.querySelectorAll<Table>("astro-metadata-table")) {
190
+ if (await table.highlight(field)) return
191
+ }
192
+ section.scrollIntoView({ block: "start", behavior: "smooth" })
193
+ }
194
+
195
+ /**
196
+ * Returns an alert with the checks of one level, e.g. all errors.
197
+ */
198
+ renderAlert(level: Level, heading: string, checks: Check[]) {
199
+ const items: AlertItem[] = checks.map((check) => {
200
+ return { content: withCode(check.message), target: check.field }
201
+ })
202
+ return html`
203
+ <astro-metadata-alert
204
+ level=${level}
205
+ heading=${heading}
206
+ .items=${items}
207
+ @select=${(event: CustomEvent<string>) => {
208
+ this.showField(event.detail)
209
+ }}
210
+ ></astro-metadata-alert>
211
+ `
212
+ }
213
+
214
+ /**
215
+ * The status of the page: one alert for each level, with the errors first.
216
+ * Without errors and warnings, a green alert shows that all is good.
217
+ */
218
+ renderStatus() {
219
+ if (this.data.ignored) {
220
+ return html`
221
+ <div class="alerts">
222
+ <astro-metadata-alert
223
+ level="info"
224
+ heading="Checks are off for this page"
225
+ .items=${[{ content: withCode("The `pages.ignore` option of the integration matches this page.") }]}
226
+ ></astro-metadata-alert>
227
+ </div>
228
+ `
229
+ }
230
+
231
+ const checks = this.data.checks
232
+ const byLevel = (level: Check["level"]) => {
233
+ return checks.filter((check) => {
234
+ return check.level === level
235
+ })
236
+ }
237
+ const errors = byLevel("error")
238
+ const warnings = byLevel("warning")
239
+ const notes = byLevel("info")
240
+
241
+ // A noindex page is not a problem, but it is also not "all good"
242
+ const hidden = /noindex/i.test(this.data.robots ?? "")
243
+ const good = errors.length === 0 && warnings.length === 0 && !hidden
244
+
245
+ return html`
246
+ <div class="alerts">
247
+ ${errors.length > 0 ? this.renderAlert("error", heading("Error", errors.length), errors) : nothing}
248
+ ${
249
+ warnings.length > 0
250
+ ? this.renderAlert("warning", heading("Warning", warnings.length), warnings)
251
+ : nothing
252
+ }
253
+ ${good ? this.renderAlert("success", "No issues found", []) : nothing}
254
+ ${
255
+ notes.length > 0
256
+ ? this.renderAlert(
257
+ "info",
258
+ hidden ? "Hidden from search engines" : heading("Note", notes.length),
259
+ notes
260
+ )
261
+ : nothing
262
+ }
263
+ </div>
264
+ `
265
+ }
266
+
267
+ /**
268
+ * The favicon row: the logo, and the number of files. Its child rows list all icon
269
+ * files, from the smallest to the largest. The logo is the SVG, or else the largest file.
270
+ */
271
+ faviconRow(): TableRow {
272
+ const files = iconFiles(this.data)
273
+ if (files.length === 0) return { label: "Favicons", key: "favicon" }
274
+
275
+ const svg = files.find((file) => {
276
+ return file.type === "image/svg+xml"
277
+ })
278
+ const logo = svg ?? files[files.length - 1]
279
+
280
+ return {
281
+ label: "Favicons",
282
+ key: "favicon",
283
+ value: html`<span class="favicon"
284
+ ><img src=${logo.src} alt="" width="20" height="20"
285
+ /></span>`,
286
+ note: count(files.length, "file"),
287
+ children: files.map((file) => {
288
+ const size = sizeOf(file)
289
+ return {
290
+ label: Number.isFinite(size) ? `${size} × ${size}` : "Any size",
291
+ value: code(file.path),
292
+ note: file.use,
293
+ }
294
+ }),
295
+ }
296
+ }
297
+
298
+ renderLocales() {
299
+ const links = this.data.hreflang
300
+ if (links.length === 0) return nothing
301
+
302
+ const page = this.data.canonical
303
+ const rows: TableRow[] = links.map((link) => {
304
+ const fallback = link.hreflang === "x-default"
305
+ return {
306
+ label: fallback ? "Default" : (link.hreflang ?? ""),
307
+ labelCode: !fallback,
308
+ value: link.href,
309
+ // Shows which row is the current page, and what "Default" is
310
+ note: fallback ? "x-default" : link.href === page ? "this page" : undefined,
311
+ }
312
+ })
313
+ return html`<astro-metadata-table .rows=${rows}></astro-metadata-table>`
314
+ }
315
+
316
+ renderFeeds() {
317
+ const feeds = this.data.feeds
318
+ if (feeds.length === 0) return nothing
319
+
320
+ const rows: TableRow[] = feeds.map((feed) => {
321
+ return { label: feed.title ?? "Feed", value: code(feed.href), note: feedFormat(feed.type) }
322
+ })
323
+ return html`<astro-metadata-table .rows=${rows}></astro-metadata-table>`
324
+ }
325
+
326
+ render() {
327
+ const data = this.data
328
+ const og = data.openGraph
329
+ const image = findMeta(og, "og:image")
330
+ const width = findMeta(og, "og:image:width")
331
+ const height = findMeta(og, "og:image:height")
332
+
333
+ return html`
334
+ ${this.renderStatus()}
335
+
336
+ <astro-metadata-section heading="General">
337
+ <astro-metadata-table
338
+ .rows=${[
339
+ lengthRow("Title", "title", data.title, LIMITS.title),
340
+ lengthRow("Description", "description", data.description, LIMITS.description),
341
+ { label: "Canonical", key: "canonical", value: data.canonical },
342
+ { label: "Robots", key: "robots", value: code(data.robots ?? "index, follow") },
343
+ { label: "Language", key: "lang", value: code(data.lang) },
344
+ {
345
+ label: "Theme color",
346
+ key: "theme-color",
347
+ value:
348
+ data.themeColors.length > 0
349
+ ? html`${data.themeColors.map((theme) => {
350
+ return html`<astro-metadata-color-swatch color=${theme.color}>
351
+ </astro-metadata-color-swatch>`
352
+ })}`
353
+ : undefined,
354
+ },
355
+ this.faviconRow(),
356
+ ]}
357
+ ></astro-metadata-table>
358
+ </astro-metadata-section>
359
+
360
+ <astro-metadata-section heading="Social">
361
+ <astro-metadata-image class="image" .src=${image}></astro-metadata-image>
362
+ <h3 class="subheading">Open Graph</h3>
363
+ <astro-metadata-table
364
+ .rows=${[
365
+ tagRow("Title", findMeta(og, "og:title"), "og:title"),
366
+ tagRow("Description", findMeta(og, "og:description"), "og:description"),
367
+ tagRow("Type", code(findMeta(og, "og:type")), "og:type"),
368
+ tagRow("Site name", findMeta(og, "og:site_name"), "og:site_name"),
369
+ tagRow("Image", image, "og:image"),
370
+ tagRow(
371
+ "Image size",
372
+ width && height ? `${width} × ${height}` : undefined,
373
+ "og:image:width"
374
+ ),
375
+ tagRow("Image alt", findMeta(og, "og:image:alt"), "og:image:alt"),
376
+ ]}
377
+ ></astro-metadata-table>
378
+ <h3 class="subheading">Twitter (X)</h3>
379
+ <astro-metadata-table .rows=${this.twitterRows()}></astro-metadata-table>
380
+ </astro-metadata-section>
381
+
382
+ <astro-metadata-section heading="Locales" ?empty=${data.hreflang.length === 0}
383
+ >${this.renderLocales()}</astro-metadata-section
384
+ >
385
+
386
+ <astro-metadata-section heading="Structured data" ?empty=${data.schemas.length === 0}>
387
+ ${schemaBlocks(data.schemas)}
388
+ </astro-metadata-section>
389
+
390
+ <astro-metadata-section heading="Web app manifest" ?empty=${!data.manifest}>
391
+ ${manifestBlock(data.manifest)}
392
+ </astro-metadata-section>
393
+
394
+ <astro-metadata-section heading="Feeds" ?empty=${data.feeds.length === 0}
395
+ >${this.renderFeeds()}</astro-metadata-section
396
+ >
397
+ `
398
+ }
399
+ }
400
+
401
+ define("astro-metadata-overview", OverviewView)