@webjsdev/cli 0.10.63 → 0.10.65
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/lib/create.js +14 -1
- package/package.json +2 -2
- package/templates/.agents/skills/webjs/references/auth-and-sessions.md +2 -0
- package/templates/.agents/skills/webjs/references/client-router-and-streaming.md +2 -0
- package/templates/.agents/skills/webjs/references/routing-and-pages.md +50 -3
- package/templates/.agents/skills/webjs/references/runtime.md +4 -0
- package/templates/gallery/app/opengraph-image.ts +4 -4
- package/templates/gallery/app/robots.ts +14 -10
- package/templates/gallery/app/sitemap.ts +11 -17
- package/templates/gallery/app/sitemaps/files/route.ts +14 -0
- package/templates/gallery/app/sitemaps/route.ts +8 -7
package/lib/create.js
CHANGED
|
@@ -974,9 +974,22 @@ import { Pool } from 'pg';
|
|
|
974
974
|
import * as schema from './schema.server.ts';
|
|
975
975
|
|
|
976
976
|
// The only file that opens the driver. Cached on globalThis across dev reloads.
|
|
977
|
+
// The pool relies on no session state (no SET, no LISTEN, no advisory locks
|
|
978
|
+
// held across queries), so it also works behind a transaction pooler. Idle
|
|
979
|
+
// connections close after 10 s; a database that is down fails a request in
|
|
980
|
+
// 5 s instead of hanging it; DATABASE_POOL_MAX caps the pool (default 10).
|
|
977
981
|
const g = globalThis as unknown as { __webjs_db?: unknown };
|
|
978
982
|
function open() {
|
|
979
|
-
|
|
983
|
+
const pool = new Pool({
|
|
984
|
+
connectionString: process.env.DATABASE_URL,
|
|
985
|
+
max: Number(process.env.DATABASE_POOL_MAX) || 10,
|
|
986
|
+
idleTimeoutMillis: 10_000,
|
|
987
|
+
connectionTimeoutMillis: 5_000,
|
|
988
|
+
});
|
|
989
|
+
// An idle connection the server drops (a restart, a failover) emits
|
|
990
|
+
// 'error' on the pool, and an unhandled one would crash the process.
|
|
991
|
+
pool.on('error', (err) => console.error('[db] idle connection lost:', err.message));
|
|
992
|
+
return drizzle({ client: pool, relations: schema.relations });
|
|
980
993
|
}
|
|
981
994
|
export const db = (g.__webjs_db ??= open()) as ReturnType<typeof open>;
|
|
982
995
|
`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webjsdev/cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.65",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The CLI for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Runs the dev and production servers, scaffolds apps, validates conventions, and drives the database. Node 24+ or Bun.",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@webjsdev/mcp": "^0.1.0",
|
|
21
|
-
"@webjsdev/server": "^0.8.
|
|
21
|
+
"@webjsdev/server": "^0.8.81",
|
|
22
22
|
"@webjsdev/ui": "^0.3.15"
|
|
23
23
|
},
|
|
24
24
|
"publishConfig": {
|
|
@@ -128,6 +128,8 @@ A `redirectTo` that arrives from a request (a form field or a query param, for O
|
|
|
128
128
|
|
|
129
129
|
For a programmatic sign-in (the auto-login-after-signup pattern), `signIn('credentials', creds, { redirectTo })` returns a `302` `Response` that a form-bound action can return directly (the framework honors a returned `Response` verbatim).
|
|
130
130
|
|
|
131
|
+
**The header updates on its own after sign-in and sign-out.** A layout that reads `auth()` to render "Sign in" / "Sign out" is re-rendered after any mutating form submission the router handles (a form bound to a sign-in action, or a plain POST to `/api/auth/signin/credentials` / `/api/auth/signout`), and its markup is morphed in place, so there is nothing to refresh by hand (#1557). Only a sign-out done over RPC from a component (`await logout(); navigate('/')`) needs `await refreshPage('shell')` after it, since that navigation is a plain GET.
|
|
132
|
+
|
|
131
133
|
Sessions are JWT by default (stateless, scales horizontally). OAuth
|
|
132
134
|
providers handle the full redirect flow. Read the session anywhere on the
|
|
133
135
|
server with `auth()`.
|
|
@@ -17,6 +17,8 @@ Read this when a task touches client navigation, prefetch, partial-page swaps, s
|
|
|
17
17
|
|
|
18
18
|
The router auto-enables the moment `@webjsdev/core` loads in the browser, which is any page that ships a component. There is nothing to import or opt into. It intercepts same-origin `<a>` clicks (including inside shadow DOM), fetches the target HTML, and replaces only the inside of the deepest shared layout. Outer header, sidenav, and footer DOM is never re-rendered, so scroll positions, input values, and `<details>` state survive a navigation.
|
|
19
19
|
|
|
20
|
+
**A mutating form submission re-renders the layouts (#1557).** A link click keeps the layout chrome by construction, but a form action can change what a layout renders: a sign-in sets the cookie the header reads, a sign-out clears it. So a POST / PUT / PATCH / DELETE form the router submits sends NO `X-Webjs-Have` (the redirect `fetch` follows keeps request headers, so a have-header would make the server short-circuit the very layouts that need to re-run), and the swap applies the boundary plan as usual and then MORPHS the chrome outside the plan's range, level by level from `<body>` down. The morph is keyed + positional and state-preserving, so a hydrated layout component survives unless its own markup changed, and form controls keep what the reader typed (a 422 re-render included). When the chain from `<body>` to the range cannot be paired safely (a different depth or tag, or a hydrated component wrapping `${children}`), it takes the full-body tier instead, which is correct and only loses component state. Nodes appended to `<body>` at runtime (a dev overlay, a dialog portal) trail the server-rendered content and are left alone. A frame-targeted submission keeps its frame-only swap, and an RPC mutation followed by `navigate()` is a GET navigation, so call `refreshPage('shell')` after it when the layout depends on what changed.
|
|
21
|
+
|
|
20
22
|
**The nav parse must preserve comments.** SSR wraps each layout's children AND the page itself in a KEYED boundary comment pair (open `<!--wj:children:<segment>:<route-key>-->`, close `<!--/wj:children:<segment>-->`, #1015). The route-key is the region's resolved concrete path with each substituted param value percent-encoded (so a user-controlled value can never terminate the comment or collide with the `:` delimiter). The router STRICTLY scans both the live and incoming DOM into segment maps: a close must id-match its innermost open, and ANY truncation, mispair, duplicate, or legacy anonymous open poisons the whole scan. The swap decision is two-tier with Next.js remount parity: a CHANGED route-key REPLACES (a fresh remount, permanents regrafted) at the PARENT of the shallowest changed boundary (a layout's boundary wraps only its children, so its own param-derived markup lives in the parent's range; anchoring there remounts the layout chrome too, exactly like Next re-rendering the layout with new params), else MORPH (the keyed state-preserving reconcile) at the deepest shared boundary when it is the leaf on both sides. The X-Webjs-Have header carries `segment:route-key` entries so the server re-renders (and re-ships) a dynamic layout the client holds for other params instead of short-circuiting past it. A poisoned scan or no shared segment degrades to a FULL PAGE LOAD (dev logs the cause), never a guessed recovery, so silent DOM corruption is structurally impossible. Hydration keys off another comment (`<!--webjs-hydrate-->`, which `__isHydrating()` reads as a component's first child). So the router and hydration both ride on comments SURVIVING the parse that turns a navigation response into a Document, which makes that parse a load-bearing correctness boundary rather than an implementation detail.
|
|
21
23
|
|
|
22
24
|
`Document.parseHTMLUnsafe` STRIPS every comment in Chromium 150 (#1007). No other parse API does: `DOMParser`, `setHTMLUnsafe`, `template.innerHTML`, and plain `innerHTML` all preserve them, and so does the document's own navigation parser, which is why a hard refresh always looked correct and only soft nav broke. With the boundaries gone the router degrades to a full page load (correct, just not soft); with `webjs-hydrate` gone a slotted light-DOM component misses the hydration adopt path. `parseHTML` therefore PROBES `parseHTMLUnsafe` once for losslessness instead of sniffing versions, uses it when it is lossless (it is the only single-pass API that also processes Declarative Shadow DOM), and otherwise parses with `DOMParser`, which preserves comments. A fixed browser silently returns to the fast path.
|
|
@@ -138,6 +138,30 @@ export async function generateMetadata(ctx: MetadataContext): Promise<Metadata>
|
|
|
138
138
|
|
|
139
139
|
Common fields: `title` (string or `{ template, default, absolute }`), `description`, `keywords`, `metadataBase` (resolves relative URLs in `openGraph` / `twitter` / `alternates` / `icons`), `openGraph`, `twitter`, `robots`, `alternates.canonical`, `icons`, `manifest`, and `jsonLd` (schema.org structured data, single object or array, HTML-safe-escaped automatically). `viewport`, `themeColor`, and `colorScheme` may also be set via a split `export const viewport = { ... }`. `cacheControl` is emitted as a response HEADER (not a `<meta>`); pages default to `no-store`, and any other value enables conditional GET (a weak `ETag` + `304`), `private` included. See https://webjs.dev/docs for the full field list.
|
|
140
140
|
|
|
141
|
+
### Search and share defaults (SEO)
|
|
142
|
+
|
|
143
|
+
Set `SITE_URL` (the app's public origin, e.g. `https://shop.example.com`) in production. It is the default `metadataBase`, and with it every page gets, unless it sets its own:
|
|
144
|
+
|
|
145
|
+
- `<link rel="canonical">` = site URL + the page's pathname (query dropped). Opt one page out with `alternates: { canonical: null }`. Without a site URL no canonical is emitted, because the request host can be set by a client.
|
|
146
|
+
- `og:title` / `og:description` from `title` / `description`, `og:type` `website`, `og:url` = the canonical.
|
|
147
|
+
- `og:image` from the nearest `opengraph-image` route above the page (and `twitter:image` from `twitter-image`), when the page declares no image.
|
|
148
|
+
- `twitter:card`: `summary_large_image` when there is an image, else `summary`.
|
|
149
|
+
|
|
150
|
+
`openGraph` and `twitter` take Next's shapes: `images` is a string, `URL`, `{ url, width, height, alt, type }` or an array of those (emits `og:image` + `og:image:width` / `height` / `alt`); camelCase keys become snake_case (`siteName` -> `og:site_name`); `publishedTime` / `modifiedTime` / `authors` / `section` / `tags` emit `article:*`. `metadataBase` may be a string or a `URL`.
|
|
151
|
+
|
|
152
|
+
So a public page needs only a specific title and description:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
export const metadata: Metadata = {
|
|
156
|
+
title: 'Opening hours',
|
|
157
|
+
description: 'When the bakery is open, including public holidays.',
|
|
158
|
+
};
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
A signed-in or private page says `robots: { index: false }` (put it on the private section's layout and every page below inherits it); `pages()` then leaves it out of the sitemap too.
|
|
162
|
+
|
|
163
|
+
Structured data goes in `jsonLd` (from `generateMetadata` when it depends on a record): one schema.org object or an array, escaped safely into `<script type="application/ld+json">`.
|
|
164
|
+
|
|
141
165
|
## Control-flow throws
|
|
142
166
|
|
|
143
167
|
From `@webjsdev/core`: throw to short-circuit a page / layout render or a form-bound action.
|
|
@@ -225,7 +249,30 @@ The BOUNDARY FILE's own crash (it throws, or fails to import at all) is reported
|
|
|
225
249
|
Two further consequences: a layout that fetches runs its fetch again on a boundary response, and a `<webjs-suspense>` inside a wrapped layout shows its fallback, because a boundary response is buffered so its status is final before the first byte. A 404 for a URL that matched NO route has no chain to wrap in and stays a bare document.
|
|
226
250
|
- Root-only (in `app/` exactly): `global-error.ts` is the app-wide catch-all after nested `error` boundaries are exhausted and renders its OWN `<!doctype><html><body>` (returned verbatim, so keep it static HTML with no components or hydration). That verbatim document is exactly why it is the one boundary left UNWRAPPED: a second shell would nest inside the root layout's, wrapping it would re-run the code that just threw, and with no boot script it could not soft-swap anyway. `global-not-found.ts` renders for an unmatched-anywhere URL when no `not-found` matches.
|
|
227
251
|
|
|
228
|
-
Metadata routes (`sitemap.ts`, `robots.ts`, `manifest.ts`, `icon.ts`, `apple-icon.ts`, `opengraph-image.ts`, `twitter-image.ts`) live at app root or static segments and default-export a possibly-async function; `sitemap()` / `sitemapIndex()` from `@webjsdev/server`
|
|
252
|
+
Metadata routes (`sitemap.ts`, `robots.ts`, `manifest.ts`, `icon.ts`, `apple-icon.ts`, `opengraph-image.ts`, `twitter-image.ts`) live at app root or static segments (a nested one answers under its segment: `app/blog/opengraph-image.ts` is `/blog/opengraph-image`) and default-export a possibly-async function that receives `{ request, url, siteUrl, pages }`. `siteUrl` is `SITE_URL`, else the request origin. `sitemap` may return an ARRAY of entries (urls may be paths, resolved against `siteUrl`) and `robots` an OBJECT (Next's `MetadataRoute.Robots` shape); the server serializes them. `sitemap()` / `sitemapIndex()` / `robots()` from `@webjsdev/server` are the same serializers, for a route that builds its own Response.
|
|
253
|
+
|
|
254
|
+
`pages()` lists every public page as sitemap entries: each static page (route groups dropped), each dynamic page that exports `generateSitemapParams()`, and none whose static metadata chain says `robots: { index: false }`. So the sitemap follows the app as pages are added:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
// app/sitemap.ts
|
|
258
|
+
import type { MetadataRouteContext } from '@webjsdev/server';
|
|
259
|
+
export default async function sitemap({ pages }: MetadataRouteContext) {
|
|
260
|
+
return pages();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// app/products/[id]/page.ts: list the products this route serves
|
|
264
|
+
export async function generateSitemapParams() {
|
|
265
|
+
return (await listProducts()).map((p) => ({ params: { id: p.id }, lastModified: p.updatedAt }));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// app/robots.ts
|
|
269
|
+
import type { MetadataRouteContext } from '@webjsdev/server';
|
|
270
|
+
export default function robots({ siteUrl }: MetadataRouteContext) {
|
|
271
|
+
return { rules: { userAgent: '*', allow: '/', disallow: ['/account', '/api/'] }, sitemap: `${siteUrl}/sitemap.xml` };
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`generateSitemapParams()` returns params objects (`[{ id: '1' }]`) or entries carrying them (`[{ params: { id: '1' }, lastModified }]`).
|
|
229
276
|
|
|
230
277
|
The IMAGE metadata routes (`icon`, `apple-icon`, `opengraph-image`, `twitter-image`) default-export a function returning a `Response` with an explicit `content-type`, so an inline SVG needs no asset file (buildless).
|
|
231
278
|
|
|
@@ -248,7 +295,7 @@ export const metadata = {
|
|
|
248
295
|
|
|
249
296
|
Declare a favicon through `metadata.icons` (or a metadata route), never as a hand-written `<link rel="icon">`: only the root layout may write a shell at all (invariant 8), so a hand-written tag is unavailable to every other layout. A `public/favicon.ico` needs no declaration either way, since the framework serves it at the origin root for crawlers that read no markup.
|
|
250
297
|
|
|
251
|
-
`opengraph-image` and `twitter-image` are
|
|
298
|
+
`opengraph-image` and `twitter-image` are LINKED too, Next's behaviour: a page that declares no `openGraph.images` gets `og:image` pointing at the nearest `opengraph-image` route above it (absolute against the site URL), and likewise `twitter:image`. A page that declares its own image keeps it.
|
|
252
299
|
|
|
253
300
|
```ts
|
|
254
301
|
// app/opengraph-image.ts (OG is 1200x630; apple-icon 180x180)
|
|
@@ -256,5 +303,5 @@ export default function OgImage() {
|
|
|
256
303
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630">...</svg>`;
|
|
257
304
|
return new Response(svg, { headers: { 'content-type': 'image/svg+xml' } });
|
|
258
305
|
}
|
|
259
|
-
//
|
|
306
|
+
// every page without its own image now carries og:image = <site>/opengraph-image
|
|
260
307
|
```
|
|
@@ -89,6 +89,10 @@ The scaffold ships a matching Dockerfile per runtime: a Node image for the defau
|
|
|
89
89
|
|
|
90
90
|
Both `node:sqlite` and `bun:sqlite` default `busy_timeout` to 0, so a contended write throws `database is locked` immediately. The generated connection sets `PRAGMA busy_timeout = 5000` plus `PRAGMA journal_mode = WAL` on the raw client before Drizzle wraps it, on both runtime branches, so you get a sane 5-second wait instead of an instant failure. This is already wired in the scaffold's `db/connection.server.ts`.
|
|
91
91
|
|
|
92
|
+
## Postgres pool (`--db postgres`)
|
|
93
|
+
|
|
94
|
+
A `--db postgres` app opens one `pg` Pool in `db/connection.server.ts`. It holds no session state between queries (no `SET`, `LISTEN`, or advisory lock kept across them), so the same code works behind a transaction pooler. Idle connections close after 10 seconds, a connection attempt gives up after 5 seconds (a database that is down fails the request instead of hanging it), `DATABASE_POOL_MAX` caps the pool (default 10), and an idle connection the server drops (a restart or failover) is logged rather than crashing the process. Keep it that way: do not add `SET` statements or session-scoped features to app code.
|
|
95
|
+
|
|
92
96
|
## Verifying a runtime-sensitive change
|
|
93
97
|
|
|
94
98
|
Most app code needs no runtime-specific testing, because it does not touch a runtime seam. If you DO change something runtime-sensitive (the serializer, a stream, `node:crypto`, low-level request handling, anything that behaves differently under `Bun.serve` versus `node:http`), prove it on both runtimes. The Node suite is the source of truth, and an additive Bun matrix re-runs the runtime-sensitive tests under Bun. See `testing.md` for the cross-runtime matrix and the `test/bun/**` assertions.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// app/opengraph-image.ts serves /opengraph-image (the preview card social
|
|
2
2
|
// platforms show when the site is shared). The Open Graph spec wants 1200x630.
|
|
3
|
-
// Returning a Response with an inline SVG keeps this buildless
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Returning a Response with an inline SVG keeps this buildless. Every page that
|
|
4
|
+
// declares no `openGraph.images` gets og:image pointing here automatically
|
|
5
|
+
// (absolute against SITE_URL); a nested `app/<segment>/opengraph-image.ts`
|
|
6
|
+
// covers the pages under that segment instead.
|
|
7
7
|
export default function OpengraphImage() {
|
|
8
8
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
|
|
9
9
|
<rect width="1200" height="630" fill="#1e2226"/>
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
// app/robots.ts serves /robots.txt.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
1
|
+
// app/robots.ts serves /robots.txt. `robots()` from @webjsdev/server writes the
|
|
2
|
+
// file from Next's robots object (returning the bare object works too; the
|
|
3
|
+
// server runs it through the same helper). `siteUrl` is SITE_URL (your public
|
|
4
|
+
// origin) when set, else the request origin, so the Sitemap line is always
|
|
5
|
+
// absolute. Disallow signed-in areas and API paths, and mark those pages
|
|
6
|
+
// `robots: { index: false }` too, so a crawler that follows a link still does
|
|
7
|
+
// not index them.
|
|
8
|
+
import { robots } from '@webjsdev/server';
|
|
9
|
+
import type { MetadataRouteContext } from '@webjsdev/server';
|
|
5
10
|
|
|
6
|
-
export default function Robots() {
|
|
7
|
-
return
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
].join('\n');
|
|
11
|
+
export default function Robots({ siteUrl }: MetadataRouteContext) {
|
|
12
|
+
return robots({
|
|
13
|
+
rules: { userAgent: '*', allow: '/', disallow: ['/api/'] },
|
|
14
|
+
sitemap: [`${siteUrl}/sitemap.xml`, `${siteUrl}/sitemaps`],
|
|
15
|
+
});
|
|
12
16
|
}
|
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
// app/sitemap.ts serves /sitemap.xml. The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
1
|
+
// app/sitemap.ts serves /sitemap.xml. The route receives `{ pages, siteUrl, ... }`
|
|
2
|
+
// and may return an ARRAY of entries, which the server serializes to spec-valid
|
|
3
|
+
// XML. `pages()` lists every public page of the app: each static page, each
|
|
4
|
+
// dynamic page that exports `generateSitemapParams()` (the params it serves,
|
|
5
|
+
// usually from a query), and none whose metadata says `robots: { index: false }`.
|
|
6
|
+
// So adding a page adds it here with no edit. URLs use SITE_URL (your public
|
|
7
|
+
// origin) when it is set, else the request origin. Append data-driven entries
|
|
8
|
+
// that are not pages (a feed, a file) to the array.
|
|
9
|
+
import type { MetadataRouteContext } from '@webjsdev/server';
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export default function Sitemap() {
|
|
11
|
-
const routes = ['/', '/features/routing', '/features/boundaries'];
|
|
12
|
-
return sitemap(
|
|
13
|
-
routes.map((path) => ({
|
|
14
|
-
url: `${SITE_URL}${path}`,
|
|
15
|
-
changeFrequency: 'weekly' as const,
|
|
16
|
-
priority: path === '/' ? 1.0 : 0.7,
|
|
17
|
-
})),
|
|
18
|
-
);
|
|
11
|
+
export default async function Sitemap({ pages }: MetadataRouteContext) {
|
|
12
|
+
return pages();
|
|
19
13
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// A child sitemap built by hand with `sitemap(entries)`: for URLs that are not
|
|
2
|
+
// pages (a feed, a downloadable file), which `pages()` in app/sitemap.ts cannot
|
|
3
|
+
// know about. Each entry is { url, lastModified?, changeFrequency?, priority? };
|
|
4
|
+
// the helper escapes every URL and drops a malformed entry instead of emitting
|
|
5
|
+
// broken XML.
|
|
6
|
+
import { sitemap } from '@webjsdev/server';
|
|
7
|
+
|
|
8
|
+
export async function GET(req: Request) {
|
|
9
|
+
const site = (process.env.SITE_URL || new URL(req.url).origin).replace(/\/$/, '');
|
|
10
|
+
return new Response(
|
|
11
|
+
sitemap([{ url: `${site}/manifest.json`, changeFrequency: 'yearly', priority: 0.1 }]),
|
|
12
|
+
{ headers: { 'content-type': 'application/xml; charset=utf-8' } },
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
// A sitemap INDEX: for a large site split across several sitemaps, this points
|
|
2
2
|
// crawlers at each child sitemap. `sitemapIndex(sitemaps)` (from @webjsdev/server)
|
|
3
|
-
// serializes the spec-valid <sitemapindex> XML
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// serializes the spec-valid <sitemapindex> XML; `sitemap(entries)` serializes one
|
|
4
|
+
// child. app/sitemap.ts (the pages) is one child; ./files/route.ts (URLs that are
|
|
5
|
+
// not pages) is another. In a real app the children would be sharded (posts,
|
|
6
|
+
// products, ...). Build absolute URLs from SITE_URL, your public origin.
|
|
6
7
|
import { sitemapIndex } from '@webjsdev/server';
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export async function GET() {
|
|
9
|
+
export async function GET(req: Request) {
|
|
10
|
+
const site = (process.env.SITE_URL || new URL(req.url).origin).replace(/\/$/, '');
|
|
11
11
|
return new Response(
|
|
12
12
|
sitemapIndex([
|
|
13
|
-
{ url: `${
|
|
13
|
+
{ url: `${site}/sitemap.xml` },
|
|
14
|
+
{ url: `${site}/sitemaps/files` },
|
|
14
15
|
]),
|
|
15
16
|
{ headers: { 'content-type': 'application/xml; charset=utf-8' } },
|
|
16
17
|
);
|