@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.
- package/README.md +427 -82
- package/package.json +25 -14
- 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 +929 -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 +105 -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 -96
- package/src/components/Head.astro +0 -94
- 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 -18
- package/src/components/Title.astro +0 -13
- package/src/components/Twitter.astro +0 -27
- package/src/lib/metadata.ts +0 -77
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
* {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* The z-index of the Audit app window of Astro: above the site, e.g. a fixed navigation,
|
|
7
|
+
* and under the toolbar bar (2000000010). The toolbar element has no stacking context.
|
|
8
|
+
*/
|
|
9
|
+
.panel {
|
|
10
|
+
position: fixed;
|
|
11
|
+
inset: 0 0 0 auto;
|
|
12
|
+
z-index: 2000000009;
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
width: var(--panel-width, 480px);
|
|
16
|
+
max-width: 100vw;
|
|
17
|
+
background: var(--zinc-950);
|
|
18
|
+
border-left: 1px solid var(--zinc-800);
|
|
19
|
+
box-shadow: var(--shadow);
|
|
20
|
+
animation: slide-in 160ms ease-out;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@keyframes slide-in {
|
|
24
|
+
from {
|
|
25
|
+
transform: translateX(24px);
|
|
26
|
+
opacity: 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* The handle on the left edge: drag it to change the width */
|
|
31
|
+
.resize {
|
|
32
|
+
position: absolute;
|
|
33
|
+
inset: 0 auto 0 -3px;
|
|
34
|
+
width: 6px;
|
|
35
|
+
cursor: ew-resize;
|
|
36
|
+
|
|
37
|
+
&:hover,
|
|
38
|
+
&.active {
|
|
39
|
+
background: var(--zinc-700);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.header {
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: var(--space-300);
|
|
47
|
+
padding: var(--space-300) var(--space-400);
|
|
48
|
+
border-bottom: 1px solid var(--zinc-800);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.heading {
|
|
52
|
+
flex: 1;
|
|
53
|
+
min-width: 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* The title and the Beta badge */
|
|
57
|
+
.title {
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
gap: var(--space-200);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
h1 {
|
|
64
|
+
margin: 0;
|
|
65
|
+
font-size: var(--font-size-body);
|
|
66
|
+
font-weight: 600;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.path {
|
|
70
|
+
margin: 0;
|
|
71
|
+
overflow: hidden;
|
|
72
|
+
font-size: var(--font-size-small);
|
|
73
|
+
color: var(--zinc-400);
|
|
74
|
+
text-overflow: ellipsis;
|
|
75
|
+
white-space: nowrap;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
button {
|
|
79
|
+
font: inherit;
|
|
80
|
+
color: inherit;
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
background: none;
|
|
83
|
+
border: 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.close {
|
|
87
|
+
display: grid;
|
|
88
|
+
place-items: center;
|
|
89
|
+
width: 28px;
|
|
90
|
+
height: 28px;
|
|
91
|
+
border-radius: var(--radius-small);
|
|
92
|
+
|
|
93
|
+
&:hover {
|
|
94
|
+
background: var(--zinc-800);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
astro-metadata-tab-group {
|
|
99
|
+
flex: 1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.loading {
|
|
103
|
+
padding: var(--space-400);
|
|
104
|
+
color: var(--zinc-400);
|
|
105
|
+
}
|
|
@@ -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)
|