@gusnips/vite 0.1.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/LICENSE +21 -0
- package/README.md +123 -0
- package/dist/escape.d.ts +6 -0
- package/dist/escape.d.ts.map +1 -0
- package/dist/escape.js +6 -0
- package/dist/escape.js.map +1 -0
- package/dist/head.d.ts +110 -0
- package/dist/head.d.ts.map +1 -0
- package/dist/head.js +183 -0
- package/dist/head.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/node.d.ts +57 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +104 -0
- package/dist/node.js.map +1 -0
- package/dist/og.d.ts +70 -0
- package/dist/og.d.ts.map +1 -0
- package/dist/og.js +60 -0
- package/dist/og.js.map +1 -0
- package/dist/preset.d.ts +37 -0
- package/dist/preset.d.ts.map +1 -0
- package/dist/preset.js +43 -0
- package/dist/preset.js.map +1 -0
- package/dist/render.d.ts +36 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +50 -0
- package/dist/render.js.map +1 -0
- package/dist/sitemap.d.ts +82 -0
- package/dist/sitemap.d.ts.map +1 -0
- package/dist/sitemap.js +91 -0
- package/dist/sitemap.js.map +1 -0
- package/package.json +105 -0
- package/src/escape.ts +8 -0
- package/src/head.test.ts +258 -0
- package/src/head.ts +288 -0
- package/src/index.ts +59 -0
- package/src/node.test.ts +102 -0
- package/src/node.ts +140 -0
- package/src/og.test.ts +53 -0
- package/src/og.ts +103 -0
- package/src/preset.ts +78 -0
- package/src/render.test.ts +67 -0
- package/src/render.ts +75 -0
- package/src/sitemap.test.ts +124 -0
- package/src/sitemap.ts +159 -0
package/dist/node.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* The whole filesystem surface of this package: read the template, load the SSR bundle, write
|
|
11
|
+
* files.
|
|
12
|
+
*
|
|
13
|
+
* It is one file on purpose. Everything else here is string work that a test can run without a
|
|
14
|
+
* disk, and keeping the four impure functions together is what lets `head.ts`, `sitemap.ts`,
|
|
15
|
+
* `render.ts` and `og.ts` stay that way.
|
|
16
|
+
*
|
|
17
|
+
* The prerender LOOP is not here. Every donor's loop is different — one walks a registry once,
|
|
18
|
+
* another walks it per locale, a third writes three separate shells — and all of that is
|
|
19
|
+
* policy the product owns. What is identical in every one of them is these four functions and
|
|
20
|
+
* the gate in `assertRendered`, so those ship and the ~40-line loop stays in the app.
|
|
21
|
+
*/
|
|
22
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
23
|
+
import { dirname, join } from "node:path";
|
|
24
|
+
import { pathToFileURL } from "node:url";
|
|
25
|
+
import { EMPTY_ROOT } from "./head.js";
|
|
26
|
+
import { describeOverflow } from "./og.js";
|
|
27
|
+
/**
|
|
28
|
+
* The built `index.html`, and a refusal to bake one twice.
|
|
29
|
+
*
|
|
30
|
+
* `dist/index.html` is BOTH the template and the front page's destination, so a second run over
|
|
31
|
+
* a `dist/` the prerender has already touched would read a finished page as its blank shell and
|
|
32
|
+
* nest one render inside another. `vite build` empties `dist/` and normally makes this
|
|
33
|
+
* impossible; what does not is a restored build cache, or somebody running the script directly
|
|
34
|
+
* to debug it. Caught here, by name, rather than as a missing-root error three frames down.
|
|
35
|
+
*/
|
|
36
|
+
export async function loadTemplate(distDir) {
|
|
37
|
+
const file = join(distDir, "index.html");
|
|
38
|
+
const template = await readFile(file, "utf8");
|
|
39
|
+
if (!template.includes(EMPTY_ROOT))
|
|
40
|
+
throw new Error(`prerender: ${file} does not carry ${EMPTY_ROOT}. Either it is already a rendered page ` +
|
|
41
|
+
"— run `vite build` to regenerate the shell this reads — or the app's root element " +
|
|
42
|
+
"carries attributes, which the baker cannot fill.");
|
|
43
|
+
return template;
|
|
44
|
+
}
|
|
45
|
+
function exportsRenderer(mod) {
|
|
46
|
+
return (typeof mod === "object" &&
|
|
47
|
+
mod !== null &&
|
|
48
|
+
"renderPage" in mod &&
|
|
49
|
+
typeof mod.renderPage === "function");
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The app compiled for the server, imported out of the BUILT bundle.
|
|
53
|
+
*
|
|
54
|
+
* The prerender is a script rather than a Vite plugin for exactly this reason: it needs the SSR
|
|
55
|
+
* bundle, and a plugin running in `closeBundle` is inside the build that would have to have
|
|
56
|
+
* produced it. So the entry is a path on disk, written by `vite build --ssr src/entry-server.tsx`,
|
|
57
|
+
* and nothing in the source tree references it.
|
|
58
|
+
*/
|
|
59
|
+
export async function loadRenderer(entryFile) {
|
|
60
|
+
const mod = await import(__rewriteRelativeImportExtension(pathToFileURL(entryFile).href));
|
|
61
|
+
if (!exportsRenderer(mod))
|
|
62
|
+
throw new Error(`prerender: ${entryFile} does not export renderPage — run \`vite build --ssr\` first`);
|
|
63
|
+
return mod.renderPage;
|
|
64
|
+
}
|
|
65
|
+
/** Write one file under `dist`, creating the folders a nested route needs. */
|
|
66
|
+
export async function writeDist(distDir, file, contents) {
|
|
67
|
+
const target = join(distDir, file);
|
|
68
|
+
await mkdir(dirname(target), { recursive: true });
|
|
69
|
+
await writeFile(target, contents, "utf8");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Walk a page registry and write one share card per page.
|
|
73
|
+
*
|
|
74
|
+
* Every card is laid out BEFORE any is written, and a single line that does not fit refuses the
|
|
75
|
+
* whole run. That order is the point: a card that cannot hold its copy is a product decision,
|
|
76
|
+
* not something to resolve with an ellipsis — an ellipsis makes every string "fit", so
|
|
77
|
+
* overgrown copy has no failing case and ships a card missing the end of the one line the card
|
|
78
|
+
* exists to carry. The reader who finds out is someone else's link unfurl.
|
|
79
|
+
*
|
|
80
|
+
* Overflow is collected rather than thrown on the first card, so one run names every bad one:
|
|
81
|
+
* copy lands per locale in batches, and a build that dies on the first of six sends its
|
|
82
|
+
* operator round the loop six times.
|
|
83
|
+
*/
|
|
84
|
+
export async function writeOgCards({ pages, outDir, file, card, check = false, }) {
|
|
85
|
+
const laid = [];
|
|
86
|
+
const overflows = [];
|
|
87
|
+
for (const page of pages) {
|
|
88
|
+
const { png, overflow } = await card(page);
|
|
89
|
+
if (overflow?.length)
|
|
90
|
+
overflows.push(...overflow);
|
|
91
|
+
laid.push({ file: file(page), png });
|
|
92
|
+
}
|
|
93
|
+
if (overflows.length > 0)
|
|
94
|
+
throw new Error(`og: ${String(overflows.length)} card(s) cannot hold their copy —\n\n` +
|
|
95
|
+
overflows.map((o) => ` · ${describeOverflow(o)}`).join("\n\n") +
|
|
96
|
+
"\n\n Shorten the copy, or change the size ladder deliberately. Nothing was written.");
|
|
97
|
+
if (check)
|
|
98
|
+
return laid.map((entry) => entry.file);
|
|
99
|
+
await mkdir(outDir, { recursive: true });
|
|
100
|
+
for (const entry of laid)
|
|
101
|
+
await writeFile(join(outDir, entry.file), entry.png);
|
|
102
|
+
return laid.map((entry) => entry.file);
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=node.js.map
|
package/dist/node.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.js","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"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"}
|
package/dist/og.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fitting copy into a share card.
|
|
3
|
+
*
|
|
4
|
+
* The card's ART — the palette, the lockup, the size ladder — is the product, and it stays in
|
|
5
|
+
* the product. What ships here is the part two independent generators got wrong the same way:
|
|
6
|
+
* deciding what happens when a headline does not fit.
|
|
7
|
+
*
|
|
8
|
+
* Pure math, no `node:` import. The renderer that turns lines into SVG and SVG into a PNG is
|
|
9
|
+
* the caller's; `writeOgCards` in `node.ts` is the loop around it.
|
|
10
|
+
*/
|
|
11
|
+
/** The canvas every platform crops from. 1200×630 is the Open Graph size, not a brand choice. */
|
|
12
|
+
export declare const OG_CANVAS: {
|
|
13
|
+
readonly width: 1200;
|
|
14
|
+
readonly height: 630;
|
|
15
|
+
};
|
|
16
|
+
/** Copy that did not fit its column. */
|
|
17
|
+
export interface OgOverflow {
|
|
18
|
+
/** Which line of which card, so an operator can act on the report without grepping. */
|
|
19
|
+
label: string;
|
|
20
|
+
/** The string as given. */
|
|
21
|
+
text: string;
|
|
22
|
+
maxLines: number;
|
|
23
|
+
/** Characters per line at this size — the budget the text blew. */
|
|
24
|
+
maxChars: number;
|
|
25
|
+
size: number;
|
|
26
|
+
}
|
|
27
|
+
export interface FitOptions {
|
|
28
|
+
/** Pixel width of the column the text has to live in. */
|
|
29
|
+
width: number;
|
|
30
|
+
/** Font size, in pixels. */
|
|
31
|
+
size: number;
|
|
32
|
+
/** Hard cap on lines. */
|
|
33
|
+
maxLines: number;
|
|
34
|
+
/**
|
|
35
|
+
* Average glyph advance as a fraction of the font size.
|
|
36
|
+
*
|
|
37
|
+
* Character-budget estimation rather than real metrics: an SVG rasterizer gives no measuring
|
|
38
|
+
* API, and card copy is short enough that the estimate never drifts more than a word. Measure
|
|
39
|
+
* it off a rendered card and round UP — a budget that is too generous overflows the column,
|
|
40
|
+
* while one that is too mean only breaks a line early. Donor values: ~0.5 for a display face
|
|
41
|
+
* at headline sizes, ~0.46 for body copy.
|
|
42
|
+
*/
|
|
43
|
+
advance: number;
|
|
44
|
+
/** Names this line in an overflow report. `"pricing headline"`, not `"line 1"`. */
|
|
45
|
+
label: string;
|
|
46
|
+
}
|
|
47
|
+
export interface FitResult {
|
|
48
|
+
lines: readonly string[];
|
|
49
|
+
/** Set when the copy did not fit. See {@link fitText} for why this is reported rather than
|
|
50
|
+
* quietly truncated. */
|
|
51
|
+
overflow?: OgOverflow;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Greedy word wrap into a column, hard-capped at `maxLines`.
|
|
55
|
+
*
|
|
56
|
+
* Overflow is REPORTED, not truncated. The donor used to append `…`, which made wrapping a
|
|
57
|
+
* total function: every string "fitted", so copy that outgrew the column had no failing case to
|
|
58
|
+
* observe and shipped a card missing the end of the one line the card exists to carry. The
|
|
59
|
+
* reader who finds out is someone else's link unfurl. Cutting a headline to win an argument
|
|
60
|
+
* with a long sentence is a decision nobody reviewed; the size ladder is one somebody did.
|
|
61
|
+
*
|
|
62
|
+
* The truncated lines still come back, so a report can show what the card would have said, and
|
|
63
|
+
* so a `--check` run can lay every card out before refusing. One run names every bad card: copy
|
|
64
|
+
* lands per locale in batches, and a build that dies on the first of six sends its operator
|
|
65
|
+
* round the loop six times.
|
|
66
|
+
*/
|
|
67
|
+
export declare function fitText(text: string, options: FitOptions): FitResult;
|
|
68
|
+
/** One overflow, as the line an operator reads in a failed build. */
|
|
69
|
+
export declare function describeOverflow(overflow: OgOverflow): string;
|
|
70
|
+
//# sourceMappingURL=og.d.ts.map
|
package/dist/og.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"og.d.ts","sourceRoot":"","sources":["../src/og.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,iGAAiG;AACjG,eAAO,MAAM,SAAS;;;CAAwC,CAAC;AAE/D,wCAAwC;AACxC,MAAM,WAAW,UAAU;IACzB,uFAAuF;IACvF,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IACzB,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;;;OAQG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB;6BACyB;IACzB,QAAQ,CAAC,EAAE,UAAU,CAAC;CACvB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,SAAS,CAyBpE;AAED,qEAAqE;AACrE,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,CAM7D"}
|
package/dist/og.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fitting copy into a share card.
|
|
3
|
+
*
|
|
4
|
+
* The card's ART — the palette, the lockup, the size ladder — is the product, and it stays in
|
|
5
|
+
* the product. What ships here is the part two independent generators got wrong the same way:
|
|
6
|
+
* deciding what happens when a headline does not fit.
|
|
7
|
+
*
|
|
8
|
+
* Pure math, no `node:` import. The renderer that turns lines into SVG and SVG into a PNG is
|
|
9
|
+
* the caller's; `writeOgCards` in `node.ts` is the loop around it.
|
|
10
|
+
*/
|
|
11
|
+
/** The canvas every platform crops from. 1200×630 is the Open Graph size, not a brand choice. */
|
|
12
|
+
export const OG_CANVAS = { width: 1200, height: 630 };
|
|
13
|
+
/**
|
|
14
|
+
* Greedy word wrap into a column, hard-capped at `maxLines`.
|
|
15
|
+
*
|
|
16
|
+
* Overflow is REPORTED, not truncated. The donor used to append `…`, which made wrapping a
|
|
17
|
+
* total function: every string "fitted", so copy that outgrew the column had no failing case to
|
|
18
|
+
* observe and shipped a card missing the end of the one line the card exists to carry. The
|
|
19
|
+
* reader who finds out is someone else's link unfurl. Cutting a headline to win an argument
|
|
20
|
+
* with a long sentence is a decision nobody reviewed; the size ladder is one somebody did.
|
|
21
|
+
*
|
|
22
|
+
* The truncated lines still come back, so a report can show what the card would have said, and
|
|
23
|
+
* so a `--check` run can lay every card out before refusing. One run names every bad card: copy
|
|
24
|
+
* lands per locale in batches, and a build that dies on the first of six sends its operator
|
|
25
|
+
* round the loop six times.
|
|
26
|
+
*/
|
|
27
|
+
export function fitText(text, options) {
|
|
28
|
+
const { width, size, maxLines, advance, label } = options;
|
|
29
|
+
const maxChars = Math.max(8, Math.floor(width / (size * advance)));
|
|
30
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
31
|
+
const lines = [];
|
|
32
|
+
let line = "";
|
|
33
|
+
let overflowed = false;
|
|
34
|
+
for (const word of flat.split(" ").filter(Boolean)) {
|
|
35
|
+
const candidate = line ? `${line} ${word}` : word;
|
|
36
|
+
if (candidate.length <= maxChars || !line)
|
|
37
|
+
line = candidate;
|
|
38
|
+
else if (lines.length < maxLines - 1) {
|
|
39
|
+
lines.push(line);
|
|
40
|
+
line = word;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
overflowed = true;
|
|
44
|
+
line = `${candidate.slice(0, maxChars - 1).trimEnd()}…`;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (line)
|
|
49
|
+
lines.push(line);
|
|
50
|
+
return overflowed
|
|
51
|
+
? { lines, overflow: { label, text: flat, maxLines, maxChars, size } }
|
|
52
|
+
: { lines };
|
|
53
|
+
}
|
|
54
|
+
/** One overflow, as the line an operator reads in a failed build. */
|
|
55
|
+
export function describeOverflow(overflow) {
|
|
56
|
+
return (`${overflow.label} — ${String(overflow.maxLines)} lines of ~${String(overflow.maxChars)} ` +
|
|
57
|
+
`chars at ${String(overflow.size)}px, ${String(overflow.text.length)} chars given\n` +
|
|
58
|
+
` ${JSON.stringify(overflow.text)}`);
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=og.js.map
|
package/dist/og.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"og.js","sourceRoot":"","sources":["../src/og.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,iGAAiG;AACjG,MAAM,CAAC,MAAM,SAAS,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAW,CAAC;AA0C/D;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,OAAO,CAAC,IAAY,EAAE,OAAmB;IACvD,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IAC1D,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACnE,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAC9C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,UAAU,GAAG,KAAK,CAAC;IAEvB,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;QACnD,MAAM,SAAS,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QAClD,IAAI,SAAS,CAAC,MAAM,IAAI,QAAQ,IAAI,CAAC,IAAI;YAAE,IAAI,GAAG,SAAS,CAAC;aACvD,IAAI,KAAK,CAAC,MAAM,GAAG,QAAQ,GAAG,CAAC,EAAE,CAAC;YACrC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjB,IAAI,GAAG,IAAI,CAAC;QACd,CAAC;aAAM,CAAC;YACN,UAAU,GAAG,IAAI,CAAC;YAClB,IAAI,GAAG,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC;YACxD,MAAM;QACR,CAAC;IACH,CAAC;IACD,IAAI,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAE3B,OAAO,UAAU;QACf,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE;QACtE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;AAChB,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,gBAAgB,CAAC,QAAoB;IACnD,OAAO,CACL,GAAG,QAAQ,CAAC,KAAK,MAAM,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,cAAc,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG;QAC1F,YAAY,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,gBAAgB;QACpF,OAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CACvC,CAAC;AACJ,CAAC"}
|
package/dist/preset.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Plugin, UserConfig } from "vite";
|
|
2
|
+
/**
|
|
3
|
+
* Substitute `%NAME%` placeholders in `index.html`.
|
|
4
|
+
*
|
|
5
|
+
* `index.html` cannot import TypeScript, so a brand name or tagline written there is a literal
|
|
6
|
+
* that drifts from the one the app renders. This injects them from the module that owns them.
|
|
7
|
+
*
|
|
8
|
+
* Vite already replaces `%VITE_FOO%` from the environment, and that is the right tool when the
|
|
9
|
+
* value IS environment — an API URL, a build id. It is the wrong one for brand identity, which
|
|
10
|
+
* belongs in a typed module the app imports, not in a `.env` nobody reviews.
|
|
11
|
+
*/
|
|
12
|
+
export declare function htmlPlaceholders(values: Readonly<Record<string, string>>): Plugin;
|
|
13
|
+
export interface WebPresetOptions {
|
|
14
|
+
/** The app folder — the one holding `index.html`. In a `vite.config.ts` that is
|
|
15
|
+
* `import.meta.dirname`. `@` resolves to `<root>/src`. */
|
|
16
|
+
root: string;
|
|
17
|
+
/** Dev server port. Two apps in one repo must not share one, which is why it has no clever
|
|
18
|
+
* default beyond Vite's own. */
|
|
19
|
+
port?: number;
|
|
20
|
+
/** Preview server port. Defaults to `port - 1000`, the pairing both donors landed on
|
|
21
|
+
* (5173/4173, 5174/4174). */
|
|
22
|
+
previewPort?: number;
|
|
23
|
+
/** `{ BRAND_NAME: "Acme" }` replaces `%BRAND_NAME%` in `index.html`. */
|
|
24
|
+
placeholders?: Readonly<Record<string, string>>;
|
|
25
|
+
/**
|
|
26
|
+
* The workspace scope to bundle into the SSR build, as in `"@acme"`.
|
|
27
|
+
*
|
|
28
|
+
* INSURANCE, not a fix. Measured: Vite already bundles linked workspace dependencies in an
|
|
29
|
+
* SSR build — one donor runs without this declaration and its SSR output has zero bare
|
|
30
|
+
* imports. It is here because workspace packages are consumed as TypeScript SOURCE through
|
|
31
|
+
* subpath exports, and leaving them external would hand the runtime `.ts` files with
|
|
32
|
+
* Vite-only semantics in them (aliases, `?raw`, `define`) if that behaviour ever changed.
|
|
33
|
+
*/
|
|
34
|
+
ssrScope?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function webPreset({ root, port, previewPort, placeholders, ssrScope, }: WebPresetOptions): UserConfig;
|
|
37
|
+
//# sourceMappingURL=preset.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preset.d.ts","sourceRoot":"","sources":["../src/preset.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAE/C;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,CAUjF;AAED,MAAM,WAAW,gBAAgB;IAC/B;+DAC2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb;qCACiC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;kCAC8B;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,SAAS,CAAC,EACxB,IAAI,EACJ,IAAI,EACJ,WAAW,EACX,YAAY,EACZ,QAAQ,GACT,EAAE,gBAAgB,GAAG,UAAU,CAa/B"}
|
package/dist/preset.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `vite.config.ts` every app in this stack was writing by hand.
|
|
3
|
+
*
|
|
4
|
+
* Its own entry point (`@gusnips/vite/preset`) rather than the barrel, because it imports the
|
|
5
|
+
* React and Tailwind plugins: a prerender script that only wants `bakeHead` would otherwise
|
|
6
|
+
* load a build toolchain it never uses. Same reason the four packages are split at all.
|
|
7
|
+
*/
|
|
8
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
9
|
+
import react from "@vitejs/plugin-react";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
/**
|
|
12
|
+
* Substitute `%NAME%` placeholders in `index.html`.
|
|
13
|
+
*
|
|
14
|
+
* `index.html` cannot import TypeScript, so a brand name or tagline written there is a literal
|
|
15
|
+
* that drifts from the one the app renders. This injects them from the module that owns them.
|
|
16
|
+
*
|
|
17
|
+
* Vite already replaces `%VITE_FOO%` from the environment, and that is the right tool when the
|
|
18
|
+
* value IS environment — an API URL, a build id. It is the wrong one for brand identity, which
|
|
19
|
+
* belongs in a typed module the app imports, not in a `.env` nobody reviews.
|
|
20
|
+
*/
|
|
21
|
+
export function htmlPlaceholders(values) {
|
|
22
|
+
return {
|
|
23
|
+
name: "frontkit:html-placeholders",
|
|
24
|
+
transformIndexHtml(html) {
|
|
25
|
+
return Object.entries(values).reduce((out, [name, value]) => out.replaceAll(`%${name}%`, value), html);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function webPreset({ root, port, previewPort, placeholders, ssrScope, }) {
|
|
30
|
+
const preview = previewPort ?? (port === undefined ? undefined : port - 1000);
|
|
31
|
+
return {
|
|
32
|
+
plugins: [react(), tailwindcss(), ...(placeholders ? [htmlPlaceholders(placeholders)] : [])],
|
|
33
|
+
resolve: {
|
|
34
|
+
alias: { "@": path.resolve(root, "src") },
|
|
35
|
+
},
|
|
36
|
+
...(ssrScope && {
|
|
37
|
+
ssr: { noExternal: [new RegExp(`^${ssrScope.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/`)] },
|
|
38
|
+
}),
|
|
39
|
+
...(port !== undefined && { server: { port } }),
|
|
40
|
+
...(preview !== undefined && { preview: { port: preview } }),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=preset.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preset.js","sourceRoot":"","sources":["../src/preset.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,WAAW,MAAM,mBAAmB,CAAC;AAC5C,OAAO,KAAK,MAAM,sBAAsB,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAwC;IACvE,OAAO;QACL,IAAI,EAAE,4BAA4B;QAClC,kBAAkB,CAAC,IAAY;YAC7B,OAAO,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAClC,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,IAAI,GAAG,EAAE,KAAK,CAAC,EAC1D,IAAI,CACL,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC;AA0BD,MAAM,UAAU,SAAS,CAAC,EACxB,IAAI,EACJ,IAAI,EACJ,WAAW,EACX,YAAY,EACZ,QAAQ,GACS;IACjB,MAAM,OAAO,GAAG,WAAW,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAC9E,OAAO;QACL,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,WAAW,EAAE,EAAE,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC5F,OAAO,EAAE;YACP,KAAK,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE;SAC1C;QACD,GAAG,CAAC,QAAQ,IAAI;YACd,GAAG,EAAE,EAAE,UAAU,EAAE,CAAC,IAAI,MAAM,CAAC,IAAI,QAAQ,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE;SAC1F,CAAC;QACF,GAAG,CAAC,IAAI,KAAK,SAAS,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC;QAC/C,GAAG,CAAC,OAAO,KAAK,SAAS,IAAI,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,CAAC;KAC7D,CAAC;AACJ,CAAC"}
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One page, as the markup that goes inside `<div id="root">`.
|
|
3
|
+
*
|
|
4
|
+
* This is the build-time half of `main.tsx`, and the app's `entry-server.tsx` is expected to
|
|
5
|
+
* be four lines around it: the same `<App />` and the same route table the browser runs, with
|
|
6
|
+
* a `StaticRouter` in place of the history the build does not have. `main.tsx` is deliberately
|
|
7
|
+
* NOT reused — it reads `window.location`, registers listeners and starts analytics at module
|
|
8
|
+
* scope, none of which mean anything here.
|
|
9
|
+
*
|
|
10
|
+
* No `node:` import: this file is bundled into the SSR build by Vite, and it is React's own
|
|
11
|
+
* static renderer plus two string rules.
|
|
12
|
+
*/
|
|
13
|
+
import type { ReactNode } from "react";
|
|
14
|
+
/**
|
|
15
|
+
* Render a tree to markup with `prerender` from `react-dom/static`.
|
|
16
|
+
*
|
|
17
|
+
* **Never `renderToString`.** With `lazy()` routes behind a `<Suspense fallback={<Spinner />}>`,
|
|
18
|
+
* `renderToString` renders the FALLBACK — it would write a loading screen into every file and
|
|
19
|
+
* pass every gate that only asks whether the root has children. `prerender` waits for the tree
|
|
20
|
+
* to settle, which is also why this is async and why the whole renderer contract is.
|
|
21
|
+
*
|
|
22
|
+
* Two more halves of the same lesson are here too: `onError` is captured and rethrown, so a
|
|
23
|
+
* render failure fails the build rather than shipping a partial page; and an empty result is
|
|
24
|
+
* refused, because a router whose basename does not match its location answers `""` with no
|
|
25
|
+
* error and no warning.
|
|
26
|
+
*/
|
|
27
|
+
export declare function renderTree(tree: ReactNode): Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* What an SSR entry exports, and what {@link loadRenderer} looks for.
|
|
30
|
+
*
|
|
31
|
+
* `context` is opaque on purpose. A multi-locale app builds its i18n instance per call — three
|
|
32
|
+
* languages render in one process and a shared singleton would have them racing for one `lng` —
|
|
33
|
+
* while a single-locale app ignores the argument and keeps its singleton.
|
|
34
|
+
*/
|
|
35
|
+
export type PageRenderer<Context = unknown> = (route: string, context?: Context) => Promise<string>;
|
|
36
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAuBvC;;;;;;;;;;;;GAYG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAiBjE;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,CAAC,OAAO,GAAG,OAAO,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC"}
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// `static.browser`, not `static`. Three donor entry-servers converged on it independently, and
|
|
2
|
+
// the reason is resolution rather than behaviour: `react-dom/static` is condition-resolved into
|
|
3
|
+
// four different files (node, edge-light, workerd, browser) and an SSR bundler picks the
|
|
4
|
+
// condition, not us. `.browser` is one implementation everywhere. Measured, `prerender` hands
|
|
5
|
+
// back a Web `ReadableStream` from every one of them — which is what `new Response()` below
|
|
6
|
+
// wants — so this pins a resolution, it does not fix a stream type.
|
|
7
|
+
import { prerender } from "react-dom/static.browser";
|
|
8
|
+
/**
|
|
9
|
+
* React 19 hoists `<title>`, `<meta>` and `<link>` rendered anywhere in the tree into the
|
|
10
|
+
* document head — and on the server it does that by emitting them at the FRONT of the stream,
|
|
11
|
+
* for a caller that is expected to be assembling a whole document. We are not: we are filling
|
|
12
|
+
* one `<div>`, so an in-tree SEO component's tags would land inside the body, where that markup
|
|
13
|
+
* is invalid and duplicates the head the prerender bakes.
|
|
14
|
+
*
|
|
15
|
+
* They are dropped rather than lifted, because the page registry is the one source of that head
|
|
16
|
+
* by contract and the baked copy is the half that exists before React runs. `<style>` and
|
|
17
|
+
* `<script>` are deliberately NOT in this pattern: if one appears here it belongs to the page
|
|
18
|
+
* and must survive.
|
|
19
|
+
*/
|
|
20
|
+
const HOISTED_HEAD = /^(?:<title>[^<]*<\/title>|<meta\b[^>]*\/?>|<link\b[^>]*\/?>|\s+)+/;
|
|
21
|
+
/**
|
|
22
|
+
* Render a tree to markup with `prerender` from `react-dom/static`.
|
|
23
|
+
*
|
|
24
|
+
* **Never `renderToString`.** With `lazy()` routes behind a `<Suspense fallback={<Spinner />}>`,
|
|
25
|
+
* `renderToString` renders the FALLBACK — it would write a loading screen into every file and
|
|
26
|
+
* pass every gate that only asks whether the root has children. `prerender` waits for the tree
|
|
27
|
+
* to settle, which is also why this is async and why the whole renderer contract is.
|
|
28
|
+
*
|
|
29
|
+
* Two more halves of the same lesson are here too: `onError` is captured and rethrown, so a
|
|
30
|
+
* render failure fails the build rather than shipping a partial page; and an empty result is
|
|
31
|
+
* refused, because a router whose basename does not match its location answers `""` with no
|
|
32
|
+
* error and no warning.
|
|
33
|
+
*/
|
|
34
|
+
export async function renderTree(tree) {
|
|
35
|
+
let failure;
|
|
36
|
+
const { prelude } = await prerender(tree, {
|
|
37
|
+
onError(error) {
|
|
38
|
+
failure ??= error;
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
const html = await new Response(prelude).text();
|
|
42
|
+
if (failure !== undefined)
|
|
43
|
+
throw failure;
|
|
44
|
+
const markup = html.replace(HOISTED_HEAD, "");
|
|
45
|
+
if (markup.trim() === "")
|
|
46
|
+
throw new Error("renderTree: the tree rendered to nothing — usually a router whose location or basename " +
|
|
47
|
+
"matches no route, which React reports as an empty string rather than an error");
|
|
48
|
+
return markup;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAaA,+FAA+F;AAC/F,gGAAgG;AAChG,yFAAyF;AACzF,8FAA8F;AAC9F,4FAA4F;AAC5F,oEAAoE;AACpE,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAErD;;;;;;;;;;;GAWG;AACH,MAAM,YAAY,GAAG,mEAAmE,CAAC;AAEzF;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAe;IAC9C,IAAI,OAAgB,CAAC;IACrB,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,IAAI,EAAE;QACxC,OAAO,CAAC,KAAc;YACpB,OAAO,KAAK,KAAK,CAAC;QACpB,CAAC;KACF,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,MAAM,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;IAChD,IAAI,OAAO,KAAK,SAAS;QAAE,MAAM,OAAO,CAAC;IAEzC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;IAC9C,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE;QACtB,MAAM,IAAI,KAAK,CACb,yFAAyF;YACvF,+EAA+E,CAClF,CAAC;IACJ,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Alternate } from "./head.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Where a rendered page lands.
|
|
4
|
+
*
|
|
5
|
+
* FLAT (`pricing.html`), not directory-style (`pricing/index.html`). Cloudflare Pages serves
|
|
6
|
+
* the directory form at `/pricing/` and answers `/pricing` with a 308 to it — so every address
|
|
7
|
+
* the app advertises in its canonical and its sitemap would be a redirect rather than a page.
|
|
8
|
+
* A flat file is served at `/pricing` AND `/pricing/`, 200 either way.
|
|
9
|
+
*
|
|
10
|
+
* A nested route keeps its folders: `/guides/errors` → `guides/errors.html`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function pageFile(routePath: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* The origin every canonical, share URL and sitemap entry is built on.
|
|
15
|
+
*
|
|
16
|
+
* The trailing slash is dropped because the caller appends a path that starts with one, and
|
|
17
|
+
* `https://example.com//pricing` is a different address to every crawler that reads it. The
|
|
18
|
+
* scheme is demanded because the value normally comes from an env var: `VITE_SITE_URL=acme.com`
|
|
19
|
+
* looks right in a `.env`, and it silently turns every canonical on the site into a relative
|
|
20
|
+
* URL. That is a bad build, not a bad page, so it fails here.
|
|
21
|
+
*/
|
|
22
|
+
export declare function siteOrigin(url: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* The three things about a public page that do not translate: where it is, how often it
|
|
25
|
+
* changes, and how it ranks against its siblings.
|
|
26
|
+
*
|
|
27
|
+
* A product's registry extends this with its own copy fields — one donor names an i18n key per
|
|
28
|
+
* page, another keys its locale catalogs by `pageSlug` and stores no copy here at all. Where
|
|
29
|
+
* the copy lives is the product's call. These three are what a sitemap needs from every one of
|
|
30
|
+
* them, and the registry is the single source the prerender, the sitemap and the share cards
|
|
31
|
+
* all walk, so a page can never be in one and missing from another.
|
|
32
|
+
*/
|
|
33
|
+
export interface PublicPage {
|
|
34
|
+
path: string;
|
|
35
|
+
/** Relative crawl priority, 0.0–1.0. */
|
|
36
|
+
priority: number;
|
|
37
|
+
changeFrequency: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
|
|
38
|
+
}
|
|
39
|
+
/** URL slug for a page path (`/pricing` → `pricing`, `/` → `home`), used to name its share
|
|
40
|
+
* card and to key a locale catalog's copy. */
|
|
41
|
+
export declare function pageSlug(path: string): string;
|
|
42
|
+
/** Public path of a page's generated share card (`/pricing` → `/og/pricing.png`). */
|
|
43
|
+
export declare function ogImagePath(path: string): string;
|
|
44
|
+
export interface SitemapEntry {
|
|
45
|
+
loc: string;
|
|
46
|
+
changefreq: string;
|
|
47
|
+
/** Already formatted — an app ranks its own pages, and that rule does not belong here. */
|
|
48
|
+
priority: string;
|
|
49
|
+
/** `YYYY-MM-DD`. The one hint in a sitemap Google actually reads. One date for the whole
|
|
50
|
+
* build is the honest answer: these pages ship together. */
|
|
51
|
+
lastmod?: string;
|
|
52
|
+
/** The same reciprocal set the page's `<head>` carries. Stated twice on purpose: a crawler
|
|
53
|
+
* that reaches an address through the sitemap has not read the head yet. */
|
|
54
|
+
alternates?: readonly Alternate[];
|
|
55
|
+
}
|
|
56
|
+
/** `sitemap.xml`, from whichever registry the caller walks. */
|
|
57
|
+
export declare function sitemapXml(entries: readonly SitemapEntry[]): string;
|
|
58
|
+
/**
|
|
59
|
+
* `sitemap.xml` for a single-language site, straight from the registry — the common case, and
|
|
60
|
+
* the reason a registry exists: the sitemap can never drift from the routes the app serves.
|
|
61
|
+
*
|
|
62
|
+
* A localized site walks its own locales and calls {@link sitemapXml}, because only it knows
|
|
63
|
+
* how an address carries a language.
|
|
64
|
+
*/
|
|
65
|
+
export declare function sitemapFor(origin: string, pages: readonly PublicPage[], lastmod?: string): string;
|
|
66
|
+
export interface RobotsOptions {
|
|
67
|
+
/** The origin this file is served from. Every sitemap path is resolved against it, so the
|
|
68
|
+
* file can never advertise a host it is not on — a mistake one donor is still shipping,
|
|
69
|
+
* where a static `robots.txt` and the build's fallback name different domains. */
|
|
70
|
+
origin: string;
|
|
71
|
+
/** Every sitemap on this ORIGIN, as paths (`/sitemap.xml`, `/docs/sitemap.xml`).
|
|
72
|
+
*
|
|
73
|
+
* A crawler reads only the robots.txt at the origin root. An app served from a
|
|
74
|
+
* subdirectory therefore cannot ship its own — the one file at the root has to list its
|
|
75
|
+
* sitemap too, or nothing ever finds it. */
|
|
76
|
+
sitemaps: readonly string[];
|
|
77
|
+
/** Paths to keep out of every index. A page nobody should be able to find by searching —
|
|
78
|
+
* a per-request status page, an unsubscribe link — belongs here AND in `noindex`. */
|
|
79
|
+
disallow?: readonly string[];
|
|
80
|
+
}
|
|
81
|
+
export declare function robotsTxt({ origin, sitemaps, disallow }: RobotsOptions): string;
|
|
82
|
+
//# sourceMappingURL=sitemap.d.ts.map
|
|
@@ -0,0 +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;;;;;;;;;GASG;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
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three build outputs that are addresses rather than pages: where a rendered page lands,
|
|
3
|
+
* `sitemap.xml`, and `robots.txt`.
|
|
4
|
+
*
|
|
5
|
+
* Pure string work, like `head.ts` — no `node:` import, so every rule here is unit-testable.
|
|
6
|
+
*/
|
|
7
|
+
import { escapeAttr } from "./escape.js";
|
|
8
|
+
/**
|
|
9
|
+
* Where a rendered page lands.
|
|
10
|
+
*
|
|
11
|
+
* FLAT (`pricing.html`), not directory-style (`pricing/index.html`). Cloudflare Pages serves
|
|
12
|
+
* the directory form at `/pricing/` and answers `/pricing` with a 308 to it — so every address
|
|
13
|
+
* the app advertises in its canonical and its sitemap would be a redirect rather than a page.
|
|
14
|
+
* A flat file is served at `/pricing` AND `/pricing/`, 200 either way.
|
|
15
|
+
*
|
|
16
|
+
* A nested route keeps its folders: `/guides/errors` → `guides/errors.html`.
|
|
17
|
+
*/
|
|
18
|
+
export function pageFile(routePath) {
|
|
19
|
+
const trimmed = routePath.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
20
|
+
return trimmed === "" ? "index.html" : `${trimmed}.html`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The origin every canonical, share URL and sitemap entry is built on.
|
|
24
|
+
*
|
|
25
|
+
* The trailing slash is dropped because the caller appends a path that starts with one, and
|
|
26
|
+
* `https://example.com//pricing` is a different address to every crawler that reads it. The
|
|
27
|
+
* scheme is demanded because the value normally comes from an env var: `VITE_SITE_URL=acme.com`
|
|
28
|
+
* looks right in a `.env`, and it silently turns every canonical on the site into a relative
|
|
29
|
+
* URL. That is a bad build, not a bad page, so it fails here.
|
|
30
|
+
*/
|
|
31
|
+
export function siteOrigin(url) {
|
|
32
|
+
if (!/^https?:\/\/[^/]+/.test(url))
|
|
33
|
+
throw new Error(`prerender: "${url}" is not an absolute origin — it needs a scheme, as in https://example.com`);
|
|
34
|
+
return url.replace(/\/+$/, "");
|
|
35
|
+
}
|
|
36
|
+
/** URL slug for a page path (`/pricing` → `pricing`, `/` → `home`), used to name its share
|
|
37
|
+
* card and to key a locale catalog's copy. */
|
|
38
|
+
export function pageSlug(path) {
|
|
39
|
+
const trimmed = path.replace(/^\/+|\/+$/g, "");
|
|
40
|
+
return trimmed === "" ? "home" : trimmed.replaceAll("/", "-");
|
|
41
|
+
}
|
|
42
|
+
/** Public path of a page's generated share card (`/pricing` → `/og/pricing.png`). */
|
|
43
|
+
export function ogImagePath(path) {
|
|
44
|
+
return `/og/${pageSlug(path)}.png`;
|
|
45
|
+
}
|
|
46
|
+
/** `sitemap.xml`, from whichever registry the caller walks. */
|
|
47
|
+
export function sitemapXml(entries) {
|
|
48
|
+
const urls = entries
|
|
49
|
+
.map((entry) => {
|
|
50
|
+
const links = (entry.alternates ?? []).map((alt) => ` <xhtml:link rel="alternate" hreflang="${escapeAttr(alt.hreflang)}" href="${escapeAttr(alt.href)}" />\n`);
|
|
51
|
+
return (` <url>\n` +
|
|
52
|
+
// Escaped like every other URL here: a query string with an `&` in it is not valid XML,
|
|
53
|
+
// and an invalid sitemap is rejected whole rather than per entry.
|
|
54
|
+
` <loc>${escapeAttr(entry.loc)}</loc>\n` +
|
|
55
|
+
links.join("") +
|
|
56
|
+
(entry.lastmod ? ` <lastmod>${entry.lastmod}</lastmod>\n` : "") +
|
|
57
|
+
` <changefreq>${entry.changefreq}</changefreq>\n` +
|
|
58
|
+
` <priority>${entry.priority}</priority>\n` +
|
|
59
|
+
` </url>`);
|
|
60
|
+
})
|
|
61
|
+
.join("\n");
|
|
62
|
+
return (`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
63
|
+
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"` +
|
|
64
|
+
` xmlns:xhtml="http://www.w3.org/1999/xhtml">\n${urls}\n</urlset>\n`);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* `sitemap.xml` for a single-language site, straight from the registry — the common case, and
|
|
68
|
+
* the reason a registry exists: the sitemap can never drift from the routes the app serves.
|
|
69
|
+
*
|
|
70
|
+
* A localized site walks its own locales and calls {@link sitemapXml}, because only it knows
|
|
71
|
+
* how an address carries a language.
|
|
72
|
+
*/
|
|
73
|
+
export function sitemapFor(origin, pages, lastmod) {
|
|
74
|
+
const root = siteOrigin(origin);
|
|
75
|
+
return sitemapXml(pages.map((page) => ({
|
|
76
|
+
loc: `${root}${page.path}`,
|
|
77
|
+
changefreq: page.changeFrequency,
|
|
78
|
+
// One decimal, always. `<priority>` is a number a crawler compares, and floating-point
|
|
79
|
+
// noise from a computed rank would print as `0.7000000000000001`.
|
|
80
|
+
priority: page.priority.toFixed(1),
|
|
81
|
+
...(lastmod !== undefined && { lastmod }),
|
|
82
|
+
})));
|
|
83
|
+
}
|
|
84
|
+
export function robotsTxt({ origin, sitemaps, disallow = [] }) {
|
|
85
|
+
const root = siteOrigin(origin);
|
|
86
|
+
return (`User-agent: *\nAllow: /\n` +
|
|
87
|
+
disallow.map((path) => `Disallow: ${path}\n`).join("") +
|
|
88
|
+
`\n` +
|
|
89
|
+
sitemaps.map((path) => `Sitemap: ${root}${path}\n`).join(""));
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=sitemap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap.js","sourceRoot":"","sources":["../src/sitemap.ts"],"names":[],"mappings":"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"}
|