@gusnips/vite 0.4.5 → 0.4.7

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 CHANGED
@@ -111,6 +111,19 @@ text-fitting maths. When copy does not fit, it **records the overflow and refuse
111
111
  rather than appending an ellipsis — an ellipsis makes every string "fit", so copy that outgrew
112
112
  its column has no failing case and ships.
113
113
 
114
+ `assertOgImages("dist", origin)` reads every page the build wrote and checks that each share card
115
+ it advertises is a file that exists. Run it last:
116
+
117
+ ```ts
118
+ await assertOgImages("dist", "https://example.com");
119
+ ```
120
+
121
+ Three codebases shipped a `404.html` whose `og:image` named a card nothing had ever rendered —
122
+ the card generator walks the page registry, and a not-found shell is not in it. Every share of a
123
+ dead link unfurled broken, and nothing in a browser shows it. `bakeHead` cannot catch this: it
124
+ writes the image it is handed, and a shell carrying the brand card is correct. The build can,
125
+ because by then the card is on disk or it is not. Cards on another host are skipped.
126
+
114
127
  `loadTemplate` refuses a `dist/index.html` that is already a rendered page. `dist/index.html` is
115
128
  both the file every page is baked _from_ and the home page's own output, so a second pass over a
116
129
  written `dist/` reads a finished page as its blank and nests one render inside another. `vite
@@ -1 +1 @@
1
- {"version":3,"file":"head.d.ts","sourceRoot":"","sources":["../src/head.ts"],"names":[],"mappings":"AAiBA,4FAA4F;AAC5F,eAAO,MAAM,UAAU,4BAA0B,CAAC;AAgClD,uFAAuF;AACvF,MAAM,WAAW,SAAS;IACxB;yBACqB;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,wDAAwD;IACxD,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;2DACuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;gDAC4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IAClC;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACxC;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG5C;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,MAAM,CAmGjE;AAED,MAAM,WAAW,cAAc;IAC7B,2FAA2F;IAC3F,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;uDACmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,cAAmB,GAC1B,IAAI,CAqCN"}
1
+ {"version":3,"file":"head.d.ts","sourceRoot":"","sources":["../src/head.ts"],"names":[],"mappings":"AAiBA,4FAA4F;AAC5F,eAAO,MAAM,UAAU,4BAA0B,CAAC;AAmDlD,uFAAuF;AACvF,MAAM,WAAW,SAAS;IACxB;yBACqB;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,wDAAwD;IACxD,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;2DACuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;gDAC4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IAClC;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACxC;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG5C;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,MAAM,CAmGjE;AAED,MAAM,WAAW,cAAc;IAC7B,2FAA2F;IAC3F,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;uDACmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAWD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,cAAmB,GAC1B,IAAI,CA2CN"}
package/dist/head.js CHANGED
@@ -30,7 +30,23 @@ function setMeta(html, selector, value) {
30
30
  return html.replace(re, `$1${escapeAttr(value)}$2`);
31
31
  }
32
32
  /**
33
- * The same, for a tag a template is allowed not to carry.
33
+ * The same regex, for a key a template may spell with EITHER attribute.
34
+ *
35
+ * The `twitter:` family is written both ways in the wild and both work: X's own documentation
36
+ * says `name=`, the Open Graph spec says `property=`, and two adopters' templates carry
37
+ * `name="twitter:card"` in the same head as `property="twitter:title"`. Matching one spelling
38
+ * silently skips the other — and these tags go through `setMetaIfPresent`, where "skipped" is
39
+ * not an error but the intended behaviour for a template that does not have them.
40
+ *
41
+ * The fourth adopter spells the whole family `name=`. Baked with the narrow regex, every one of
42
+ * its 45 prerendered files — eleven pages in three languages — would have kept the template's
43
+ * card title and description, which is the front door's, and nothing in a browser shows it.
44
+ * That is the exact failure this file exists to prevent, arriving through the package rather
45
+ * than despite it.
46
+ */
47
+ const eitherRe = (key) => new RegExp(`(<meta[^>]*(?:name|property)="${escapeRegex(key)}"[^>]*content=")[^"]*(")`);
48
+ /**
49
+ * Replace the `content` of a tag a template is allowed not to carry.
34
50
  *
35
51
  * Only the `twitter:` tags, `og:image:alt` and `name="title"` get this. X reads the `og:` tags when
36
52
  * the `twitter:` ones are absent, no crawler reads `name="title"` at all, and most templates carry
@@ -38,9 +54,10 @@ function setMeta(html, selector, value) {
38
54
  * that never had them. A template that DOES carry one and gets a stale value is still a bug, which
39
55
  * is why they are written rather than ignored. Everything a crawler actually depends on goes
40
56
  * through `setMeta`.
57
+ *
58
+ * Takes the regex rather than the selector, so each caller picks how strict the spelling is.
41
59
  */
42
- function setMetaIfPresent(html, selector, value) {
43
- const re = metaRe(selector);
60
+ function setMetaIfPresent(html, re, value) {
44
61
  return re.test(html) ? html.replace(re, `$1${escapeAttr(value)}$2`) : html;
45
62
  }
46
63
  /**
@@ -71,16 +88,16 @@ export function bakeHead(template, tags) {
71
88
  html = setMeta(html, 'name="description"', tags.description);
72
89
  html = setMeta(html, 'property="og:title"', shareTitle);
73
90
  html = setMeta(html, 'property="og:description"', shareDescription);
74
- html = setMetaIfPresent(html, 'name="title"', tags.title);
75
- html = setMetaIfPresent(html, 'property="twitter:title"', shareTitle);
76
- html = setMetaIfPresent(html, 'property="twitter:description"', shareDescription);
91
+ html = setMetaIfPresent(html, metaRe('name="title"'), tags.title);
92
+ html = setMetaIfPresent(html, eitherRe("twitter:title"), shareTitle);
93
+ html = setMetaIfPresent(html, eitherRe("twitter:description"), shareDescription);
77
94
  if (tags.image !== undefined) {
78
95
  html = setMeta(html, 'property="og:image"', tags.image);
79
- html = setMetaIfPresent(html, 'property="twitter:image"', tags.image);
96
+ html = setMetaIfPresent(html, eitherRe("twitter:image"), tags.image);
80
97
  // A card's description goes with the card. Left alone, every page's own card was described
81
98
  // with the front page's title — the stale value this function exists to prevent, on the one
82
99
  // tag written for somebody who cannot see the picture.
83
- html = setMetaIfPresent(html, 'property="og:image:alt"', shareTitle);
100
+ html = setMetaIfPresent(html, eitherRe("og:image:alt"), shareTitle);
84
101
  }
85
102
  if (tags.canonical === null) {
86
103
  // Strip rather than blank: an empty canonical is a claim about "" and an empty og:url is a
@@ -88,11 +105,11 @@ export function bakeHead(template, tags) {
88
105
  // claim about this address, and a dead link that still unfurls the brand card is fine.
89
106
  html = html
90
107
  .replace(/\s*<link rel="canonical"[^>]*>/, "")
91
- .replace(/\s*<meta property="(?:og|twitter):url"[^>]*>/g, "");
108
+ .replace(/\s*<meta[^>]*(?:name|property)="(?:og|twitter):url"[^>]*>/g, "");
92
109
  }
93
110
  else {
94
111
  html = setMeta(html, 'property="og:url"', tags.canonical);
95
- html = setMetaIfPresent(html, 'property="twitter:url"', tags.canonical);
112
+ html = setMetaIfPresent(html, eitherRe("twitter:url"), tags.canonical);
96
113
  // Demanded, like every meta tag a crawler depends on. `String.replace` with no match is a
97
114
  // silent no-op, so a template that never had a canonical would get one on no page at all
98
115
  // and say nothing about it — the exact failure `setMeta` exists to turn into a red build.
@@ -144,6 +161,14 @@ export function bakeHead(template, tags) {
144
161
  }
145
162
  return html;
146
163
  }
164
+ /**
165
+ * Past this much growth the file has a page in it, whatever else it also contains.
166
+ *
167
+ * It is what lets the loading-screen search below look at a whole slice rather than one tag: a
168
+ * real page that opens with a live region ("Saved", a connection banner) is far over this, and a
169
+ * splash never is. The donor's was 1,174 bytes.
170
+ */
171
+ const SPLASH_MAX_GROWTH = 2000;
147
172
  /**
148
173
  * What every written file must be true of before the build is allowed to pass.
149
174
  *
@@ -195,7 +220,13 @@ export function assertRendered(file, html, template, checks = {}) {
195
220
  .trimStart();
196
221
  if (!/^<[a-z]/.test(head))
197
222
  fail("has no element inside its root");
198
- if (/^<[^>]*role="status"/.test(head))
223
+ // Anywhere in that bounded slice, not only its first tag, and only while the file is small
224
+ // enough to BE a splash. Anchored at the root's first element this missed the fourth adopter's
225
+ // loading screen, which centres its live region inside a `<main>` — one element deeper than the
226
+ // donor's, and an ordinary-looking wrapper to a test that reads one tag. That adopter's own
227
+ // guard missed it from the other side: it failed a file containing a spinner class its splash
228
+ // does not use. Two checks written for one regression, neither of which could ever fire.
229
+ if (grew < SPLASH_MAX_GROWTH && head.includes('role="status"'))
199
230
  fail("rendered the loading screen, not the page — something suspended and never resolved");
200
231
  }
201
232
  //# sourceMappingURL=head.js.map
package/dist/head.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"head.js","sourceRoot":"","sources":["../src/head.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"head.js","sourceRoot":"","sources":["../src/head.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -17,7 +17,7 @@
17
17
  */
18
18
  export { resolveKey } from "./catalog.ts";
19
19
  export { assertRendered, bakeHead, EMPTY_ROOT, ogLocale, type Alternate, type HeadTags, type RenderedChecks, } from "./head.ts";
20
- export { loadRenderer, loadTemplate, writeDist, writeOgCards, type OgCard, type WriteOgCardsOptions, } from "./node.ts";
20
+ export { assertOgImages, loadRenderer, loadTemplate, writeDist, writeOgCards, type OgCard, type WriteOgCardsOptions, } from "./node.ts";
21
21
  export { describeOverflow, fitText, OG_CANVAS, type FitOptions, type FitResult, type OgOverflow, } from "./og.ts";
22
22
  export type { PageRenderer } from "./render.ts";
23
23
  export { ogImagePath, pageFile, pageSlug, robotsTxt, siteOrigin, sitemapFor, sitemapXml, type PublicPage, type RobotsOptions, type SitemapEntry, } from "./sitemap.ts";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EACL,cAAc,EACd,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,cAAc,GACpB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,YAAY,EACZ,KAAK,MAAM,EACX,KAAK,mBAAmB,GACzB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,gBAAgB,EAChB,OAAO,EACP,SAAS,EACT,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,UAAU,GAChB,MAAM,SAAS,CAAC;AAKjB,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,UAAU,EACV,UAAU,EACV,UAAU,EACV,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EACL,cAAc,EACd,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,cAAc,GACpB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,YAAY,EACZ,KAAK,MAAM,EACX,KAAK,mBAAmB,GACzB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,gBAAgB,EAChB,OAAO,EACP,SAAS,EACT,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,UAAU,GAChB,MAAM,SAAS,CAAC;AAKjB,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,UAAU,EACV,UAAU,EACV,UAAU,EACV,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@
17
17
  */
18
18
  export { resolveKey } from "./catalog.js";
19
19
  export { assertRendered, bakeHead, EMPTY_ROOT, ogLocale, } from "./head.js";
20
- export { loadRenderer, loadTemplate, writeDist, writeOgCards, } from "./node.js";
20
+ export { assertOgImages, loadRenderer, loadTemplate, writeDist, writeOgCards, } from "./node.js";
21
21
  export { describeOverflow, fitText, OG_CANVAS, } from "./og.js";
22
22
  export { ogImagePath, pageFile, pageSlug, robotsTxt, siteOrigin, sitemapFor, sitemapXml, } from "./sitemap.js";
23
23
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EACL,cAAc,EACd,QAAQ,EACR,UAAU,EACV,QAAQ,GAIT,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,YAAY,GAGb,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,gBAAgB,EAChB,OAAO,EACP,SAAS,GAIV,MAAM,SAAS,CAAC;AAMjB,OAAO,EACL,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,UAAU,EACV,UAAU,EACV,UAAU,GAIX,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EACL,cAAc,EACd,QAAQ,EACR,UAAU,EACV,QAAQ,GAIT,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,YAAY,GAGb,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,gBAAgB,EAChB,OAAO,EACP,SAAS,GAIV,MAAM,SAAS,CAAC;AAMjB,OAAO,EACL,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,UAAU,EACV,UAAU,EACV,UAAU,GAIX,MAAM,cAAc,CAAC"}
package/dist/node.d.ts CHANGED
@@ -54,4 +54,28 @@ export interface WriteOgCardsOptions<Page> {
54
54
  * operator round the loop six times.
55
55
  */
56
56
  export declare function writeOgCards<Page>({ pages, outDir, file, card, check, }: WriteOgCardsOptions<Page>): Promise<string[]>;
57
+ /**
58
+ * Every share card a built page advertises is a file that exists.
59
+ *
60
+ * Three repos shipped a page whose `og:image` named a card nothing had ever rendered. All three
61
+ * were `404.html`, all three for the same reason: a card generator walks the page registry, and
62
+ * a not-found shell is not in the registry, so a head built from the same template inherits a
63
+ * card address with no card behind it. Every share of a dead link unfurled broken, in one case
64
+ * in three languages, and **nothing in a browser shows it** — the page looks perfect, and the
65
+ * only reader who finds out is whoever the link was sent to.
66
+ *
67
+ * `bakeHead` cannot catch it: it is handed an `image` and writes it, and a shell carrying the
68
+ * brand card is correct, so it has no way to tell a right image from a wrong one (invariant 12).
69
+ * The build can, because by then the cards are either on disk or they are not. Run it last,
70
+ * after the pages and the cards are written:
71
+ *
72
+ * ```ts
73
+ * await assertOgImages("dist", "https://example.com");
74
+ * ```
75
+ *
76
+ * Cards on another host are somebody else's to serve and are skipped — but a build whose pages
77
+ * all advertise cards and whose origin matches NONE of them has been given the wrong origin,
78
+ * and a check that silently passes is worse than no check, so that is an error too.
79
+ */
80
+ export declare function assertOgImages(distDir: string, origin: string): Promise<number>;
57
81
  //# sourceMappingURL=node.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAUnE;AAWD;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,OAAO,GAAG,OAAO,EAClD,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAOhC;AAED,8EAA8E;AAC9E,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI9F;AAED,yEAAyE;AACzE,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,UAAU,CAAC;IAChB;gCAC4B;IAC5B,QAAQ,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,mBAAmB,CAAC,IAAI;IACvC,KAAK,EAAE,SAAS,IAAI,EAAE,CAAC;IACvB;+CAC2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC;IAC7B,+FAA+F;IAC/F,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/C,+DAA+D;IAC/D,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,EACvC,KAAK,EACL,MAAM,EACN,IAAI,EACJ,IAAI,EACJ,KAAa,GACd,EAAE,mBAAmB,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAsB/C"}
1
+ {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAUnE;AAWD;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,OAAO,GAAG,OAAO,EAClD,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAOhC;AAED,8EAA8E;AAC9E,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI9F;AAED,yEAAyE;AACzE,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,UAAU,CAAC;IAChB;gCAC4B;IAC5B,QAAQ,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,mBAAmB,CAAC,IAAI;IACvC,KAAK,EAAE,SAAS,IAAI,EAAE,CAAC;IACvB;+CAC2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC;IAC7B,+FAA+F;IAC/F,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/C,+DAA+D;IAC/D,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,EACvC,KAAK,EACL,MAAM,EACN,IAAI,EACJ,IAAI,EACJ,KAAa,GACd,EAAE,mBAAmB,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAsB/C;AAgDD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAiCrF"}
package/dist/node.js CHANGED
@@ -19,11 +19,12 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
19
19
  * policy the product owns. What is identical in every one of them is these four functions and
20
20
  * the gate in `assertRendered`, so those ship and the ~40-line loop stays in the app.
21
21
  */
22
- import { mkdir, readFile, writeFile } from "node:fs/promises";
22
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
23
23
  import { dirname, join } from "node:path";
24
24
  import { pathToFileURL } from "node:url";
25
25
  import { EMPTY_ROOT } from "./head.js";
26
26
  import { describeOverflow } from "./og.js";
27
+ import { siteOrigin } from "./sitemap.js";
27
28
  /**
28
29
  * The built `index.html`, and a refusal to bake one twice.
29
30
  *
@@ -101,4 +102,91 @@ export async function writeOgCards({ pages, outDir, file, card, check = false, }
101
102
  await writeFile(join(outDir, entry.file), entry.png);
102
103
  return laid.map((entry) => entry.file);
103
104
  }
105
+ const CARD_META = /<meta[^>]+(?:property|name)="(?:og:image|twitter:image)"[^>]+content="([^"]*)"[^>]*>/gi;
106
+ function advertisedCards(page, html) {
107
+ const seen = new Set();
108
+ const cards = [];
109
+ for (const match of html.matchAll(CARD_META)) {
110
+ const url = match[1];
111
+ if (url && !seen.has(url)) {
112
+ seen.add(url);
113
+ cards.push({ page, url });
114
+ }
115
+ }
116
+ return cards;
117
+ }
118
+ /**
119
+ * The path a card URL points at inside `dist`, or null when the card is not this build's to
120
+ * serve. A `?v=2` or a `#` is addressing, not a file name, so both come off.
121
+ */
122
+ function distPathOf(url, origin) {
123
+ if (url.startsWith("/"))
124
+ return decodeURIComponent(url.split(/[?#]/)[0] ?? "");
125
+ try {
126
+ const parsed = new URL(url);
127
+ return parsed.origin === origin ? decodeURIComponent(parsed.pathname) : null;
128
+ }
129
+ catch {
130
+ // Not an address at all. A card URL relative to the page's own folder cannot be resolved
131
+ // without knowing where that page sits, and Open Graph asks for an absolute URL anyway.
132
+ return null;
133
+ }
134
+ }
135
+ async function isFile(path) {
136
+ try {
137
+ return (await stat(path)).isFile();
138
+ }
139
+ catch {
140
+ return false;
141
+ }
142
+ }
143
+ /**
144
+ * Every share card a built page advertises is a file that exists.
145
+ *
146
+ * Three repos shipped a page whose `og:image` named a card nothing had ever rendered. All three
147
+ * were `404.html`, all three for the same reason: a card generator walks the page registry, and
148
+ * a not-found shell is not in the registry, so a head built from the same template inherits a
149
+ * card address with no card behind it. Every share of a dead link unfurled broken, in one case
150
+ * in three languages, and **nothing in a browser shows it** — the page looks perfect, and the
151
+ * only reader who finds out is whoever the link was sent to.
152
+ *
153
+ * `bakeHead` cannot catch it: it is handed an `image` and writes it, and a shell carrying the
154
+ * brand card is correct, so it has no way to tell a right image from a wrong one (invariant 12).
155
+ * The build can, because by then the cards are either on disk or they are not. Run it last,
156
+ * after the pages and the cards are written:
157
+ *
158
+ * ```ts
159
+ * await assertOgImages("dist", "https://example.com");
160
+ * ```
161
+ *
162
+ * Cards on another host are somebody else's to serve and are skipped — but a build whose pages
163
+ * all advertise cards and whose origin matches NONE of them has been given the wrong origin,
164
+ * and a check that silently passes is worse than no check, so that is an error too.
165
+ */
166
+ export async function assertOgImages(distDir, origin) {
167
+ const root = siteOrigin(origin);
168
+ const entries = await readdir(distDir, { recursive: true, withFileTypes: true });
169
+ const pages = entries
170
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".html"))
171
+ .map((entry) => join(entry.parentPath, entry.name));
172
+ const advertised = [];
173
+ for (const page of pages)
174
+ advertised.push(...advertisedCards(page, await readFile(page, "utf8")));
175
+ const mine = advertised
176
+ .map((card) => ({ ...card, file: distPathOf(card.url, root) }))
177
+ .filter((card) => card.file !== null);
178
+ if (advertised.length > 0 && mine.length === 0)
179
+ throw new Error(`og: ${String(advertised.length)} page(s) advertise a share card and none is under ` +
180
+ `${root}, so nothing was checked. Pass the origin this build renders for.`);
181
+ const missing = [];
182
+ for (const card of mine) {
183
+ if (!(await isFile(join(distDir, card.file))))
184
+ missing.push(card);
185
+ }
186
+ if (missing.length > 0)
187
+ throw new Error(`og: ${String(missing.length)} page(s) advertise a share card that is not in ${distDir} —\n\n` +
188
+ missing.map((card) => ` · ${card.page} → ${card.url}`).join("\n") +
189
+ "\n\n Either render the card, or leave `image` out of the head for that page.");
190
+ return mine.length;
191
+ }
104
192
  //# sourceMappingURL=node.js.map
package/dist/node.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"node.js","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"node.js","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":";;;;;;;;AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAE3C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAAe;IAChD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IACzC,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC9C,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC;QAChC,MAAM,IAAI,KAAK,CACb,cAAc,IAAI,mBAAmB,UAAU,yCAAyC;YACtF,oFAAoF;YACpF,kDAAkD,CACrD,CAAC;IACJ,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,eAAe,CAAU,GAAY;IAC5C,OAAO,CACL,OAAO,GAAG,KAAK,QAAQ;QACvB,GAAG,KAAK,IAAI;QACZ,YAAY,IAAI,GAAG;QACnB,OAAO,GAAG,CAAC,UAAU,KAAK,UAAU,CACrC,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,SAAiB;IAEjB,MAAM,GAAG,GAAY,MAAM,MAAM,kCAAC,aAAa,CAAC,SAAS,CAAC,CAAC,IAAI,EAAC,CAAC;IACjE,IAAI,CAAC,eAAe,CAAU,GAAG,CAAC;QAChC,MAAM,IAAI,KAAK,CACb,cAAc,SAAS,8DAA8D,CACtF,CAAC;IACJ,OAAO,GAAG,CAAC,UAAU,CAAC;AACxB,CAAC;AAED,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAe,EAAE,IAAY,EAAE,QAAgB;IAC7E,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACnC,MAAM,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,MAAM,SAAS,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;AAC5C,CAAC;AAuBD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAO,EACvC,KAAK,EACL,MAAM,EACN,IAAI,EACJ,IAAI,EACJ,KAAK,GAAG,KAAK,GACa;IAC1B,MAAM,IAAI,GAAwC,EAAE,CAAC;IACrD,MAAM,SAAS,GAAiB,EAAE,CAAC;IAEnC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3C,IAAI,QAAQ,EAAE,MAAM;YAAE,SAAS,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC;QAClD,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC;IACvC,CAAC;IAED,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;QACtB,MAAM,IAAI,KAAK,CACb,OAAO,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,uCAAuC;YACpE,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,gBAAgB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;YAC/D,sFAAsF,CACzF,CAAC;IAEJ,IAAI,KAAK;QAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAElD,MAAM,KAAK,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,KAAK,MAAM,KAAK,IAAI,IAAI;QAAE,MAAM,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAC/E,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACzC,CAAC;AAQD,MAAM,SAAS,GACb,wFAAwF,CAAC;AAE3F,SAAS,eAAe,CAAC,IAAY,EAAE,IAAY;IACjD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,KAAK,GAAqB,EAAE,CAAC;IACnC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7C,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACrB,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACd,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,SAAS,UAAU,CAAC,GAAW,EAAE,MAAc;IAC7C,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAC/E,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5B,OAAO,MAAM,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,kBAAkB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/E,CAAC;IAAC,MAAM,CAAC;QACP,yFAAyF;QACzF,wFAAwF;QACxF,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,MAAM,CAAC,IAAY;IAChC,IAAI,CAAC;QACH,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAAe,EAAE,MAAc;IAClE,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;IAChC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IACjF,MAAM,KAAK,GAAG,OAAO;SAClB,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;SACjE,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IAEtD,MAAM,UAAU,GAAqB,EAAE,CAAC;IACxC,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,UAAU,CAAC,IAAI,CAAC,GAAG,eAAe,CAAC,IAAI,EAAE,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IAElG,MAAM,IAAI,GAAG,UAAU;SACpB,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;SAC9D,MAAM,CAAC,CAAC,IAAI,EAA6C,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IAEnF,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAC5C,MAAM,IAAI,KAAK,CACb,OAAO,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,oDAAoD;YAClF,GAAG,IAAI,mEAAmE,CAC7E,CAAC;IAEJ,MAAM,OAAO,GAA0C,EAAE,CAAC;IAC1D,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,IAAI,CAAC,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpE,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QACpB,MAAM,IAAI,KAAK,CACb,OAAO,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,kDAAkD,OAAO,QAAQ;YAC5F,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;YAClE,+EAA+E,CAClF,CAAC;IAEJ,OAAO,IAAI,CAAC,MAAM,CAAC;AACrB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gusnips/vite",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "The build-time half of a prerendered Vite SPA: head baking, sitemap, robots, the prerender driver, and a shared vite preset.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -59,7 +59,7 @@
59
59
  "release:major": "bun pm version major && bun publish --access public"
60
60
  },
61
61
  "dependencies": {
62
- "@gusnips/react": "0.4.5"
62
+ "@gusnips/react": "0.4.6"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "@tailwindcss/vite": "^4",
package/src/head.test.ts CHANGED
@@ -48,6 +48,14 @@ const LEAN_TEMPLATE = `<!doctype html>
48
48
  </body>
49
49
  </html>`;
50
50
 
51
+ /**
52
+ * A fourth adopter's spelling of the same tags. X's own documentation writes them `name=`, the
53
+ * Open Graph spec writes `property=`, and crawlers read both — so a template may use either, and
54
+ * two templates in this fleet use both in one head. Matching only `property=` turned the other
55
+ * spelling into a silent skip rather than a build error.
56
+ */
57
+ const NAME_TWITTER_TEMPLATE = TEMPLATE.replace(/property="twitter:/g, 'name="twitter:');
58
+
51
59
  const BASE = { title: "T", description: "D", canonical: "https://acme.com/pricing" };
52
60
 
53
61
  describe("bakeHead", () => {
@@ -96,6 +104,22 @@ describe("bakeHead", () => {
96
104
  expect(html).not.toContain('name="title"');
97
105
  });
98
106
 
107
+ // …and the trap right beside that exception. A template that DOES carry these, spelled the
108
+ // way X documents them, must not read as one that omits them: skipping is correct for an
109
+ // absent tag and silent data loss for a present one. An adopter on this spelling would have
110
+ // shipped every page wearing the front door's card.
111
+ it("writes the share tags whichever attribute the template spells them with", () => {
112
+ const html = bakeHead(NAME_TWITTER_TEMPLATE, { ...BASE, ogTitle: "share" });
113
+ expect(html).toContain('<meta name="twitter:title" content="share" />');
114
+ expect(html).toContain('<meta name="twitter:description" content="D" />');
115
+ });
116
+
117
+ it("strips a share URL spelled with either attribute", () => {
118
+ const html = bakeHead(NAME_TWITTER_TEMPLATE, { ...BASE, canonical: null });
119
+ expect(html).not.toContain("twitter:url");
120
+ expect(html).not.toContain('content=""');
121
+ });
122
+
99
123
  it("strips the canonical and the share URL rather than blanking them", () => {
100
124
  const html = bakeHead(TEMPLATE, { ...BASE, canonical: null, noindex: true });
101
125
  expect(html).not.toContain('rel="canonical"');
@@ -255,6 +279,21 @@ describe("assertRendered", () => {
255
279
  expect(() => assertRendered("pricing.html", page(spinner), shell)).toThrow(/loading screen/);
256
280
  });
257
281
 
282
+ // The donor's splash IS the root's first element. A fourth adopter's centres the live region
283
+ // inside a `<main>`, so a check anchored at the first tag saw an ordinary wrapper and passed
284
+ // the file — while that repo's own guard looked for a spinner class its splash does not use.
285
+ it("catches a loading screen nested inside a wrapper element", () => {
286
+ const nested = `<div id="root"><main class="grid place-items-center"><p role="status">Loading${"x".repeat(700)}</p></main></div>`;
287
+ expect(() => assertRendered("pricing.html", page(nested), shell)).toThrow(/loading screen/);
288
+ });
289
+
290
+ // What makes the wider search safe: a page with this much in it has a page in it, whatever a
291
+ // live region at the top of it says. A real banner ("Saved", a connection notice) is ordinary.
292
+ it("does not flag a full page that opens with a live region", () => {
293
+ const banner = `<div id="root"><div role="status">Saved</div><main>${"x".repeat(2500)}</main></div>`;
294
+ expect(() => assertRendered("pricing.html", page(banner), shell)).not.toThrow();
295
+ });
296
+
258
297
  // The reason the check slices rather than matching in one pattern: a regex that skips
259
298
  // Suspense markers can backtrack across the whole document and find a `role="status"` 40 KB
260
299
  // down in a perfectly good page.
package/src/head.ts CHANGED
@@ -34,7 +34,25 @@ function setMeta(html: string, selector: string, value: string): string {
34
34
  }
35
35
 
36
36
  /**
37
- * The same, for a tag a template is allowed not to carry.
37
+ * The same regex, for a key a template may spell with EITHER attribute.
38
+ *
39
+ * The `twitter:` family is written both ways in the wild and both work: X's own documentation
40
+ * says `name=`, the Open Graph spec says `property=`, and two adopters' templates carry
41
+ * `name="twitter:card"` in the same head as `property="twitter:title"`. Matching one spelling
42
+ * silently skips the other — and these tags go through `setMetaIfPresent`, where "skipped" is
43
+ * not an error but the intended behaviour for a template that does not have them.
44
+ *
45
+ * The fourth adopter spells the whole family `name=`. Baked with the narrow regex, every one of
46
+ * its 45 prerendered files — eleven pages in three languages — would have kept the template's
47
+ * card title and description, which is the front door's, and nothing in a browser shows it.
48
+ * That is the exact failure this file exists to prevent, arriving through the package rather
49
+ * than despite it.
50
+ */
51
+ const eitherRe = (key: string): RegExp =>
52
+ new RegExp(`(<meta[^>]*(?:name|property)="${escapeRegex(key)}"[^>]*content=")[^"]*(")`);
53
+
54
+ /**
55
+ * Replace the `content` of a tag a template is allowed not to carry.
38
56
  *
39
57
  * Only the `twitter:` tags, `og:image:alt` and `name="title"` get this. X reads the `og:` tags when
40
58
  * the `twitter:` ones are absent, no crawler reads `name="title"` at all, and most templates carry
@@ -42,9 +60,10 @@ function setMeta(html: string, selector: string, value: string): string {
42
60
  * that never had them. A template that DOES carry one and gets a stale value is still a bug, which
43
61
  * is why they are written rather than ignored. Everything a crawler actually depends on goes
44
62
  * through `setMeta`.
63
+ *
64
+ * Takes the regex rather than the selector, so each caller picks how strict the spelling is.
45
65
  */
46
- function setMetaIfPresent(html: string, selector: string, value: string): string {
47
- const re = metaRe(selector);
66
+ function setMetaIfPresent(html: string, re: RegExp, value: string): string {
48
67
  return re.test(html) ? html.replace(re, `$1${escapeAttr(value)}$2`) : html;
49
68
  }
50
69
 
@@ -142,17 +161,17 @@ export function bakeHead(template: string, tags: HeadTags): string {
142
161
  html = setMeta(html, 'name="description"', tags.description);
143
162
  html = setMeta(html, 'property="og:title"', shareTitle);
144
163
  html = setMeta(html, 'property="og:description"', shareDescription);
145
- html = setMetaIfPresent(html, 'name="title"', tags.title);
146
- html = setMetaIfPresent(html, 'property="twitter:title"', shareTitle);
147
- html = setMetaIfPresent(html, 'property="twitter:description"', shareDescription);
164
+ html = setMetaIfPresent(html, metaRe('name="title"'), tags.title);
165
+ html = setMetaIfPresent(html, eitherRe("twitter:title"), shareTitle);
166
+ html = setMetaIfPresent(html, eitherRe("twitter:description"), shareDescription);
148
167
 
149
168
  if (tags.image !== undefined) {
150
169
  html = setMeta(html, 'property="og:image"', tags.image);
151
- html = setMetaIfPresent(html, 'property="twitter:image"', tags.image);
170
+ html = setMetaIfPresent(html, eitherRe("twitter:image"), tags.image);
152
171
  // A card's description goes with the card. Left alone, every page's own card was described
153
172
  // with the front page's title — the stale value this function exists to prevent, on the one
154
173
  // tag written for somebody who cannot see the picture.
155
- html = setMetaIfPresent(html, 'property="og:image:alt"', shareTitle);
174
+ html = setMetaIfPresent(html, eitherRe("og:image:alt"), shareTitle);
156
175
  }
157
176
 
158
177
  if (tags.canonical === null) {
@@ -161,10 +180,10 @@ export function bakeHead(template: string, tags: HeadTags): string {
161
180
  // claim about this address, and a dead link that still unfurls the brand card is fine.
162
181
  html = html
163
182
  .replace(/\s*<link rel="canonical"[^>]*>/, "")
164
- .replace(/\s*<meta property="(?:og|twitter):url"[^>]*>/g, "");
183
+ .replace(/\s*<meta[^>]*(?:name|property)="(?:og|twitter):url"[^>]*>/g, "");
165
184
  } else {
166
185
  html = setMeta(html, 'property="og:url"', tags.canonical);
167
- html = setMetaIfPresent(html, 'property="twitter:url"', tags.canonical);
186
+ html = setMetaIfPresent(html, eitherRe("twitter:url"), tags.canonical);
168
187
  // Demanded, like every meta tag a crawler depends on. `String.replace` with no match is a
169
188
  // silent no-op, so a template that never had a canonical would get one on no page at all
170
189
  // and say nothing about it — the exact failure `setMeta` exists to turn into a red build.
@@ -243,6 +262,15 @@ export interface RenderedChecks {
243
262
  minGrowth?: number;
244
263
  }
245
264
 
265
+ /**
266
+ * Past this much growth the file has a page in it, whatever else it also contains.
267
+ *
268
+ * It is what lets the loading-screen search below look at a whole slice rather than one tag: a
269
+ * real page that opens with a live region ("Saved", a connection banner) is far over this, and a
270
+ * splash never is. The donor's was 1,174 bytes.
271
+ */
272
+ const SPLASH_MAX_GROWTH = 2000;
273
+
246
274
  /**
247
275
  * What every written file must be true of before the build is allowed to pass.
248
276
  *
@@ -298,6 +326,12 @@ export function assertRendered(
298
326
  .trimStart();
299
327
 
300
328
  if (!/^<[a-z]/.test(head)) fail("has no element inside its root");
301
- if (/^<[^>]*role="status"/.test(head))
329
+ // Anywhere in that bounded slice, not only its first tag, and only while the file is small
330
+ // enough to BE a splash. Anchored at the root's first element this missed the fourth adopter's
331
+ // loading screen, which centres its live region inside a `<main>` — one element deeper than the
332
+ // donor's, and an ordinary-looking wrapper to a test that reads one tag. That adopter's own
333
+ // guard missed it from the other side: it failed a file containing a spinner class its splash
334
+ // does not use. Two checks written for one regression, neither of which could ever fire.
335
+ if (grew < SPLASH_MAX_GROWTH && head.includes('role="status"'))
302
336
  fail("rendered the loading screen, not the page — something suspended and never resolved");
303
337
  }
package/src/index.ts CHANGED
@@ -26,6 +26,7 @@ export {
26
26
  type RenderedChecks,
27
27
  } from "./head.ts";
28
28
  export {
29
+ assertOgImages,
29
30
  loadRenderer,
30
31
  loadTemplate,
31
32
  writeDist,
package/src/node.test.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
1
+ import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
- import { join } from "node:path";
3
+ import { dirname, join } from "node:path";
4
4
  import { afterEach, describe, expect, it } from "vitest";
5
- import { loadTemplate, writeOgCards } from "./node.ts";
5
+ import { assertOgImages, loadTemplate, writeOgCards } from "./node.ts";
6
6
 
7
7
  const dirs: string[] = [];
8
8
 
@@ -100,3 +100,77 @@ describe("writeOgCards", () => {
100
100
  await expect(run).rejects.toThrow(/2 card\(s\)/);
101
101
  });
102
102
  });
103
+
104
+ describe("assertOgImages", () => {
105
+ const page = (image: string) =>
106
+ `<html><head><meta property="og:image" content="${image}" />` +
107
+ `<meta property="twitter:image" content="${image}" /></head></html>`;
108
+
109
+ async function distWith(files: Record<string, string>): Promise<string> {
110
+ const dir = await mkdtemp(join(tmpdir(), "frontkit-og-"));
111
+ dirs.push(dir);
112
+ for (const [file, contents] of Object.entries(files)) {
113
+ await mkdir(join(dir, dirname(file)), { recursive: true });
114
+ await writeFile(join(dir, file), contents, "utf8");
115
+ }
116
+ return dir;
117
+ }
118
+
119
+ it("passes when every advertised card is on disk", async () => {
120
+ const dist = await distWith({
121
+ "index.html": page("https://example.com/og/home.png"),
122
+ "og/home.png": "png",
123
+ });
124
+ await expect(assertOgImages(dist, "https://example.com")).resolves.toBe(1);
125
+ });
126
+
127
+ /**
128
+ * The bug itself, three repos over: the card generator walks the page registry, the 404 shell
129
+ * is not in the registry, and the head it inherits advertises a card that has never been
130
+ * rendered. Nothing in a browser shows it — only the person the link was sent to.
131
+ */
132
+ it("names the page whose card was never rendered", async () => {
133
+ const dist = await distWith({
134
+ "index.html": page("https://example.com/og/home.png"),
135
+ "404.html": page("https://example.com/og/__not-found__.png"),
136
+ "og/home.png": "png",
137
+ });
138
+
139
+ await expect(assertOgImages(dist, "https://example.com")).rejects.toThrow(
140
+ /404\.html → https:\/\/example\.com\/og\/__not-found__\.png/,
141
+ );
142
+ });
143
+
144
+ it("finds a page nested under a directory, and reads a root-relative card", async () => {
145
+ const dist = await distWith({
146
+ "precos/index.html": page("/og/precos.png"),
147
+ });
148
+ await expect(assertOgImages(dist, "https://example.com")).rejects.toThrow(/og\/precos\.png/);
149
+ });
150
+
151
+ it("skips a card served by somebody else", async () => {
152
+ const dist = await distWith({
153
+ "index.html": page("https://cdn.example.net/card.png"),
154
+ "about.html": page("https://example.com/og/about.png"),
155
+ "og/about.png": "png",
156
+ });
157
+ await expect(assertOgImages(dist, "https://example.com")).resolves.toBe(1);
158
+ });
159
+
160
+ // A check that silently passes is worse than no check: with the origin wrong, every card on
161
+ // the page looks like somebody else's and nothing gets looked at.
162
+ it("refuses an origin that matches none of the cards it found", async () => {
163
+ const dist = await distWith({ "index.html": page("https://example.com/og/home.png") });
164
+ await expect(assertOgImages(dist, "https://wrong.example")).rejects.toThrow(
165
+ /none is under https:\/\/wrong\.example/,
166
+ );
167
+ });
168
+
169
+ it("ignores a cache-busting query when looking for the file", async () => {
170
+ const dist = await distWith({
171
+ "index.html": page("https://example.com/og/home.png?v=2"),
172
+ "og/home.png": "png",
173
+ });
174
+ await expect(assertOgImages(dist, "https://example.com")).resolves.toBe(1);
175
+ });
176
+ });
package/src/node.ts CHANGED
@@ -11,13 +11,14 @@
11
11
  * policy the product owns. What is identical in every one of them is these four functions and
12
12
  * the gate in `assertRendered`, so those ship and the ~40-line loop stays in the app.
13
13
  */
14
- import { mkdir, readFile, writeFile } from "node:fs/promises";
14
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
15
15
  import { dirname, join } from "node:path";
16
16
  import { pathToFileURL } from "node:url";
17
17
  import { EMPTY_ROOT } from "./head.ts";
18
18
  import type { OgOverflow } from "./og.ts";
19
19
  import { describeOverflow } from "./og.ts";
20
20
  import type { PageRenderer } from "./render.ts";
21
+ import { siteOrigin } from "./sitemap.ts";
21
22
 
22
23
  /**
23
24
  * The built `index.html`, and a refusal to bake one twice.
@@ -138,3 +139,107 @@ export async function writeOgCards<Page>({
138
139
  for (const entry of laid) await writeFile(join(outDir, entry.file), entry.png);
139
140
  return laid.map((entry) => entry.file);
140
141
  }
142
+
143
+ /** Where a page says its share card is, and the page that says it. */
144
+ interface AdvertisedCard {
145
+ page: string;
146
+ url: string;
147
+ }
148
+
149
+ const CARD_META =
150
+ /<meta[^>]+(?:property|name)="(?:og:image|twitter:image)"[^>]+content="([^"]*)"[^>]*>/gi;
151
+
152
+ function advertisedCards(page: string, html: string): AdvertisedCard[] {
153
+ const seen = new Set<string>();
154
+ const cards: AdvertisedCard[] = [];
155
+ for (const match of html.matchAll(CARD_META)) {
156
+ const url = match[1];
157
+ if (url && !seen.has(url)) {
158
+ seen.add(url);
159
+ cards.push({ page, url });
160
+ }
161
+ }
162
+ return cards;
163
+ }
164
+
165
+ /**
166
+ * The path a card URL points at inside `dist`, or null when the card is not this build's to
167
+ * serve. A `?v=2` or a `#` is addressing, not a file name, so both come off.
168
+ */
169
+ function distPathOf(url: string, origin: string): string | null {
170
+ if (url.startsWith("/")) return decodeURIComponent(url.split(/[?#]/)[0] ?? "");
171
+ try {
172
+ const parsed = new URL(url);
173
+ return parsed.origin === origin ? decodeURIComponent(parsed.pathname) : null;
174
+ } catch {
175
+ // Not an address at all. A card URL relative to the page's own folder cannot be resolved
176
+ // without knowing where that page sits, and Open Graph asks for an absolute URL anyway.
177
+ return null;
178
+ }
179
+ }
180
+
181
+ async function isFile(path: string): Promise<boolean> {
182
+ try {
183
+ return (await stat(path)).isFile();
184
+ } catch {
185
+ return false;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Every share card a built page advertises is a file that exists.
191
+ *
192
+ * Three repos shipped a page whose `og:image` named a card nothing had ever rendered. All three
193
+ * were `404.html`, all three for the same reason: a card generator walks the page registry, and
194
+ * a not-found shell is not in the registry, so a head built from the same template inherits a
195
+ * card address with no card behind it. Every share of a dead link unfurled broken, in one case
196
+ * in three languages, and **nothing in a browser shows it** — the page looks perfect, and the
197
+ * only reader who finds out is whoever the link was sent to.
198
+ *
199
+ * `bakeHead` cannot catch it: it is handed an `image` and writes it, and a shell carrying the
200
+ * brand card is correct, so it has no way to tell a right image from a wrong one (invariant 12).
201
+ * The build can, because by then the cards are either on disk or they are not. Run it last,
202
+ * after the pages and the cards are written:
203
+ *
204
+ * ```ts
205
+ * await assertOgImages("dist", "https://example.com");
206
+ * ```
207
+ *
208
+ * Cards on another host are somebody else's to serve and are skipped — but a build whose pages
209
+ * all advertise cards and whose origin matches NONE of them has been given the wrong origin,
210
+ * and a check that silently passes is worse than no check, so that is an error too.
211
+ */
212
+ export async function assertOgImages(distDir: string, origin: string): Promise<number> {
213
+ const root = siteOrigin(origin);
214
+ const entries = await readdir(distDir, { recursive: true, withFileTypes: true });
215
+ const pages = entries
216
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".html"))
217
+ .map((entry) => join(entry.parentPath, entry.name));
218
+
219
+ const advertised: AdvertisedCard[] = [];
220
+ for (const page of pages) advertised.push(...advertisedCards(page, await readFile(page, "utf8")));
221
+
222
+ const mine = advertised
223
+ .map((card) => ({ ...card, file: distPathOf(card.url, root) }))
224
+ .filter((card): card is AdvertisedCard & { file: string } => card.file !== null);
225
+
226
+ if (advertised.length > 0 && mine.length === 0)
227
+ throw new Error(
228
+ `og: ${String(advertised.length)} page(s) advertise a share card and none is under ` +
229
+ `${root}, so nothing was checked. Pass the origin this build renders for.`,
230
+ );
231
+
232
+ const missing: (AdvertisedCard & { file: string })[] = [];
233
+ for (const card of mine) {
234
+ if (!(await isFile(join(distDir, card.file)))) missing.push(card);
235
+ }
236
+
237
+ if (missing.length > 0)
238
+ throw new Error(
239
+ `og: ${String(missing.length)} page(s) advertise a share card that is not in ${distDir} —\n\n` +
240
+ missing.map((card) => ` · ${card.page} → ${card.url}`).join("\n") +
241
+ "\n\n Either render the card, or leave `image` out of the head for that page.",
242
+ );
243
+
244
+ return mine.length;
245
+ }