@transclude/core 0.11.0 → 0.11.2
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/bin/build.js +28 -7
- package/package.json +3 -2
- package/skills/transclude/SKILL.md +22 -1
- package/skills/transclude/references/server.md +127 -0
- package/src/gate.js +70 -0
- package/src/plugin.js +7 -2
- package/src/sitemap.js +8 -0
package/bin/build.js
CHANGED
|
@@ -18,6 +18,7 @@ import transclude from '../src/plugin.js';
|
|
|
18
18
|
import { loadProject } from '../src/project.js';
|
|
19
19
|
import { renderRoute, urlFor } from '../src/document.js';
|
|
20
20
|
import { prerenderContext, refusePrerender } from '../src/prerender.js';
|
|
21
|
+
import { isGated, readGated } from '../src/gate.js';
|
|
21
22
|
import { feed, feedPath } from '../src/feed.js';
|
|
22
23
|
import { includeContext } from '../src/include.js';
|
|
23
24
|
import { nodeLookup } from '../src/lookup.js';
|
|
@@ -121,7 +122,20 @@ fs.writeFileSync(entry, `// @ts-nocheck\n${fs.readFileSync(entry, 'utf8')}`);
|
|
|
121
122
|
|
|
122
123
|
// ---- prerender ------------------------------------------------------------
|
|
123
124
|
|
|
124
|
-
const { pages } = await import(pathToFileURL(entry).href);
|
|
125
|
+
const { pages, gated: declared } = await import(pathToFileURL(entry).href);
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Paths `app/server.js` says are not public, and the routes they cover.
|
|
129
|
+
*
|
|
130
|
+
* Middleware does not run during a build, so nothing here can tell a payment
|
|
131
|
+
* gate or an auth check from an open page. Without the declaration a gated page
|
|
132
|
+
* is written to `dist/static` and served by any static host that finds it, and
|
|
133
|
+
* the build reports it as a page it prerendered.
|
|
134
|
+
*
|
|
135
|
+
* An entry matching nothing is a typo, and a typo here fails open, so it is an
|
|
136
|
+
* error rather than a shrug.
|
|
137
|
+
*/
|
|
138
|
+
const gated = readGated(declared);
|
|
125
139
|
|
|
126
140
|
// ---- drafts ---------------------------------------------------------------
|
|
127
141
|
|
|
@@ -158,16 +172,19 @@ manifest.routes = manifest.routes.filter((route) => !isDraft(route));
|
|
|
158
172
|
*/
|
|
159
173
|
async function urlsFor(route) {
|
|
160
174
|
if (pages[route.id]?.prerender === false) return [];
|
|
161
|
-
if (!route.params.length)
|
|
175
|
+
if (!route.params.length) {
|
|
176
|
+
return isGated(route.pattern, gated) ? [] : [{ url: route.pattern, params: {} }];
|
|
177
|
+
}
|
|
162
178
|
|
|
163
179
|
const paths = pages[route.id]?.paths;
|
|
164
180
|
if (typeof paths !== 'function') return [];
|
|
165
181
|
|
|
166
182
|
const listed = (await paths()) ?? [];
|
|
167
|
-
return listed
|
|
168
|
-
url: urlFor(route, params),
|
|
169
|
-
|
|
170
|
-
|
|
183
|
+
return listed
|
|
184
|
+
.map((params) => ({ url: urlFor(route, params), params }))
|
|
185
|
+
// Matched per URL, not per route: `/notes/[id]` can be open while
|
|
186
|
+
// `/notes/secret` is not, and the pattern is the same for both.
|
|
187
|
+
.filter(({ url }) => !isGated(url, gated));
|
|
171
188
|
}
|
|
172
189
|
|
|
173
190
|
/**
|
|
@@ -284,7 +301,7 @@ const prerendered = outcomes.filter((outcome) => outcome.ok).map((outcome) => ou
|
|
|
284
301
|
// site with no sitemap there would be missing one only on the host that needs it
|
|
285
302
|
// written down most.
|
|
286
303
|
if (config.sitemap) {
|
|
287
|
-
write('sitemap.xml', await sitemap({ routes: manifest.routes }, pages, config.sitemap));
|
|
304
|
+
write('sitemap.xml', await sitemap({ routes: manifest.routes, gated }, pages, config.sitemap));
|
|
288
305
|
prerendered.push('/sitemap.xml');
|
|
289
306
|
}
|
|
290
307
|
|
|
@@ -306,6 +323,10 @@ fs.writeFileSync(
|
|
|
306
323
|
path.join(dist, 'routes.json'),
|
|
307
324
|
JSON.stringify(
|
|
308
325
|
{
|
|
326
|
+
// Carried so `/sitemap.xml` at runtime leaves out what the build left out.
|
|
327
|
+
// The gate itself is `app/server.js` middleware, which runs at runtime and
|
|
328
|
+
// needs no help from here.
|
|
329
|
+
gated,
|
|
309
330
|
dynamic: dynamic.map((route) => ({
|
|
310
331
|
id: route.id,
|
|
311
332
|
pattern: route.pattern,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@transclude/core",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"description": "An HTML-first server framework. A page is an .html file, the directory tree is the route table, and any fragment of a page is a URL of its own. Runs on Node, Bun, Deno and workerd, and ships no client JavaScript by default.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"html",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
],
|
|
58
58
|
"scripts": {
|
|
59
59
|
"test": "node --test \"test/**/*.test.js\"",
|
|
60
|
-
"test:examples": "npm test --prefix examples/showcase && npm test --prefix examples/todomvc && npm test --prefix examples/blog && npm test --prefix examples/search && npm test --prefix examples/htmx && npm test --prefix examples/includes && npm test --prefix examples/auth && npm test --prefix examples/live && npm test --prefix examples/elements && npm test --prefix examples/markdown",
|
|
60
|
+
"test:examples": "npm test --prefix examples/showcase && npm test --prefix examples/todomvc && npm test --prefix examples/blog && npm test --prefix examples/search && npm test --prefix examples/htmx && npm test --prefix examples/includes && npm test --prefix examples/auth && npm test --prefix examples/live && npm test --prefix examples/elements && npm test --prefix examples/markdown && npm test --prefix examples/atlas",
|
|
61
61
|
"test:www": "npm test --prefix www",
|
|
62
62
|
"showcase": "npm run dev --prefix examples/showcase",
|
|
63
63
|
"todomvc": "npm run dev --prefix examples/todomvc",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"live": "npm run dev --prefix examples/live",
|
|
70
70
|
"elements": "npm run dev --prefix examples/elements",
|
|
71
71
|
"markdown": "npm run dev --prefix examples/markdown",
|
|
72
|
+
"atlas": "npm run dev --prefix examples/atlas",
|
|
72
73
|
"www": "npm run dev --prefix www",
|
|
73
74
|
"check:src": "tsc -p tsconfig.src.json",
|
|
74
75
|
"release": "node bin/release.js"
|
|
@@ -277,6 +277,27 @@ element name. `card.html` is not, and the file is dropped.
|
|
|
277
277
|
**`<transclude>` has no self-closing form.** `<transclude src="#a" />` is read
|
|
278
278
|
as an open tag and the rest of the page becomes its fallback content.
|
|
279
279
|
|
|
280
|
+
**`app.get()` in `app/server.js` is not how a route is made.** The `app` there
|
|
281
|
+
is a real Hono instance and accepts one, but the directory tree is the route
|
|
282
|
+
table, and a handler registered by hand is invisible to the build, the sitemap
|
|
283
|
+
and `npm run check`. Middleware is what belongs in that file. Most of Hono's
|
|
284
|
+
API has an answer here already, and
|
|
285
|
+
[references/server.md](references/server.md) has the line between the two next
|
|
286
|
+
to Hono's own documentation for agents.
|
|
287
|
+
|
|
288
|
+
**Middleware does not run during the build.** A page gated only by
|
|
289
|
+
`app/server.js` is prerendered to a file and served by any static host. Declare
|
|
290
|
+
the paths so the build knows:
|
|
291
|
+
|
|
292
|
+
```js
|
|
293
|
+
// app/server.js
|
|
294
|
+
export const gated = ['/premium', '/api/*'];
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
No file is written for them and the sitemap leaves them out. They are still
|
|
298
|
+
routes. A layout guard needs no declaration: the build runs layout loaders, and
|
|
299
|
+
a guard reads a cookie.
|
|
300
|
+
|
|
280
301
|
**Reading a cookie makes a page personal.** It is then not cached and not
|
|
281
302
|
prerendered. Writing one does not do this; reading one does.
|
|
282
303
|
|
|
@@ -292,6 +313,6 @@ directly. It is the object the whole chain holds.
|
|
|
292
313
|
- [references/fragments.md](references/fragments.md) — fragments, includes and
|
|
293
314
|
transclusion
|
|
294
315
|
- [references/server.md](references/server.md) — cookies, middleware, security,
|
|
295
|
-
config, deployment
|
|
316
|
+
config, caching, deployment, and where Hono's own docs are
|
|
296
317
|
|
|
297
318
|
Full documentation: https://transclude.dev/docs
|
|
@@ -16,6 +16,57 @@ export default (app) => {
|
|
|
16
16
|
It runs before anything that serves bytes, so a guard there covers prerendered
|
|
17
17
|
pages and public files.
|
|
18
18
|
|
|
19
|
+
## Hono's documentation
|
|
20
|
+
|
|
21
|
+
Hono publishes its documentation in a form an agent can read. Fetch it rather
|
|
22
|
+
than recall it. The dependency is `hono@^4`, and it moves.
|
|
23
|
+
|
|
24
|
+
| URL | Size | What it holds |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| `https://hono.dev/llms.txt` | 6 KB | An index. Every page, one line each. |
|
|
27
|
+
| `https://hono.dev/llms-small.txt` | 190 KB | The core: routing, the `Context`, middleware, the helpers. |
|
|
28
|
+
| `https://hono.dev/llms-full.txt` | 360 KB | All of it, a page per built-in middleware. |
|
|
29
|
+
|
|
30
|
+
Read `llms.txt` and follow the one link the question needs. The other two are
|
|
31
|
+
whole manuals, and a question about `cors` does not need one.
|
|
32
|
+
|
|
33
|
+
### What this framework already answers
|
|
34
|
+
|
|
35
|
+
Most of Hono's surface has an answer here, and reaching past it is the common
|
|
36
|
+
mistake. A route registered by hand answers requests. The build, the sitemap
|
|
37
|
+
and `npm run check` never see it.
|
|
38
|
+
|
|
39
|
+
| In Hono | Here |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `app.get('/notes', …)` | `app/routes/notes.html` |
|
|
42
|
+
| `c.req.param('id')` | `params.id` in the loader |
|
|
43
|
+
| `c.req.query('q')` | `new URL(url).searchParams` |
|
|
44
|
+
| `c.req.formData()` | `request.formData()` in a verb export |
|
|
45
|
+
| `getCookie(c, 'theme')` | `ctx.cookies` |
|
|
46
|
+
| `csrf()` | on by default, `csrf` in the config |
|
|
47
|
+
| `secureHeaders()` for a policy | `csp` in the config |
|
|
48
|
+
| `trimTrailingSlash()` | `trailingSlash` in the config |
|
|
49
|
+
| `compress()`, `etag()` | both are built in, per response and at rest |
|
|
50
|
+
| `cache()` | `export const revalidate` |
|
|
51
|
+
| `serveStatic()` | `app/public/` |
|
|
52
|
+
| `c.executionCtx.waitUntil(p)` | `ctx.after(p)` |
|
|
53
|
+
| `c.html()`, `hono/html`, `hono/jsx` | a page is an `.html` file |
|
|
54
|
+
| `hono/ssg` | `npm run build` |
|
|
55
|
+
|
|
56
|
+
Hono's `Context` reaches an app in one place, the middleware in `app/server.js`.
|
|
57
|
+
A loader, an action and an endpoint are handed `ctx`, which belongs to this
|
|
58
|
+
framework and carries no `c`. Reading a form should not cost an author a
|
|
59
|
+
router's API.
|
|
60
|
+
|
|
61
|
+
### What is still Hono's
|
|
62
|
+
|
|
63
|
+
`app` in `app/server.js` is a real Hono instance, so its built-in middleware
|
|
64
|
+
works unchanged: `cors`, `basicAuth`, `bearerAuth`, `logger`, `bodyLimit`,
|
|
65
|
+
`ipRestriction`, `requestId`, `timeout`, `timing`. So do the helpers `accepts`,
|
|
66
|
+
`conninfo` and `streaming`.
|
|
67
|
+
|
|
68
|
+
`app.request()` in a test is Hono's too. See [Testing](#testing).
|
|
69
|
+
|
|
19
70
|
## Cookies
|
|
20
71
|
|
|
21
72
|
`ctx.cookies` reads and writes.
|
|
@@ -104,6 +155,7 @@ Source is JavaScript with JSDoc. Do not convert it to TypeScript.
|
|
|
104
155
|
| `sitemap` | `false` | `{ hostname }` mounts `/sitemap.xml`. |
|
|
105
156
|
| `feed` | `false` | `{ hostname, title, items }` mounts a feed. |
|
|
106
157
|
| `proxy` | `false` | `{ allow: [...] }` for cross-site includes. |
|
|
158
|
+
| `cache` | — | Where a page held by `revalidate` is kept. A bounded map in this process by default. |
|
|
107
159
|
| `precache` | `false` | `true` writes `/precache.json`. |
|
|
108
160
|
| `onError` | `null` | `(error, { request, url, method })` per failed request. |
|
|
109
161
|
|
|
@@ -149,6 +201,81 @@ skipped. Deployed, the URL is a 404. Publishing is deleting the line.
|
|
|
149
201
|
`prerender` is read off the page, never off its layouts. A layout that reads a
|
|
150
202
|
cookie makes every page under it request-dependent, and nothing says so.
|
|
151
203
|
|
|
204
|
+
## Holding a render
|
|
205
|
+
|
|
206
|
+
Between a file written once and a render on every request, there is a page held
|
|
207
|
+
for a while.
|
|
208
|
+
|
|
209
|
+
```js
|
|
210
|
+
// app/routes/notes.html
|
|
211
|
+
export const prerender = false;
|
|
212
|
+
export const revalidate = 3600;
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The number is seconds. Inside that window a request is answered from the store
|
|
216
|
+
and the loader does not run. Past it the held page goes out immediately and a
|
|
217
|
+
fresh one renders behind the response, so nobody waits for a re-render. One
|
|
218
|
+
render happens at a time per URL, however many requests arrive together.
|
|
219
|
+
|
|
220
|
+
The key is the path and the query, because a page reading `?q=` renders
|
|
221
|
+
differently for each one. A rebuild that throws leaves the held page where it is,
|
|
222
|
+
and the error goes to `onError` with the request that started it.
|
|
223
|
+
|
|
224
|
+
Three things hold nothing:
|
|
225
|
+
|
|
226
|
+
- **`npm run dev`.** The dev server renders every request and keeps nothing
|
|
227
|
+
between them. A window has no effect there at all.
|
|
228
|
+
- **A page the build wrote to a file.** The static handler answers before the
|
|
229
|
+
route handler that holds anything, so the window never runs. Adding
|
|
230
|
+
`prerender = false` puts the page back on the path that has one, which is why
|
|
231
|
+
the example above carries both lines.
|
|
232
|
+
- **A fragment.** `?fragment=list` is rendered on demand, for every route,
|
|
233
|
+
prerendered or not.
|
|
234
|
+
|
|
235
|
+
**What is never held:** a page that read a cookie, set a header, answered with a
|
|
236
|
+
`Response`, or has a status outside 2xx. A shared store holding any of those
|
|
237
|
+
hands one visitor's page, or one visitor's `Set-Cookie`, to the next. It is the
|
|
238
|
+
same rule the build uses to decide a route can be a file.
|
|
239
|
+
|
|
240
|
+
### Tags
|
|
241
|
+
|
|
242
|
+
Seconds say when a page may be out of date. A tag says when it is.
|
|
243
|
+
|
|
244
|
+
```js
|
|
245
|
+
// app/routes/notes.html
|
|
246
|
+
export const prerender = false;
|
|
247
|
+
export const revalidate = { seconds: 3600, tags: ['notes'] };
|
|
248
|
+
|
|
249
|
+
export default async () => ({ notes: await notes.all() });
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
```js
|
|
253
|
+
// app/routes/api/notes.js
|
|
254
|
+
export const POST = async ({ request, revalidateTag }) => {
|
|
255
|
+
await notes.add(await request.json());
|
|
256
|
+
|
|
257
|
+
// Every held page carrying this tag is dropped. The next request for one
|
|
258
|
+
// renders it again.
|
|
259
|
+
revalidateTag('notes');
|
|
260
|
+
|
|
261
|
+
return new Response(null, { status: 204 });
|
|
262
|
+
};
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`ctx.revalidateTag` belongs where the change was written, which is an action or
|
|
266
|
+
an endpoint: whatever made the change is what says it happened. A prerendered
|
|
267
|
+
page's loader calling it stops the build, since a build holds nothing yet to
|
|
268
|
+
drop.
|
|
269
|
+
|
|
270
|
+
`revalidate` takes a number of seconds, or `{ seconds, tags }`. Anything else
|
|
271
|
+
throws when the app starts. `revalidate: '1h'` would otherwise hold forever or
|
|
272
|
+
not at all, and say nothing either way.
|
|
273
|
+
|
|
274
|
+
The default store is a bounded map in this process. That is right for one server
|
|
275
|
+
and wrong for several: each holds its own copy, and `revalidateTag` reaches one
|
|
276
|
+
of them. `cache` in the config takes anything with the same `get`, `set`,
|
|
277
|
+
`delete` and `deleteByTag`.
|
|
278
|
+
|
|
152
279
|
## Runtimes
|
|
153
280
|
|
|
154
281
|
The same app runs on Node, Bun, Deno and workerd. `bin/serve.js`,
|
package/src/gate.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Paths an app says are not public, and whether a URL is one of them.
|
|
2
|
+
//
|
|
3
|
+
// Middleware does not run during a build, so nothing in `bin/build.js` can tell
|
|
4
|
+
// a payment gate or an auth check from an open page. `export const gated` in
|
|
5
|
+
// `app/server.js` is the only thing a build can read about a gate, and without
|
|
6
|
+
// it a gated page is written to `dist/static` and handed out by any static host,
|
|
7
|
+
// with the build reporting it as a page it prerendered.
|
|
8
|
+
//
|
|
9
|
+
// A layout guard is caught already, for a reason worth knowing: the build runs
|
|
10
|
+
// layout loaders, and a guard reads a cookie. Nothing runs `app/server.js` here.
|
|
11
|
+
//
|
|
12
|
+
// Pure. No `node:` imports: the sitemap reads this at runtime, on every runtime.
|
|
13
|
+
/**
|
|
14
|
+
* Whether a URL is one the app declared not public.
|
|
15
|
+
*
|
|
16
|
+
* `export const gated` in `app/server.js` is the only thing a build can read
|
|
17
|
+
* about a gate, because middleware does not run during one. A layout guard is
|
|
18
|
+
* caught already: the build runs layout loaders and a guard reads a cookie. A
|
|
19
|
+
* gate in `app/server.js` is run by nobody here, so a paid or signed-in page
|
|
20
|
+
* with an ordinary loader is written to `dist/static` and handed out by any
|
|
21
|
+
* static host, with the build reporting it as a success.
|
|
22
|
+
*
|
|
23
|
+
* `/premium` matches that path only. `/premium/*` matches it and everything
|
|
24
|
+
* under it. Nothing else is a pattern: this decides whether to write a file, and
|
|
25
|
+
* a rule nobody can read at a glance is the wrong shape for that.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} url the path the build is about to write
|
|
28
|
+
* @param {string[]} patterns
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
*/
|
|
31
|
+
export function isGated(url, patterns = []) {
|
|
32
|
+
return patterns.some((pattern) => {
|
|
33
|
+
if (!pattern.endsWith('/*')) return url === pattern;
|
|
34
|
+
const base = pattern.slice(0, -2);
|
|
35
|
+
return url === base || url.startsWith(`${base}/`);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The declaration, or a refusal naming what is wrong with it.
|
|
41
|
+
*
|
|
42
|
+
* Checked rather than trusted, because every mistake here fails open. A typo
|
|
43
|
+
* matches nothing, the page is written, and the build says it prerendered a page
|
|
44
|
+
* that was supposed to need paying for.
|
|
45
|
+
*
|
|
46
|
+
* @param {unknown} gated whatever `app/server.js` exported
|
|
47
|
+
* @returns {string[]}
|
|
48
|
+
* @throws when it is not a list of paths
|
|
49
|
+
*/
|
|
50
|
+
export function readGated(gated) {
|
|
51
|
+
if (gated === undefined || gated === null) return [];
|
|
52
|
+
|
|
53
|
+
if (!Array.isArray(gated)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`[transclude] app/server.js exports "gated" as ${typeof gated}. It is a list of paths, ` +
|
|
56
|
+
`like ['/premium', '/api/*'].`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const entry of gated) {
|
|
61
|
+
if (typeof entry !== 'string' || !entry.startsWith('/')) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`[transclude] "gated" in app/server.js has ${JSON.stringify(entry)}. ` +
|
|
64
|
+
`Every entry is a path beginning with "/", and "/*" at the end covers what is under it.`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return gated;
|
|
70
|
+
}
|
package/src/plugin.js
CHANGED
|
@@ -289,7 +289,7 @@ export default function transclude({
|
|
|
289
289
|
return `
|
|
290
290
|
${ids.map((pageId, i) => `import * as __P${i} from ${JSON.stringify(`${P_PAGE}${pageId}`)};`).join('\n')}
|
|
291
291
|
${apiIds.map((apiId, i) => `import * as __E${i} from ${apiSpec(endpoints.get(apiId))};`).join('\n')}
|
|
292
|
-
${hasMiddleware ? `import
|
|
292
|
+
${hasMiddleware ? `import * as __server from ${JSON.stringify(specifier)};` : ''}
|
|
293
293
|
|
|
294
294
|
export const pages = {
|
|
295
295
|
${ids.map((pageId, i) => ` ${JSON.stringify(pageId)}: __P${i},`).join('\n')}
|
|
@@ -299,7 +299,12 @@ export const endpoints = {
|
|
|
299
299
|
${apiIds.map((apiId, i) => ` ${JSON.stringify(apiId)}: __E${i},`).join('\n')}
|
|
300
300
|
};
|
|
301
301
|
|
|
302
|
-
export const middleware = ${hasMiddleware ? '
|
|
302
|
+
export const middleware = ${hasMiddleware ? '__server.default ?? null' : 'null'};
|
|
303
|
+
|
|
304
|
+
// Paths the app says are not public. The build reads this and writes no file for
|
|
305
|
+
// them, because middleware does not run during a build and a file it was meant
|
|
306
|
+
// to gate would be served by any static host.
|
|
307
|
+
export const gated = ${hasMiddleware ? '__server.gated ?? []' : '[]'};
|
|
303
308
|
`;
|
|
304
309
|
}
|
|
305
310
|
|
package/src/sitemap.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// crawler can reach by guessing, so it is left out.
|
|
8
8
|
|
|
9
9
|
import { urlFor } from './document.js';
|
|
10
|
+
import { isGated } from './gate.js';
|
|
10
11
|
|
|
11
12
|
/** The protocol's cap for one file. Past it the response is an index of files. */
|
|
12
13
|
const LIMIT = 50000;
|
|
@@ -65,9 +66,16 @@ export async function sitemapEntries(manifest, pages, { entries = [], exclude =
|
|
|
65
66
|
const extra = typeof entries === 'function' ? ((await entries()) ?? []) : entries;
|
|
66
67
|
const all = [...found, ...extra];
|
|
67
68
|
|
|
69
|
+
// `manifest.gated` rather than a second config key. A path the app declared
|
|
70
|
+
// not public should not be advertised, and reading it here covers the file the
|
|
71
|
+
// build writes and the `/sitemap.xml` route together. Two lists is two answers
|
|
72
|
+
// about which URLs a crawler is invited to.
|
|
73
|
+
const gated = manifest.gated ?? [];
|
|
74
|
+
|
|
68
75
|
const seen = new Set();
|
|
69
76
|
return all.filter((entry) => {
|
|
70
77
|
if (seen.has(entry.path) || excluded(entry.path, exclude)) return false;
|
|
78
|
+
if (isGated(entry.path, gated)) return false;
|
|
71
79
|
seen.add(entry.path);
|
|
72
80
|
return true;
|
|
73
81
|
});
|