@cparkerwebm/webmonterey 1.1.0 → 1.3.0
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/CHANGELOG.md +104 -0
- package/README.md +6 -0
- package/dist/webm.mjs +445 -61
- package/package.json +5 -3
- package/skills/launch/SKILL.md +57 -9
- package/skills/start/SKILL.md +66 -12
- package/skills/traps/SKILL.md +17 -0
- package/src/assets/opengraph-webmaster.png +0 -0
- package/src/cli/audit.test.ts +120 -0
- package/src/cli/audit.ts +323 -0
- package/src/cli/checks.test.ts +46 -6
- package/src/cli/checks.ts +52 -12
- package/src/cli/doctor.ts +78 -2
- package/src/cli/scaffold.test.ts +9 -0
- package/src/cli/scaffold.ts +12 -30
- package/src/cli/settings.ts +106 -0
- package/src/cli/sync.test.ts +61 -0
- package/src/cli/sync.ts +64 -1
- package/src/emails/footer.ts +3 -3
- package/src/includes/cloudflare/r2/media.ts +1 -1
- package/src/includes/webmonterey/config.test.ts +51 -0
- package/src/includes/webmonterey/config.ts +49 -0
- package/src/includes/webmonterey/copy-defaults.ts +20 -0
- package/src/includes/webmonterey/webmaster/Webmaster.astro +52 -0
- package/src/includes/webmonterey/webmaster/webmaster.test.ts +74 -0
- package/src/includes/webmonterey/webmaster/webmaster.ts +89 -0
- package/src/integration/index.ts +91 -4
- package/src/integration/virtual.d.ts +19 -1
- package/src/layouts/base.astro +31 -8
- package/src/package.test.ts +20 -0
- package/src/pages/robots.txt.ts +14 -0
- package/src/pages/webmaster-og.png.ts +31 -0
- package/src/pages/webmaster.astro +121 -0
- package/template/public/opengraph.png +0 -0
- package/template/scripts/test-hooks.mjs +1 -1
- package/template/site/CLAUDE.md +25 -2
- package/src/includes/webmonterey/credits/Credit.astro +0 -80
- package/src/includes/webmonterey/credits/credit.test.ts +0 -111
- package/src/includes/webmonterey/credits/credit.ts +0 -59
- package/template/public/open-graph.png +0 -0
- /package/template/assets/{open-graph.png → opengraph.png} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cparkerwebm/webmonterey",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The WebMonterey Astro framework: plumbing, design system, and Claude Code skills for client sites on Cloudflare Workers",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,6 +49,8 @@
|
|
|
49
49
|
"./pages/slug": "./src/pages/[...slug].astro",
|
|
50
50
|
"./pages/robots": "./src/pages/robots.txt.ts",
|
|
51
51
|
"./pages/webm": "./src/pages/webm.astro",
|
|
52
|
+
"./pages/webmaster": "./src/pages/webmaster.astro",
|
|
53
|
+
"./pages/webmaster-og": "./src/pages/webmaster-og.png.ts",
|
|
52
54
|
"./pages/404.astro": "./src/pages/404.astro",
|
|
53
55
|
"./emails": "./src/emails/index.ts",
|
|
54
56
|
"./cloudflare/d1": "./src/includes/cloudflare/d1/client.ts",
|
|
@@ -64,8 +66,8 @@
|
|
|
64
66
|
"./webmonterey/compliance/ConsentInit.astro": "./src/includes/webmonterey/compliance/ConsentInit.astro",
|
|
65
67
|
"./webmonterey/forms": "./src/includes/webmonterey/forms/honeypot.ts",
|
|
66
68
|
"./webmonterey/prose": "./src/includes/webmonterey/prose/inline.ts",
|
|
67
|
-
"./webmonterey/
|
|
68
|
-
"./webmonterey/
|
|
69
|
+
"./webmonterey/webmaster": "./src/includes/webmonterey/webmaster/webmaster.ts",
|
|
70
|
+
"./webmonterey/webmaster/Webmaster.astro": "./src/includes/webmonterey/webmaster/Webmaster.astro",
|
|
69
71
|
"./webmonterey/scroll-top/ScrollTop.astro": "./src/includes/webmonterey/scroll-top/ScrollTop.astro",
|
|
70
72
|
"./structured-data": "./src/includes/webmonterey/structured-data/nodes.ts",
|
|
71
73
|
"./schema/design.json": "./schema/design.json",
|
package/skills/launch/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: launch
|
|
3
|
-
description: Launch a WebMonterey client site onto its real domain - structured data, Turnstile, sending domain, production secrets, remote migrations, custom domain, verification, and the environment flip. Use for "launch the site", "point the domain at it", "we're going live", "take it out of preview".
|
|
3
|
+
description: Launch a WebMonterey client site onto its real domain - build audit (alt text, links, sitemap), structured data, Turnstile, sending domain, production secrets, remote migrations, custom domain, verification, analytics confirmation, and the environment flip. Use for "launch the site", "point the domain at it", "we're going live", "take it out of preview".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Launch
|
|
@@ -15,9 +15,34 @@ npx webm doctor
|
|
|
15
15
|
npm run build
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Both clean. Doctor's warnings about placeholder artwork and the missing
|
|
18
|
+
Both clean. Doctor's warnings about placeholder artwork and the missing webmaster credit become
|
|
19
19
|
blocking here: replace the seeded favicons and share image with the client's own, and confirm
|
|
20
|
-
the footer imports `
|
|
20
|
+
the footer imports `Webmaster.astro` - it links to the `/webmaster` page every site has, and
|
|
21
|
+
without the credit that page is reachable from nothing.
|
|
22
|
+
|
|
23
|
+
## 1b. Audit the build
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npx webm audit
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
It reads `dist/client` and reports three things a person never checks exhaustively on a
|
|
30
|
+
forty-page site. Fix each and rebuild until it is clean:
|
|
31
|
+
|
|
32
|
+
- **Images with no `alt` attribute.** Write the alt text yourself: open the image, read the
|
|
33
|
+
copy around it, and describe what it shows in that context - not what it is ("photo"), what it
|
|
34
|
+
says. A purely decorative image gets `alt=""`, declared explicitly. The alt lives wherever the
|
|
35
|
+
`<img>` is authored: the component's markup, or the block's page JSON if the schema carries it.
|
|
36
|
+
Do not leave one for later; a missing alt on a launched site is an accessibility failure and a
|
|
37
|
+
lost image-search result.
|
|
38
|
+
- **Broken internal links.** Every `href` must land on a built page or a route in
|
|
39
|
+
`run_worker_first`. A typo'd slug, a page that was renamed, a form action that moved.
|
|
40
|
+
- **The sitemap.** Built, advertised in `robots.txt`, every URL on the production domain and
|
|
41
|
+
landing on a page. After launch, fetch `https://<domain>/sitemap-index.xml` in a browser and
|
|
42
|
+
submit it in Search Console.
|
|
43
|
+
|
|
44
|
+
External links are probed too and reported as warnings: many sites refuse anything that is not a
|
|
45
|
+
browser, so open each flagged one before calling it broken.
|
|
21
46
|
|
|
22
47
|
## 2. Structured data
|
|
23
48
|
|
|
@@ -159,10 +184,25 @@ curl -H "Sec-Fetch-Dest: document" -H "Sec-Fetch-Mode: navigate" https://<domain
|
|
|
159
184
|
That header is what the asset router branches on. Without it curl gets the real page and Chrome
|
|
160
185
|
gets the 404.
|
|
161
186
|
|
|
162
|
-
**A form test before step
|
|
187
|
+
**A form test before step 11 goes to `stagingEmail`** with `[staging → …]` in the subject naming
|
|
163
188
|
who it was really for. That is the system working - check that inbox, not the client's.
|
|
164
189
|
|
|
165
|
-
## 10.
|
|
190
|
+
## 10. Analytics - two confirmations, asked out loud
|
|
191
|
+
|
|
192
|
+
Neither of these can be checked from the repo, so **ask, and wait for the answer.** Do not
|
|
193
|
+
proceed on an assumption, and do not mark either done because the field is filled in.
|
|
194
|
+
|
|
195
|
+
1. **"Is Google Tag Manager configured for this site?"** - meaning the container exists, its
|
|
196
|
+
tags are published (not just saved), and `gtmId` in `webmonterey.json` is that container. A
|
|
197
|
+
`gtmId` with an unpublished container loads a script that fires nothing. If the site has no
|
|
198
|
+
analytics by agreement, confirm that instead and move on.
|
|
199
|
+
2. **"Has the launch annotation been added in Google Analytics?"** - a dated note on the
|
|
200
|
+
property for the launch, so the traffic change that follows has an explanation next to it
|
|
201
|
+
when someone looks a year from now.
|
|
202
|
+
|
|
203
|
+
Record the answers in the launch commit message.
|
|
204
|
+
|
|
205
|
+
## 11. Hand the site its email back, and record the launch
|
|
166
206
|
|
|
167
207
|
In one change:
|
|
168
208
|
|
|
@@ -171,13 +211,21 @@ In one change:
|
|
|
171
211
|
"launched": "YYYY-MM-DD"
|
|
172
212
|
```
|
|
173
213
|
|
|
214
|
+
**This flip is also what makes the site indexable, so it happens here and not before.** While
|
|
215
|
+
`environment` says `staging`, every build is a preview - every page noindex with no canonical,
|
|
216
|
+
no sitemap, robots.txt disallowing everything, no Google Tag Manager - on every hostname, `main`
|
|
217
|
+
included. Flip it before the custom domain is live and the `workers.dev` copy is what gets
|
|
218
|
+
indexed.
|
|
219
|
+
|
|
174
220
|
Until `environment` flips, every message the site sends is redirected to `stagingEmail` - correct
|
|
175
221
|
right up to the moment the domain is attached and wrong immediately after: the form keeps saying
|
|
176
222
|
thank you, the client's inbox stays empty, and the first anyone hears of it is a customer asking
|
|
177
|
-
why nobody called back.
|
|
178
|
-
|
|
223
|
+
why nobody called back. And a launched site left on `staging` is invisible to search: every page
|
|
224
|
+
noindex, no sitemap, `Disallow: /`. `webm doctor` fails a launched site still declared staging,
|
|
225
|
+
which is why both fields change together.
|
|
179
226
|
|
|
180
|
-
Anything served from `workers.dev` is still treated as staging whatever this says,
|
|
181
|
-
previews of the live site keep
|
|
227
|
+
Anything served from `workers.dev` is still treated as staging for MAIL whatever this says, and
|
|
228
|
+
any branch other than `main` still builds as a preview, so branch previews of the live site keep
|
|
229
|
+
redirecting and stay out of the index. That is deliberate.
|
|
182
230
|
|
|
183
231
|
Commit, push, and confirm the deploy. Then run `npx webm doctor` one last time: zero failures.
|
package/skills/start/SKILL.md
CHANGED
|
@@ -97,27 +97,81 @@ like a bot does.
|
|
|
97
97
|
**Every route with `export const prerender = false` goes in `run_worker_first`, in both slash
|
|
98
98
|
forms.** Miss one and it returns 200 to curl and a 404 page to Chrome. `webm doctor` checks.
|
|
99
99
|
|
|
100
|
-
## 5.
|
|
100
|
+
## 5. Create the Worker - once, from the laptop
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
The Worker name must be the slug exactly as `wrangler.jsonc` has it - Workers Builds fails on a
|
|
104
|
-
mismatch.
|
|
102
|
+
Guarded and re-runnable: skip this step when the Worker already exists.
|
|
105
103
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
```sh
|
|
105
|
+
npx wrangler deployments list --name <slug> # a list: it exists. "does not exist [code: 10007]": create it
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The name is `name` in `wrangler.jsonc`. When there is nothing:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
npm run build # a real build; `dev` proves nothing here
|
|
112
|
+
npx wrangler deploy
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**This is the ONLY laptop deploy a site ever gets.** Before Workers Builds is connected, one
|
|
116
|
+
deploy is exactly how the Worker comes into existence. After it is connected (step 6) a laptop
|
|
117
|
+
deploy is the mistake: a version no build produced, which the next push reverts. This skill used
|
|
118
|
+
to stop here and say to create the Worker in the dashboard by hand, and on one site nobody did -
|
|
119
|
+
the result was a repo, a D1 database and nothing serving. `webm doctor` now warns on that.
|
|
120
|
+
|
|
121
|
+
The deploy provisions something `wrangler.jsonc` does not name, and that is expected: the Astro
|
|
122
|
+
Cloudflare adapter adds a `SESSION` KV binding, which wrangler auto-provisions as
|
|
123
|
+
`<slug>-session`. Do not add it to the config by hand.
|
|
124
|
+
|
|
125
|
+
Then verify. Wait a minute first - a brand-new Worker can return `error code: 1042` on valid
|
|
126
|
+
paths for about that long. In a real browser with the console open, load
|
|
127
|
+
`https://<slug>.<account>.workers.dev`: the home page renders, the console is clean. Then check
|
|
128
|
+
one on-demand route with a document-style request, if the site has one yet - a fresh scaffold's
|
|
129
|
+
only Worker route is the form action, which is POST-only:
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
curl -sI -H 'Sec-Fetch-Dest: document' https://<slug>.<account>.workers.dev/<on-demand-route>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The asset router keys off that header and plain curl does not send it, so a route that is 200 to
|
|
136
|
+
`curl` and 404 here is missing from `run_worker_first` (step 4).
|
|
137
|
+
|
|
138
|
+
## 6. Connect the repo
|
|
109
139
|
|
|
110
|
-
|
|
140
|
+
In the dashboard, on the Worker that now exists: **Settings → Builds → connect `<org>/<slug>`**,
|
|
141
|
+
production branch `main`, no build variables. Connecting a repo to an EXISTING Worker is the
|
|
142
|
+
smaller step - importing a repository and typing the Worker name in by hand is where the name
|
|
143
|
+
mismatch Workers Builds fails on comes from.
|
|
111
144
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
145
|
+
**Push to deploy from then on.** The first push supersedes the laptop version. A `wrangler
|
|
146
|
+
deploy` from a laptop after this point creates a version no build produced, so history stops
|
|
147
|
+
describing what is live and the next push reverts it.
|
|
115
148
|
|
|
116
|
-
|
|
149
|
+
Confirm it took:
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
npx wrangler deployments list --name <slug>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The newest deployment's Source is no longer `Upload`.
|
|
156
|
+
|
|
157
|
+
## 7. Verify the first push
|
|
158
|
+
|
|
159
|
+
Wait a minute after the build reports success - the 1042 window again - then load the
|
|
160
|
+
`workers.dev` URL in a real browser with the console open. The home page renders, the console is
|
|
161
|
+
clean.
|
|
162
|
+
|
|
163
|
+
## 8. Hand over
|
|
117
164
|
|
|
118
165
|
Workers Builds comments the preview URL on every PR - that is the client's review link. Preview
|
|
119
166
|
hostnames use the slug, so Chrome's lookalike warning should not appear; if it does, it is a
|
|
120
167
|
URL-shape false positive and **Ignore is safe**.
|
|
121
168
|
|
|
169
|
+
A preview build is safe to hand out: every page is noindex, there is no sitemap, robots.txt
|
|
170
|
+
disallows everything, analytics does not load, and mail is redirected to `stagingEmail`. The
|
|
171
|
+
client can click anything. **Every build of this site is a preview while `environment` is
|
|
172
|
+
`staging`** - `main` and the laptop included - so nothing is indexable before `/webm:launch`
|
|
173
|
+
flips it, and that flip is what makes the site indexable. On a launched site the production
|
|
174
|
+
branch is `main`; anything else previews.
|
|
175
|
+
|
|
122
176
|
Next: `/webm:new-component` for each block, then `/webm:launch` when the site is
|
|
123
177
|
content-complete and approved on a preview.
|
package/skills/traps/SKILL.md
CHANGED
|
@@ -134,6 +134,15 @@ _Worker_, not a _version_, so it can only serve production, and per-version prev
|
|
|
134
134
|
**`workers_dev: false` does NOT disable preview URLs.** Separate switches, which is why
|
|
135
135
|
wrangler.jsonc sets both explicitly.
|
|
136
136
|
|
|
137
|
+
**A branch preview is a different build, on purpose.** Workers Builds injects
|
|
138
|
+
`WORKERS_CI_BRANCH`; on any branch but the production one (`main`, or the integration's
|
|
139
|
+
`productionBranch`) every page is noindex with no canonical, there is no sitemap, `robots.txt`
|
|
140
|
+
disallows everything, GTM does not load, and the `/webmaster` page emits no graph. Email is
|
|
141
|
+
already redirected by the `workers.dev` hostname. So a client's review link can neither be
|
|
142
|
+
indexed nor show up in their analytics. **A local build is not a preview** - `npm run preview`
|
|
143
|
+
builds production, which is what you want to inspect. To see the preview shape locally:
|
|
144
|
+
`WORKERS_CI_BRANCH=x npm run build`.
|
|
145
|
+
|
|
137
146
|
**`wrangler deployments list` misleads twice.** `Source: Unknown (deployment)` appears even for
|
|
138
147
|
Workers Builds deployments — it describes the author, not the origin. And the list pages at 10
|
|
139
148
|
entries, so "has the count gone up?" can never become a way of waiting for a deploy.
|
|
@@ -320,6 +329,14 @@ divert every enquiry the day a site answers on `www.`.
|
|
|
320
329
|
nightly sweep — the check that reads a hostname cannot see a cron at all. This is why the switch
|
|
321
330
|
is config rather than something derived from the URL.
|
|
322
331
|
|
|
332
|
+
**A staging site is a preview build everywhere, and flipping `environment` is what makes it
|
|
333
|
+
indexable.** On `staging` every build — a branch, `main`, a laptop — is noindex on every page with
|
|
334
|
+
no canonical, no sitemap, `Disallow: /` and no Google Tag Manager. So a site that has not launched
|
|
335
|
+
cannot be indexed on its `workers.dev` URL, and a launched site left on `staging` after 1.3.0
|
|
336
|
+
drops out of search with no symptom on the page. `webm doctor` fails the second; `/webm:launch`
|
|
337
|
+
flips the switch only once the custom domain is live. A branch other than `main` previews
|
|
338
|
+
regardless of the switch.
|
|
339
|
+
|
|
323
340
|
**Check the client's existing DMARC before adding a sending subdomain.** A DMARC record on
|
|
324
341
|
`example.com` applies to its subdomains by default. If the client publishes `p=reject` and DKIM on
|
|
325
342
|
the new subdomain is not right, **every message vanishes** — no bounce, no error, nothing in the
|
|
Binary file
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { attr, audit, auditSitemap, hasAttr, imagesWithoutAlt, links, resolves } from './audit.ts';
|
|
4
|
+
|
|
5
|
+
const build = (
|
|
6
|
+
files: Record<string, string>,
|
|
7
|
+
extra: Partial<Parameters<typeof audit>[0]> = {},
|
|
8
|
+
) => ({
|
|
9
|
+
pages: new Map(Object.entries(files).filter(([k]) => k.endsWith('.html'))),
|
|
10
|
+
exists: (rel: string) => rel in files,
|
|
11
|
+
read: (rel: string) => files[rel] ?? null,
|
|
12
|
+
workerFirst: ['/_actions/*'],
|
|
13
|
+
origin: 'https://example.com',
|
|
14
|
+
...extra,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('attr reads quoted and bare values, and hasAttr sees a valueless attribute', () => {
|
|
18
|
+
assert.equal(attr('<img src="a.png" alt="A thing">', 'alt'), 'A thing');
|
|
19
|
+
assert.equal(attr("<img src='a.png' alt=''>", 'alt'), '');
|
|
20
|
+
assert.equal(attr('<img src=a.png>', 'src'), 'a.png');
|
|
21
|
+
assert.equal(attr('<img src="a.png">', 'alt'), null);
|
|
22
|
+
assert.equal(hasAttr('<img src="a.png" alt>', 'alt'), true);
|
|
23
|
+
assert.equal(hasAttr('<img src="a.png" data-alt="x">', 'alt'), false, 'data-alt is not alt');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('an image with no alt attribute is reported; an empty alt is a decorative declaration', () => {
|
|
27
|
+
const html = `<img src="/hero.jpg"><img src="/deco.svg" alt=""><img alt="Fine" src="/ok.png"><IMG SRC="/caps.png">`;
|
|
28
|
+
assert.deepEqual(imagesWithoutAlt(html), ['/hero.jpg', '/caps.png']);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('links are split into internal paths and external URLs, skipping non-navigations', () => {
|
|
32
|
+
const html = `
|
|
33
|
+
<a href="/about">a</a> <a href="/about/">b</a> <a href="mailto:x@y.z">m</a> <a href="#top">t</a>
|
|
34
|
+
<a href="https://example.com/contact">own origin</a> <a href="https://other.test/x">ext</a>
|
|
35
|
+
<a href="//cdn.test/file">proto-relative</a> <a href="tel:+1">p</a>`;
|
|
36
|
+
const { internal, external } = links(html, 'https://example.com');
|
|
37
|
+
assert.deepEqual(internal.sort(), ['/about', '/about/', '/contact']);
|
|
38
|
+
assert.deepEqual(external.sort(), ['https://cdn.test/file', 'https://other.test/x']);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('an internal link resolves to a built file in any of the forms the router serves', () => {
|
|
42
|
+
const input = build({
|
|
43
|
+
'index.html': '',
|
|
44
|
+
'about/index.html': '',
|
|
45
|
+
'privacy.html': '',
|
|
46
|
+
'a.pdf': '',
|
|
47
|
+
});
|
|
48
|
+
for (const href of [
|
|
49
|
+
'/',
|
|
50
|
+
'/about',
|
|
51
|
+
'/about/',
|
|
52
|
+
'/about#team',
|
|
53
|
+
'/about?x=1',
|
|
54
|
+
'/privacy',
|
|
55
|
+
'/a.pdf',
|
|
56
|
+
]) {
|
|
57
|
+
assert.equal(resolves(href, input), true, href);
|
|
58
|
+
}
|
|
59
|
+
assert.equal(resolves('/missing', input), false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('an on-demand route in run_worker_first is not reported as broken', () => {
|
|
63
|
+
const input = build(
|
|
64
|
+
{ 'index.html': '' },
|
|
65
|
+
{ workerFirst: ['/_actions/*', '/contact', '/contact/', '/app/*'] },
|
|
66
|
+
);
|
|
67
|
+
assert.equal(resolves('/contact', input), true);
|
|
68
|
+
assert.equal(resolves('/contact/', input), true);
|
|
69
|
+
assert.equal(resolves('/app/dashboard', input), true);
|
|
70
|
+
assert.equal(resolves('/app', input), true);
|
|
71
|
+
assert.equal(resolves('/elsewhere', input), false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('the sitemap must exist, be advertised, and every URL must land', () => {
|
|
75
|
+
const ok = build({
|
|
76
|
+
'index.html': '',
|
|
77
|
+
'about/index.html': '',
|
|
78
|
+
'robots.txt': 'User-agent: *\nAllow: /\n\nSitemap: https://example.com/sitemap-index.xml\n',
|
|
79
|
+
'sitemap-index.xml':
|
|
80
|
+
'<sitemapindex><sitemap><loc>https://example.com/sitemap-0.xml</loc></sitemap></sitemapindex>',
|
|
81
|
+
'sitemap-0.xml':
|
|
82
|
+
'<urlset><url><loc>https://example.com/</loc></url><url><loc>https://example.com/about/</loc></url></urlset>',
|
|
83
|
+
});
|
|
84
|
+
assert.deepEqual(auditSitemap(ok), { problems: [], urls: 2 });
|
|
85
|
+
|
|
86
|
+
const bad = build({
|
|
87
|
+
'index.html': '',
|
|
88
|
+
'robots.txt': 'User-agent: *\n',
|
|
89
|
+
'sitemap-index.xml':
|
|
90
|
+
'<sitemapindex><sitemap><loc>https://example.com/sitemap-0.xml</loc></sitemap></sitemapindex>',
|
|
91
|
+
'sitemap-0.xml':
|
|
92
|
+
'<urlset><url><loc>https://example.com/gone/</loc></url><url><loc>https://wrong.test/</loc></url></urlset>',
|
|
93
|
+
});
|
|
94
|
+
const { problems } = auditSitemap(bad);
|
|
95
|
+
assert.ok(problems.some((p) => p.includes('robots.txt')));
|
|
96
|
+
assert.ok(problems.some((p) => p.includes('/gone/') && p.includes('no page')));
|
|
97
|
+
assert.ok(problems.some((p) => p.includes('wrong.test')));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('a missing sitemap says why when the domain is unset', () => {
|
|
101
|
+
const { problems } = auditSitemap(build({ 'index.html': '' }, { origin: undefined }));
|
|
102
|
+
assert.match(problems[0]!, /domain/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('audit assembles every finding with the page it was found on', () => {
|
|
106
|
+
const report = audit(
|
|
107
|
+
build({
|
|
108
|
+
'index.html': '<img src="/x.png"><a href="/nope">n</a><a href="https://ext.test/">e</a>',
|
|
109
|
+
'about/index.html': '<img src="/y.png" alt="Y"><a href="/">home</a>',
|
|
110
|
+
'robots.txt': 'Sitemap: https://example.com/sitemap-index.xml',
|
|
111
|
+
'sitemap-index.xml':
|
|
112
|
+
'<sitemapindex><sitemap><loc>https://example.com/sitemap-0.xml</loc></sitemap></sitemapindex>',
|
|
113
|
+
'sitemap-0.xml': '<urlset><url><loc>https://example.com/</loc></url></urlset>',
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
assert.deepEqual(report.missingAlt, [{ page: 'index.html', src: '/x.png' }]);
|
|
117
|
+
assert.deepEqual(report.brokenInternal, [{ page: 'index.html', href: '/nope' }]);
|
|
118
|
+
assert.deepEqual(report.external, ['https://ext.test/']);
|
|
119
|
+
assert.deepEqual(report.sitemap.problems, []);
|
|
120
|
+
});
|