@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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/dist/escape.d.ts +6 -0
  4. package/dist/escape.d.ts.map +1 -0
  5. package/dist/escape.js +6 -0
  6. package/dist/escape.js.map +1 -0
  7. package/dist/head.d.ts +110 -0
  8. package/dist/head.d.ts.map +1 -0
  9. package/dist/head.js +183 -0
  10. package/dist/head.js.map +1 -0
  11. package/dist/index.d.ts +23 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +22 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/node.d.ts +57 -0
  16. package/dist/node.d.ts.map +1 -0
  17. package/dist/node.js +104 -0
  18. package/dist/node.js.map +1 -0
  19. package/dist/og.d.ts +70 -0
  20. package/dist/og.d.ts.map +1 -0
  21. package/dist/og.js +60 -0
  22. package/dist/og.js.map +1 -0
  23. package/dist/preset.d.ts +37 -0
  24. package/dist/preset.d.ts.map +1 -0
  25. package/dist/preset.js +43 -0
  26. package/dist/preset.js.map +1 -0
  27. package/dist/render.d.ts +36 -0
  28. package/dist/render.d.ts.map +1 -0
  29. package/dist/render.js +50 -0
  30. package/dist/render.js.map +1 -0
  31. package/dist/sitemap.d.ts +82 -0
  32. package/dist/sitemap.d.ts.map +1 -0
  33. package/dist/sitemap.js +91 -0
  34. package/dist/sitemap.js.map +1 -0
  35. package/package.json +105 -0
  36. package/src/escape.ts +8 -0
  37. package/src/head.test.ts +258 -0
  38. package/src/head.ts +288 -0
  39. package/src/index.ts +59 -0
  40. package/src/node.test.ts +102 -0
  41. package/src/node.ts +140 -0
  42. package/src/og.test.ts +53 -0
  43. package/src/og.ts +103 -0
  44. package/src/preset.ts +78 -0
  45. package/src/render.test.ts +67 -0
  46. package/src/render.ts +75 -0
  47. package/src/sitemap.test.ts +124 -0
  48. 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
@@ -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
@@ -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"}
@@ -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"}
@@ -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"}
@@ -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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"}