@iterant/site-runtime 3.11.1 → 3.12.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/docs/runtime-contract.md +57 -4
- package/package.json +1 -1
- package/src/config/preset.ts +36 -1
- package/src/config/site-module.ts +97 -0
- package/src/config/virtual-modules.d.ts +16 -0
- package/src/layouts/LayoutCore.astro +7 -1
- package/src/lib/folder.ts +33 -0
- package/src/routes/index.ts +8 -3
- package/src/routes/llms-txt-injected.ts +11 -0
- package/src/routes/llms-txt.ts +7 -1
- package/src/routes/robots-txt.ts +13 -2
package/docs/runtime-contract.md
CHANGED
|
@@ -50,7 +50,7 @@ runtime and says so.
|
|
|
50
50
|
|
|
51
51
|
<!-- generated: available libraries -->
|
|
52
52
|
|
|
53
|
-
_Generated from package.json by scripts/generate-kit-table.mjs. Runtime 3.
|
|
53
|
+
_Generated from package.json by scripts/generate-kit-table.mjs. Runtime 3.12.0._
|
|
54
54
|
|
|
55
55
|
**Toolchain** (this package owns the version; do NOT declare these):
|
|
56
56
|
|
|
@@ -526,9 +526,11 @@ platform and must remain a string literal, so the sitemap helper emits against a
|
|
|
526
526
|
placeholder host and swaps it at emit time.
|
|
527
527
|
|
|
528
528
|
Platform routes stay VISIBLE files in `src/pages/`, each a shim over a package
|
|
529
|
-
handler
|
|
530
|
-
|
|
531
|
-
|
|
529
|
+
handler: existence and addressing stay repo-owned so the `src/pages/` mental
|
|
530
|
+
model holds, while the behavior rides the bump. One route is addressed by the
|
|
531
|
+
platform instead, llms.txt under a brand folder (see The brand folder), because
|
|
532
|
+
no file name a template ships can carry a per-brand path segment. A repo carries
|
|
533
|
+
FOUR of them, all four unconditionally:
|
|
532
534
|
|
|
533
535
|
| File | Handler | `prerender` |
|
|
534
536
|
| -------------------------------------- | --------------------------- | ----------- |
|
|
@@ -569,6 +571,57 @@ extends rather than ejects: extra integrations and vite plugins append through
|
|
|
569
571
|
`overrides`, and any other key it needs it writes in its own `defineConfig`
|
|
570
572
|
object after the spread.
|
|
571
573
|
|
|
574
|
+
### The brand folder (3.12.0)
|
|
575
|
+
|
|
576
|
+
Every platform file a build emits can live under one path segment, the brand's
|
|
577
|
+
folder:
|
|
578
|
+
|
|
579
|
+
```js
|
|
580
|
+
import { SITE_CONFIG } from "./src/site-config";
|
|
581
|
+
|
|
582
|
+
export default defineConfig({
|
|
583
|
+
site: "https://example.com",
|
|
584
|
+
...iterantStarter({ folder: SITE_CONFIG.folder }),
|
|
585
|
+
});
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
It exists for the customer's own domain. A brand served in a subfolder is
|
|
589
|
+
reached through one forwarding rule, `example.com/feed/*`, and anything a page
|
|
590
|
+
asks for outside that prefix 404s on the customer's site. So with
|
|
591
|
+
`folder: "feed"` set:
|
|
592
|
+
|
|
593
|
+
| What | Where it lands |
|
|
594
|
+
| -------------------------------- | --------------------------------------- |
|
|
595
|
+
| hashed assets (`build.assets`) | `/feed/_astro/*`, fonts included |
|
|
596
|
+
| immutable `Cache-Control` rule | `/feed/_astro/*`, written by the adapter |
|
|
597
|
+
| sitemap index and its children | `/feed/sitemap-index.xml`, `/feed/sitemap-0.xml` |
|
|
598
|
+
| llms.txt | `/feed/llms.txt`, injected by the preset |
|
|
599
|
+
| the head's icon link | `/feed/favicon.ico` |
|
|
600
|
+
| robots.txt | the root, with its `Sitemap:` line in the folder |
|
|
601
|
+
| the mirrored legacy sitemaps | the root, the URLs the old site advertised |
|
|
602
|
+
|
|
603
|
+
**Pages do not move.** A page keeps the public path it is authored at, in every
|
|
604
|
+
serving mode, and every `<loc>` in the sitemap is that same path. The folder is
|
|
605
|
+
where the platform's own files live, not a prefix for the site.
|
|
606
|
+
|
|
607
|
+
The value is the backend's `brand.iterant_subfolder`, written into
|
|
608
|
+
`SITE_CONFIG.folder` beside the fonts, and `astro.config.mjs` hands it to the
|
|
609
|
+
preset. The PRESET is then the only source: it places the hashed assets and the
|
|
610
|
+
sitemap, and it publishes the folder to the package's own code through a virtual
|
|
611
|
+
module, which is where the layout's icon link, `robots.txt` and llms.txt read
|
|
612
|
+
it. Nothing reads `SITE_CONFIG.folder` a second time, so the value cannot
|
|
613
|
+
disagree with itself. It must be one lower-case segment of letters, digits and
|
|
614
|
+
hyphens, the shape Django validates (`backend/brands/folder.py`), which also
|
|
615
|
+
refuses the reserved names and anything shaped like a language code. Absent or
|
|
616
|
+
empty, everything stays at the root, byte-identical to a build before this
|
|
617
|
+
option existed.
|
|
618
|
+
|
|
619
|
+
Two repo-side consequences: `public/favicon.ico` moves to
|
|
620
|
+
`public/<folder>/favicon.ico`, and the root `src/pages/llms.txt.ts` goes away,
|
|
621
|
+
since the preset addresses that route. A repo that keeps the root file still
|
|
622
|
+
builds and still serves `/llms.txt`, at a URL the customer's domain does not
|
|
623
|
+
forward; the build warns until the file is deleted.
|
|
624
|
+
|
|
572
625
|
### Web fonts (3.8.0)
|
|
573
626
|
|
|
574
627
|
A site names its font families once, in `SITE_CONFIG.fonts`, and passes the same
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iterant/site-runtime",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.12.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The platform layer every Iterant brand site runs on: content grammar, collection schemas, SEO head and JSON-LD, layout core, Astro config preset, dev integrations and the verify gates.",
|
|
6
6
|
"scripts": {
|
package/src/config/preset.ts
CHANGED
|
@@ -13,12 +13,14 @@ import {
|
|
|
13
13
|
findFontFamily,
|
|
14
14
|
} from "../fonts/catalog";
|
|
15
15
|
import { DEFAULT_CHROME_DIR, DEFAULT_PAGES_DIR } from "../lib/content-paths";
|
|
16
|
+
import { assertFolder } from "../lib/folder";
|
|
16
17
|
import devServerSignals from "../integrations/dev-server-signals.mjs";
|
|
17
18
|
import iterantPlugins from "../integrations/iterant-plugins.mjs";
|
|
18
19
|
import newFileReload from "../integrations/new-file-reload.mjs";
|
|
19
20
|
import previewErrorShell from "../integrations/preview-error-shell.mjs";
|
|
20
21
|
import siteConfigWatch from "../integrations/site-config-watch.mjs";
|
|
21
22
|
import { sitemapWithCustomPages } from "../lib/sitemap";
|
|
23
|
+
import { siteModule } from "./site-module";
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* The browser floor every brand site's CSS is compiled for, stated rather than
|
|
@@ -167,6 +169,19 @@ export interface IterantStarterOptions {
|
|
|
167
169
|
* `catalogFontFamilies` measures.
|
|
168
170
|
*/
|
|
169
171
|
fonts?: readonly string[] | "catalog";
|
|
172
|
+
/**
|
|
173
|
+
* The one path segment this brand's platform files live under (`"feed"`), the
|
|
174
|
+
* backend's `brand.iterant_subfolder` as the repo's site config carries it.
|
|
175
|
+
* A repo passes `SITE_CONFIG.folder`, the same way it passes the fonts.
|
|
176
|
+
*
|
|
177
|
+
* Set, the hashed assets, the sitemap index and llms.txt all move under it,
|
|
178
|
+
* so the customer's edge forwards `example.com/feed/*` and every file a page
|
|
179
|
+
* needs arrives. PAGES DO NOT MOVE: they keep the public paths they are
|
|
180
|
+
* authored at, here and in every serving mode. Absent or empty is the root,
|
|
181
|
+
* byte-identical to a build before folders existed, so a repo that has not
|
|
182
|
+
* taken the upgrade keeps building.
|
|
183
|
+
*/
|
|
184
|
+
folder?: string;
|
|
170
185
|
/**
|
|
171
186
|
* The repo's site config module, relative to the project root. In list mode
|
|
172
187
|
* it is watched, so a change to `SITE_CONFIG.fonts` restarts the dev server
|
|
@@ -200,9 +215,11 @@ export function iterantStarter({
|
|
|
200
215
|
pagesDir = DEFAULT_PAGES_DIR,
|
|
201
216
|
chromeDir = DEFAULT_CHROME_DIR,
|
|
202
217
|
fonts = [],
|
|
218
|
+
folder = "",
|
|
203
219
|
siteConfigPath = "src/site-config.ts",
|
|
204
220
|
overrides,
|
|
205
221
|
}: IterantStarterOptions = {}) {
|
|
222
|
+
const siteFolder = folder === "" ? "" : assertFolder(folder);
|
|
206
223
|
// A repo's own wrangler config, when it has one. The template checks in a
|
|
207
224
|
// wrangler.jsonc and its compatibility_date decides how the adapter's workerd
|
|
208
225
|
// prerender behaves, so passing configPath is not a detail; the lookup stays
|
|
@@ -225,7 +242,20 @@ export function iterantStarter({
|
|
|
225
242
|
const integrations: NonNullable<AstroUserConfig["integrations"]> = [
|
|
226
243
|
react(),
|
|
227
244
|
// For SSR-only dynamic routes, the page entries' routes are read from disk.
|
|
228
|
-
|
|
245
|
+
// Under a folder the index and its children land in it; every <loc> stays a
|
|
246
|
+
// public path, because the pages did not move. @astrojs/sitemap creates
|
|
247
|
+
// dist/client and nothing below it, so the folder directory is there because
|
|
248
|
+
// `build.assets` below wrote the hashed assets into it first: a site that
|
|
249
|
+
// emitted no assets at all would ENOENT here rather than skip the sitemap.
|
|
250
|
+
...sitemapWithCustomPages({
|
|
251
|
+
pagesDir,
|
|
252
|
+
...(siteFolder && { filenameBase: `${siteFolder}/sitemap` }),
|
|
253
|
+
}),
|
|
254
|
+
// The folder and the site config, as a module the package's own layout and
|
|
255
|
+
// routes can read. Always registered: those readers do not care whether this
|
|
256
|
+
// site has a folder, and with none they read `""` and address the root.
|
|
257
|
+
// With one, it also injects /<folder>/llms.txt.
|
|
258
|
+
siteModule({ folder: siteFolder, siteConfigPath }),
|
|
229
259
|
// Dev-only: platform visual editor, gated on ?editor=1 in an iframe.
|
|
230
260
|
iterantPlugins(),
|
|
231
261
|
// Dev-only: swap 5xx error pages for the branded in-progress shell.
|
|
@@ -323,6 +353,11 @@ export function iterantStarter({
|
|
|
323
353
|
...(site !== undefined && { site }),
|
|
324
354
|
output: "server" as const,
|
|
325
355
|
trailingSlash: "never" as const,
|
|
356
|
+
// Hashed assets under the brand's folder, so one forwarding rule on the
|
|
357
|
+
// customer's domain covers every stylesheet, script and image a page asks
|
|
358
|
+
// for. The Cloudflare adapter reads this key too and writes its immutable
|
|
359
|
+
// Cache-Control rule for the same prefix into _headers.
|
|
360
|
+
...(siteFolder && { build: { assets: `${siteFolder}/_astro` } }),
|
|
326
361
|
// No Astro sessions: the in-memory driver stops the Cloudflare adapter from
|
|
327
362
|
// auto-provisioning a KV namespace per deploy.
|
|
328
363
|
session: {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import type { AstroIntegration } from "astro";
|
|
4
|
+
|
|
5
|
+
// The platform's build-time facts, under a fixed module id: the brand's folder
|
|
6
|
+
// as the preset was given it, and the repo's site config. A repo never imports
|
|
7
|
+
// this, its own files import `@/site-config` directly; package code does,
|
|
8
|
+
// because the layout, robots.txt and the injected llms.txt route all need the
|
|
9
|
+
// folder and none of them is called from the config.
|
|
10
|
+
//
|
|
11
|
+
// It is what makes the folder ONE value. The preset receives it, the assets and
|
|
12
|
+
// the sitemap go under it, and every other reader takes it from here, so there
|
|
13
|
+
// is no second copy for a repo to set differently.
|
|
14
|
+
const SITE_ID = "virtual:iterant/site";
|
|
15
|
+
const RESOLVED_SITE_ID = `\0${SITE_ID}`;
|
|
16
|
+
|
|
17
|
+
export interface SiteModuleOptions {
|
|
18
|
+
/** The folder, already validated. Empty means the site serves from the root. */
|
|
19
|
+
folder: string;
|
|
20
|
+
/** The repo's site config module, relative to the project root. */
|
|
21
|
+
siteConfigPath: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The virtual module above, and the one route this package addresses itself.
|
|
26
|
+
*
|
|
27
|
+
* llms.txt has to move under a folder, and no file name the template ships can
|
|
28
|
+
* carry a per-brand path segment, so with a folder set the preset injects the
|
|
29
|
+
* handler the repo used to mount at the root. A repo that still has the root
|
|
30
|
+
* src/pages/llms.txt.ts keeps serving /llms.txt too, at a different URL, so
|
|
31
|
+
* nothing collides; the file is dead weight on the customer's domain, where only
|
|
32
|
+
* the folder is forwarded, and the starter upgrade deletes it. The warning below
|
|
33
|
+
* says so on every build until it does.
|
|
34
|
+
*/
|
|
35
|
+
export function siteModule({
|
|
36
|
+
folder,
|
|
37
|
+
siteConfigPath,
|
|
38
|
+
}: SiteModuleOptions): AstroIntegration {
|
|
39
|
+
return {
|
|
40
|
+
name: "iterant-site",
|
|
41
|
+
hooks: {
|
|
42
|
+
"astro:config:setup": ({ config, injectRoute, logger, updateConfig }) => {
|
|
43
|
+
const siteConfigFile = fileURLToPath(
|
|
44
|
+
new URL(siteConfigPath, config.root),
|
|
45
|
+
);
|
|
46
|
+
updateConfig({
|
|
47
|
+
vite: {
|
|
48
|
+
plugins: [
|
|
49
|
+
{
|
|
50
|
+
name: "iterant-site-module",
|
|
51
|
+
resolveId: (id: string) =>
|
|
52
|
+
id === SITE_ID ? RESOLVED_SITE_ID : null,
|
|
53
|
+
load: (id: string) =>
|
|
54
|
+
id === RESOLVED_SITE_ID
|
|
55
|
+
? `export { SITE_CONFIG } from ${JSON.stringify(siteConfigFile)};\nexport const FOLDER = ${JSON.stringify(folder)};\n`
|
|
56
|
+
: null,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
if (!folder) return;
|
|
62
|
+
injectRoute({
|
|
63
|
+
pattern: `/${folder}/llms.txt`,
|
|
64
|
+
entrypoint: new URL(
|
|
65
|
+
"../routes/llms-txt-injected.ts",
|
|
66
|
+
import.meta.url,
|
|
67
|
+
),
|
|
68
|
+
prerender: true,
|
|
69
|
+
});
|
|
70
|
+
if (
|
|
71
|
+
existsSync(
|
|
72
|
+
fileURLToPath(new URL("src/pages/llms.txt.ts", config.root)),
|
|
73
|
+
)
|
|
74
|
+
) {
|
|
75
|
+
logger.warn(
|
|
76
|
+
`src/pages/llms.txt.ts still serves /llms.txt; delete it so llms.txt lives under /${folder} alone`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
// The module's TYPES, into the repo's .astro/ where its tsconfig already
|
|
81
|
+
// reads them. Without this a consumer's `astro check` has no declaration
|
|
82
|
+
// for the id and reports TS2307 in three package files it type-checks
|
|
83
|
+
// through their imports. The text is the declaration this package ships
|
|
84
|
+
// and checks itself against, read rather than repeated, so the two
|
|
85
|
+
// readings cannot drift.
|
|
86
|
+
"astro:config:done": ({ injectTypes }) => {
|
|
87
|
+
injectTypes({
|
|
88
|
+
filename: "site.d.ts",
|
|
89
|
+
content: readFileSync(
|
|
90
|
+
new URL("./virtual-modules.d.ts", import.meta.url),
|
|
91
|
+
"utf8",
|
|
92
|
+
),
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The platform's build-time facts, served by the vite plugin in ./site-module.ts
|
|
2
|
+
// and read by the package's own routes and layout. A repo imports its site
|
|
3
|
+
// config directly; this id exists because that code is not called from the
|
|
4
|
+
// config and cannot be handed the folder any other way.
|
|
5
|
+
//
|
|
6
|
+
// This file is BOTH the package's own declaration and the one every consumer
|
|
7
|
+
// gets: the integration reads it and injects it into the repo's .astro/ at
|
|
8
|
+
// astro:config:done, so a brand's `astro check` resolves the id too.
|
|
9
|
+
declare module "virtual:iterant/site" {
|
|
10
|
+
export const SITE_CONFIG: {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
};
|
|
14
|
+
/** The brand's folder, `""` when the site serves from the root. */
|
|
15
|
+
export const FOLDER: string;
|
|
16
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { Font, fontData } from "astro:assets";
|
|
3
|
+
import { FOLDER } from "virtual:iterant/site";
|
|
3
4
|
import { getCollection, getEntry } from "astro:content";
|
|
4
5
|
import type { AstroComponentFactory } from "astro/runtime/server/index.js";
|
|
5
6
|
import RelatedLinks from "../components/RelatedLinks.astro";
|
|
6
7
|
import { SEO, type PageType } from "../components/seo";
|
|
7
8
|
import type { SeoJsonSchema } from "../components/seo-json";
|
|
8
9
|
import { fontCssVariable } from "../fonts/catalog";
|
|
10
|
+
import { folderPath } from "../lib/folder";
|
|
9
11
|
import type { HreflangAlternate } from "../lib/hreflang";
|
|
10
12
|
import { DEFAULT_LOCALE, localeFromPath } from "../lib/locales";
|
|
11
13
|
import { SITE_RUNTIME_VERSION } from "../version";
|
|
@@ -215,7 +217,11 @@ const fontPreload = fonts.named ? [{ style: "normal", subset: "latin" }] : false
|
|
|
215
217
|
<head>
|
|
216
218
|
<meta charset="UTF-8" />
|
|
217
219
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
218
|
-
<link
|
|
220
|
+
<link
|
|
221
|
+
rel="icon"
|
|
222
|
+
type="image/x-icon"
|
|
223
|
+
href={folderPath(FOLDER, "favicon.ico")}
|
|
224
|
+
/>
|
|
219
225
|
<meta name="generator" content={Astro.generator} />
|
|
220
226
|
<meta name="it-site-runtime" content={SITE_RUNTIME_VERSION} />
|
|
221
227
|
<meta name="it-astro-starter-version" content={SITE_RUNTIME_VERSION} />
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The brand's folder: the one path segment every platform file a build emits
|
|
2
|
+
// lives under, so the customer's edge forwards `example.com/feed/*` and never
|
|
3
|
+
// needs a second rule. Pages are untouched: they keep their public paths, and a
|
|
4
|
+
// site with no folder emits exactly what it emitted before folders existed.
|
|
5
|
+
//
|
|
6
|
+
// The value is the backend's `brand.iterant_subfolder`, written into the repo's
|
|
7
|
+
// site config beside `fonts` and handed to `iterantStarter({ folder })`. One
|
|
8
|
+
// owner, one copy, never inferred from a path.
|
|
9
|
+
|
|
10
|
+
// The backend's own rule, character for character (backend/brands/folder.py):
|
|
11
|
+
// letters, digits and hyphens, so the folder is one level deep and can never
|
|
12
|
+
// shadow a platform file name. The reserved names and the locale-shape refusal
|
|
13
|
+
// stay at the door in Django, which owns the value; what a build can still be
|
|
14
|
+
// handed is a bad shape, and a bad shape emits asset URLs nobody can fetch and a
|
|
15
|
+
// sitemap nobody can find, so the build refuses it instead.
|
|
16
|
+
const FOLDER_SEGMENT = /^[a-z0-9-]+$/;
|
|
17
|
+
|
|
18
|
+
export function assertFolder(folder: string): string {
|
|
19
|
+
if (!FOLDER_SEGMENT.test(folder)) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`iterantStarter: folder must be one lower-case segment of letters, digits and hyphens, no slashes (got ${JSON.stringify(folder)})`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return folder;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A platform file's public path: `/feed/favicon.ico` under a folder,
|
|
28
|
+
* `/favicon.ico` without one. The folder is validated here too, because this is
|
|
29
|
+
* where it becomes a URL: a value like `/evil.example` must never get that far,
|
|
30
|
+
* wherever it was read. */
|
|
31
|
+
export function folderPath(folder: string | undefined, file: string): string {
|
|
32
|
+
return folder ? `/${assertFolder(folder)}/${file}` : `/${file}`;
|
|
33
|
+
}
|
package/src/routes/index.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
// Platform route handlers. Every one of these stays behind a VISIBLE file in the
|
|
2
|
-
// repo's src/pages
|
|
3
|
-
// package bump.
|
|
4
|
-
//
|
|
2
|
+
// repo's src/pages/: existence and addressing are repo-owned, behavior rides a
|
|
3
|
+
// package bump. A route that exists in no repo file breaks the src/pages/ mental
|
|
4
|
+
// model and the debuggability contract.
|
|
5
|
+
//
|
|
6
|
+
// One route is addressed by the platform, llms.txt under a brand folder
|
|
7
|
+
// (site-runtime 3.12.0): the folder is per-brand data, so no file name the
|
|
8
|
+
// template ships can carry it, and the preset injects the same handler at
|
|
9
|
+
// /<folder>/llms.txt (../config/site-module.ts).
|
|
5
10
|
export { createLlmsTxtRoute, type LlmsTxtOptions } from "./llms-txt";
|
|
6
11
|
export {
|
|
7
12
|
AI_ANSWER_CRAWLERS,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { SITE_CONFIG } from "virtual:iterant/site";
|
|
2
|
+
|
|
3
|
+
import { createLlmsTxtRoute } from "./llms-txt";
|
|
4
|
+
|
|
5
|
+
// The body of /<folder>/llms.txt, as the preset injects it (see
|
|
6
|
+
// ../config/site-module.ts). The brand's name and description come from the
|
|
7
|
+
// repo's site config through the virtual module, since an injected route has no
|
|
8
|
+
// repo file to import them from; the folder the handler writes into its links
|
|
9
|
+
// comes from the same module.
|
|
10
|
+
export const prerender = true;
|
|
11
|
+
export const GET = createLlmsTxtRoute({ siteConfig: SITE_CONFIG });
|
package/src/routes/llms-txt.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getCollection } from "astro:content";
|
|
2
|
+
import { FOLDER } from "virtual:iterant/site";
|
|
2
3
|
import { isAdvertised } from "../lib/advertised";
|
|
4
|
+
import { folderPath } from "../lib/folder";
|
|
3
5
|
import type { APIRoute } from "astro";
|
|
4
6
|
|
|
5
7
|
// /llms.txt, generated at dev/build time with no crawler or AI step: a curated
|
|
@@ -11,6 +13,10 @@ import type { APIRoute } from "astro";
|
|
|
11
13
|
// ---
|
|
12
14
|
// export const prerender = true;
|
|
13
15
|
// export const GET = createLlmsTxtRoute({ siteConfig: SITE_CONFIG });
|
|
16
|
+
//
|
|
17
|
+
// Under a brand folder the same handler is addressed by the preset instead, at
|
|
18
|
+
// /<folder>/llms.txt, because no file name the template ships can carry a
|
|
19
|
+
// per-brand segment (../config/site-module.ts).
|
|
14
20
|
|
|
15
21
|
export interface LlmsTxtOptions {
|
|
16
22
|
/** The brand's SITE_CONFIG; name and description head the file when set. */
|
|
@@ -58,7 +64,7 @@ export function createLlmsTxtRoute({ siteConfig }: LlmsTxtOptions): APIRoute {
|
|
|
58
64
|
: []),
|
|
59
65
|
"## Discovery",
|
|
60
66
|
"",
|
|
61
|
-
`- [Sitemap](${new URL("
|
|
67
|
+
`- [Sitemap](${new URL(folderPath(FOLDER, "sitemap-index.xml"), origin).href}): Complete search-engine sitemap, if available.`,
|
|
62
68
|
`- [Robots policy](${new URL("/robots.txt", origin).href}): Crawler permissions for automated agents.`,
|
|
63
69
|
"",
|
|
64
70
|
];
|
package/src/routes/robots-txt.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { APIRoute } from "astro";
|
|
2
|
+
import { FOLDER } from "virtual:iterant/site";
|
|
2
3
|
|
|
4
|
+
import { folderPath } from "../lib/folder";
|
|
3
5
|
import { normalizeSourceUrls } from "../lib/sitemap/shared";
|
|
4
6
|
|
|
5
7
|
// /robots.txt: crawler permissions, the site's AI policy and the sitemap
|
|
@@ -108,11 +110,20 @@ function legacyDisallows(rules: string[] | undefined): string[] {
|
|
|
108
110
|
export function renderRobotsTxt(
|
|
109
111
|
config: RobotsSiteConfig,
|
|
110
112
|
site: URL | undefined,
|
|
113
|
+
// The brand's folder (site-runtime 3.12.0). robots.txt itself stays at the
|
|
114
|
+
// root, where a crawler looks for it; its index line points into the folder,
|
|
115
|
+
// where the build writes the sitemap. Resolved against the ORIGIN, because the
|
|
116
|
+
// folder is what the customer forwards from their domain's root, while a site
|
|
117
|
+
// URL that carries a path keeps resolving relative to it without one.
|
|
118
|
+
folder: string = "",
|
|
111
119
|
): string {
|
|
112
120
|
const policy = resolveAiPolicy(config.aiPolicy);
|
|
121
|
+
const indexPath = folder
|
|
122
|
+
? folderPath(folder, "sitemap-index.xml")
|
|
123
|
+
: "sitemap-index.xml";
|
|
113
124
|
const sitemap = site
|
|
114
125
|
? [
|
|
115
|
-
`Sitemap: ${new URL(
|
|
126
|
+
`Sitemap: ${new URL(indexPath, site).href}`,
|
|
116
127
|
// Mirrored sitemap, only when sourceSitemapUrl is configured.
|
|
117
128
|
...(normalizeSourceUrls(config.sourceSitemapUrl ?? "").length > 0
|
|
118
129
|
? [`Sitemap: ${new URL("sitemap.xml", site).href}`]
|
|
@@ -141,7 +152,7 @@ export function createRobotsTxtRoute(options: RobotsTxtOptions): APIRoute {
|
|
|
141
152
|
const { siteConfig, ...flat } = options;
|
|
142
153
|
const config: RobotsSiteConfig = { ...flat, ...(siteConfig ?? {}) };
|
|
143
154
|
return ({ site }) =>
|
|
144
|
-
new Response(renderRobotsTxt(config, site), {
|
|
155
|
+
new Response(renderRobotsTxt(config, site, FOLDER), {
|
|
145
156
|
headers: { "Content-Type": "text/plain" },
|
|
146
157
|
});
|
|
147
158
|
}
|