@gusnips/vite 0.4.4 → 0.4.6
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 +21 -3
- package/dist/head.d.ts.map +1 -1
- package/dist/head.js +9 -2
- package/dist/head.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +24 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +89 -1
- package/dist/node.js.map +1 -1
- package/dist/sitemap.d.ts +4 -0
- package/dist/sitemap.d.ts.map +1 -1
- package/dist/sitemap.js +4 -0
- package/dist/sitemap.js.map +1 -1
- package/package.json +2 -2
- package/src/head.test.ts +10 -0
- package/src/head.ts +9 -2
- package/src/index.ts +1 -0
- package/src/node.test.ts +77 -3
- package/src/node.ts +106 -1
- package/src/sitemap.ts +4 -0
package/README.md
CHANGED
|
@@ -86,9 +86,14 @@ build instead of shipping.
|
|
|
86
86
|
That `pageFile` at the top writes `pricing.html`, not `pricing/index.html`. Nested routes keep
|
|
87
87
|
their folders — `/guides/errors` → `guides/errors.html`.
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
Every address your app puts in a canonical or a sitemap has to answer 200 rather than redirect. On
|
|
90
|
+
Cloudflare Pages that means flat files: it serves `pricing/index.html` at `/pricing/` and answers
|
|
91
|
+
`/pricing` with a 308. A flat file answers both.
|
|
92
|
+
|
|
93
|
+
Hosts differ, though. Firebase Hosting redirects `/pricing` to `/pricing/` by default, and serves
|
|
94
|
+
`pricing/index.html` at `/pricing` itself once `trailingSlash` is `false`. On a host like that,
|
|
95
|
+
write the directory form and set the flag. Either way, run `curl -I` on one page before you trust
|
|
96
|
+
the sitemap.
|
|
92
97
|
|
|
93
98
|
## Sitemap, robots, OG cards
|
|
94
99
|
|
|
@@ -106,6 +111,19 @@ text-fitting maths. When copy does not fit, it **records the overflow and refuse
|
|
|
106
111
|
rather than appending an ellipsis — an ellipsis makes every string "fit", so copy that outgrew
|
|
107
112
|
its column has no failing case and ships.
|
|
108
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
|
+
|
|
109
127
|
`loadTemplate` refuses a `dist/index.html` that is already a rendered page. `dist/index.html` is
|
|
110
128
|
both the file every page is baked _from_ and the home page's own output, so a second pass over a
|
|
111
129
|
written `dist/` reads a finished page as its blank and nests one render inside another. `vite
|
package/dist/head.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/head.js
CHANGED
|
@@ -118,8 +118,15 @@ export function bakeHead(template, tags) {
|
|
|
118
118
|
.map((alt) => ` <meta property="og:locale:alternate" content="${escapeAttr(ogLocale(alt.hreflang))}" />`);
|
|
119
119
|
html = html.replace("</head>", ` <meta property="og:locale" content="${escapeAttr(ogLocale(tags.lang))}" />\n${alternates.length ? `${alternates.join("\n")}\n` : ""} </head>`);
|
|
120
120
|
}
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
// A template that says `index, follow` gets its tag rewritten, not a second one beside it. A head
|
|
122
|
+
// carrying both leaves a contradiction to each crawler's own tie-break rule, and one adopter's
|
|
123
|
+
// 404 shipped exactly that.
|
|
124
|
+
if (tags.noindex) {
|
|
125
|
+
const robots = metaRe('name="robots"');
|
|
126
|
+
html = robots.test(html)
|
|
127
|
+
? html.replace(robots, "$1noindex$2")
|
|
128
|
+
: html.replace("</head>", ` <meta name="robots" content="noindex" />\n </head>`);
|
|
129
|
+
}
|
|
123
130
|
if (tags.headExtra)
|
|
124
131
|
html = html.replace("</head>", ` ${tags.headExtra}\n </head>`);
|
|
125
132
|
if (tags.lang !== undefined) {
|
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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
|
package/dist/node.d.ts.map
CHANGED
|
@@ -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;
|
|
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":";;;;;;;;AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"node.js","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"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"}
|
package/dist/sitemap.d.ts
CHANGED
|
@@ -7,6 +7,10 @@ import type { Alternate } from "./head.ts";
|
|
|
7
7
|
* the app advertises in its canonical and its sitemap would be a redirect rather than a page.
|
|
8
8
|
* A flat file is served at `/pricing` AND `/pricing/`, 200 either way.
|
|
9
9
|
*
|
|
10
|
+
* That is Cloudflare Pages, not every host. Firebase Hosting serves the directory form at
|
|
11
|
+
* `/pricing` itself with `trailingSlash: false`, and an adopter there names its own files. What
|
|
12
|
+
* holds on every host is the rule underneath: an advertised address answers 200.
|
|
13
|
+
*
|
|
10
14
|
* A nested route keeps its folders: `/guides/errors` → `guides/errors.html`.
|
|
11
15
|
*/
|
|
12
16
|
export declare function pageFile(routePath: string): string;
|
package/dist/sitemap.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../src/sitemap.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C
|
|
1
|
+
{"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../src/sitemap.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAGlD;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM9C;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;CAC5F;AAED;+CAC+C;AAC/C,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG7C;AAED,qFAAqF;AACrF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,0FAA0F;IAC1F,QAAQ,EAAE,MAAM,CAAC;IACjB;iEAC6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;iFAC6E;IAC7E,UAAU,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;CACnC;AAED,+DAA+D;AAC/D,wBAAgB,UAAU,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,CAyBnE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,UAAU,EAAE,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAYjG;AAED,MAAM,WAAW,aAAa;IAC5B;;uFAEmF;IACnF,MAAM,EAAE,MAAM,CAAC;IACf;;;;iDAI6C;IAC7C,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B;0FACsF;IACtF,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B;AAED,wBAAgB,SAAS,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAa,EAAE,EAAE,aAAa,GAAG,MAAM,CAQpF"}
|
package/dist/sitemap.js
CHANGED
|
@@ -13,6 +13,10 @@ import { escapeAttr } from "./escape.js";
|
|
|
13
13
|
* the app advertises in its canonical and its sitemap would be a redirect rather than a page.
|
|
14
14
|
* A flat file is served at `/pricing` AND `/pricing/`, 200 either way.
|
|
15
15
|
*
|
|
16
|
+
* That is Cloudflare Pages, not every host. Firebase Hosting serves the directory form at
|
|
17
|
+
* `/pricing` itself with `trailingSlash: false`, and an adopter there names its own files. What
|
|
18
|
+
* holds on every host is the rule underneath: an advertised address answers 200.
|
|
19
|
+
*
|
|
16
20
|
* A nested route keeps its folders: `/guides/errors` → `guides/errors.html`.
|
|
17
21
|
*/
|
|
18
22
|
export function pageFile(routePath) {
|
package/dist/sitemap.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sitemap.js","sourceRoot":"","sources":["../src/sitemap.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAKzC
|
|
1
|
+
{"version":3,"file":"sitemap.js","sourceRoot":"","sources":["../src/sitemap.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gusnips/vite",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
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.
|
|
62
|
+
"@gusnips/react": "0.4.6"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
65
|
"@tailwindcss/vite": "^4",
|
package/src/head.test.ts
CHANGED
|
@@ -104,6 +104,16 @@ describe("bakeHead", () => {
|
|
|
104
104
|
expect(html).toContain('<meta name="robots" content="noindex" />');
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
it("rewrites the template's own robots tag rather than contradicting it", () => {
|
|
108
|
+
const indexable = TEMPLATE.replace(
|
|
109
|
+
"</head>",
|
|
110
|
+
'<meta name="robots" content="index, follow" />\n</head>',
|
|
111
|
+
);
|
|
112
|
+
const html = bakeHead(indexable, { ...BASE, canonical: null, noindex: true });
|
|
113
|
+
expect(html.match(/name="robots"/g)).toHaveLength(1);
|
|
114
|
+
expect(html).toContain('<meta name="robots" content="noindex" />');
|
|
115
|
+
});
|
|
116
|
+
|
|
107
117
|
// An empty canonical is a claim about "" and an empty og:url is a share card pointing at the
|
|
108
118
|
// origin root, which is what the strip exists to avoid.
|
|
109
119
|
it("leaves no blank URL behind on a page with no canonical", () => {
|
package/src/head.ts
CHANGED
|
@@ -204,8 +204,15 @@ export function bakeHead(template: string, tags: HeadTags): string {
|
|
|
204
204
|
);
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
// A template that says `index, follow` gets its tag rewritten, not a second one beside it. A head
|
|
208
|
+
// carrying both leaves a contradiction to each crawler's own tie-break rule, and one adopter's
|
|
209
|
+
// 404 shipped exactly that.
|
|
210
|
+
if (tags.noindex) {
|
|
211
|
+
const robots = metaRe('name="robots"');
|
|
212
|
+
html = robots.test(html)
|
|
213
|
+
? html.replace(robots, "$1noindex$2")
|
|
214
|
+
: html.replace("</head>", ` <meta name="robots" content="noindex" />\n </head>`);
|
|
215
|
+
}
|
|
209
216
|
if (tags.headExtra) html = html.replace("</head>", ` ${tags.headExtra}\n </head>`);
|
|
210
217
|
|
|
211
218
|
if (tags.lang !== undefined) {
|
package/src/index.ts
CHANGED
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
|
+
}
|
package/src/sitemap.ts
CHANGED
|
@@ -17,6 +17,10 @@ import type { Alternate } from "./head.ts";
|
|
|
17
17
|
* the app advertises in its canonical and its sitemap would be a redirect rather than a page.
|
|
18
18
|
* A flat file is served at `/pricing` AND `/pricing/`, 200 either way.
|
|
19
19
|
*
|
|
20
|
+
* That is Cloudflare Pages, not every host. Firebase Hosting serves the directory form at
|
|
21
|
+
* `/pricing` itself with `trailingSlash: false`, and an adopter there names its own files. What
|
|
22
|
+
* holds on every host is the rule underneath: an advertised address answers 200.
|
|
23
|
+
*
|
|
20
24
|
* A nested route keeps its folders: `/guides/errors` → `guides/errors.html`.
|
|
21
25
|
*/
|
|
22
26
|
export function pageFile(routePath: string): string {
|