@webjsdev/cli 0.10.64 → 0.10.66
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/webjs.js +10 -1
- package/package.json +2 -2
- package/templates/.agents/skills/webjs/references/components.md +2 -2
- package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +1 -1
- package/templates/.agents/skills/webjs/references/routing-and-pages.md +50 -3
- package/templates/.agents/skills/webjs/references/ui-kit.md +14 -3
- 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/bin/webjs.js
CHANGED
|
@@ -1577,7 +1577,16 @@ async function main() {
|
|
|
1577
1577
|
let tscPath;
|
|
1578
1578
|
try {
|
|
1579
1579
|
const req = createRequire(join(cwd, 'package.json'));
|
|
1580
|
-
|
|
1580
|
+
// The `tsc` bin the package itself declares. TypeScript 7 (the native
|
|
1581
|
+
// compiler) restricts its subpaths with `exports`, so
|
|
1582
|
+
// `typescript/bin/tsc` no longer resolves; its package.json does, in
|
|
1583
|
+
// every major, and names the bin. Its launcher spawns the native
|
|
1584
|
+
// binary: about a quarter of the 6.x compiler's memory, five times
|
|
1585
|
+
// faster.
|
|
1586
|
+
const pkgPath = req.resolve('typescript/package.json');
|
|
1587
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
1588
|
+
const bin = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.tsc;
|
|
1589
|
+
tscPath = bin ? join(dirname(pkgPath), bin) : req.resolve('typescript/bin/tsc');
|
|
1581
1590
|
} catch {
|
|
1582
1591
|
console.error(
|
|
1583
1592
|
'webjs typecheck: TypeScript is not installed in this project.\n' +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webjsdev/cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.66",
|
|
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": {
|
|
@@ -30,7 +30,7 @@ Most of what follows restates widely held component-model advice, ported. Lit's
|
|
|
30
30
|
|
|
31
31
|
**4. ARIA state is a hole in `render()`, derived from the same state that drives behaviour.** `aria-expanded=${this.open ? 'true' : 'false'}` cannot disagree with `this.open`. A second function that re-finds the button and calls `setAttribute` can, and does, the first time someone adds a close path that forgets to call it. The same holds for `class`, `?disabled`, and any `.prop`. Two caveats ride this rule:
|
|
32
32
|
|
|
33
|
-
-
|
|
33
|
+
- A plain-attribute hole follows one rule on the server and the client (#1573): `null` / `undefined` omit the attribute, `false` omits it except on an `aria-*` name, where it is written as `"false"`. So `aria-expanded=${this.open}` serves `"true"` / `"false"` and hydrates unchanged, and `aria-current=${active ? 'page' : null}` omits the attribute on an inactive link. `?attr=${bool}` is not a substitute for a tri-state ARIA attribute, since a boolean binding omits the attribute when false.
|
|
34
34
|
- A hole commits on the next render, one microtask later. The one place a direct write is still correct is a synchronous snapshot read such as `webjs:before-cache`, where the router reads `outerHTML` in the same task. That is a documented exception, not the normal path.
|
|
35
35
|
|
|
36
36
|
**5. Behaviour needs an importable surface, or its test is a copy of it.** An inline `<script>` in a layout has no module identity, so a browser test cannot import it. It can only transcribe the listener into the test file and assert against the transcription, which then needs a SECOND test to grep the original for drift. Two tests, neither running shipping code. A component is importable, so its browser test mounts the real element and drives real events. A page or layout may still carry an inline `<script>`, but only for pre-paint boot work no module can do: reading a stored theme before first paint so the wrong palette never flashes, or measuring the header height into a CSS custom property. It must not be interactivity, and WHERE it sits decides how often it runs. The ROOT layout's markup sits OUTSIDE every swap range, so a soft navigation does not re-run its script, which is what makes it the right home for boot work and the wrong home for anything that has to respond to a later navigation. A page or a NESTED layout sits inside the swap range instead, so its script re-executes on every navigation that swaps that range (#1102), which means it has to be idempotent or guard on a flag it sets the first time. Neither shape gives you a listener that simply works, which is what a custom element is for. Under an opt-in CSP the script also needs the nonce from `cspNonce()`. `client-router-and-streaming.md` carries the full re-execution rule.
|
|
@@ -402,7 +402,7 @@ A component that does no client-side work renders the same SSR'd HTML with or wi
|
|
|
402
402
|
- a factory-declared reactive property that is not `{ state: true }`
|
|
403
403
|
- an overridden lifecycle hook (including `renderFallback` / `renderError`)
|
|
404
404
|
- an imported `signal` / `computed` / `watch` / `Task` / `ref` / streaming directive, or `addController` / `requestUpdate`
|
|
405
|
-
- code that runs at module load (a top-level call, non-data `new`, dynamic `import(...)`, top-level `await`); only declarations and `X.register(...)` are allowed. TypeScript types are erased before the analyser reads a module, so an annotation can never be a blocker however call-shaped it looks (`readonly (readonly [number, number, number])[]` is fine)
|
|
405
|
+
- code that runs at module load (a top-level call, non-data `new`, dynamic `import(...)`, top-level `await`); only declarations and `X.register(...)` are allowed. A call inside a declaration does not run at load, so it is not a blocker: an arrow's expression body (`export const usd = (m) => Math.round(m) / MICROS`) and a function parameter default (`function relativeTime(when, now = Date.now())`) are both fine. TypeScript types are erased before the analyser reads a module, so an annotation can never be a blocker however call-shaped it looks (`readonly (readonly [number, number, number])[]` is fine)
|
|
406
406
|
- the dynamic slot READ surface (`slotchange`, `assignedNodes` / `assignedElements` / `assignedSlot`); merely RENDERING a `<slot>` does not ship (the SSR output carries the placed children, so a display-only slotted wrapper is byte-identical without its JS; native-write liveness is consumer-driven and the consumer's tag reference forces the ship)
|
|
407
407
|
- being rendered by a component that itself ships
|
|
408
408
|
|
|
@@ -174,7 +174,7 @@ html`<form action=${submitFeedback}><input name="email"></form>`;
|
|
|
174
174
|
html`<form method="post"><input name="email"></form>`;
|
|
175
175
|
```
|
|
176
176
|
|
|
177
|
-
A hole that resolves to `null`
|
|
177
|
+
A plain attribute hole that resolves to `null` or `undefined` omits the attribute on both renderers (#1573), so `method=${null}` and `?method=${false}` both emit nothing and WebJs supplies `method="post"`. An EMPTY string is different: `method=${''}` renders `method=""`, which cannot submit and is refused.
|
|
178
178
|
|
|
179
179
|
A string stays a string: `action="/search"` and `action=${'/search'}` are unchanged, which is what a search form (`<form method="get" action="/search">`) and a `route.ts` endpoint both want. Other attributes keep their existing stringify behaviour; only a FUNCTION under `action` / `formaction` is claimed.
|
|
180
180
|
|
|
@@ -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
|
```
|
|
@@ -81,9 +81,9 @@ So the loop is: `add` the component, then query `ui <name>` (MCP) or
|
|
|
81
81
|
## Inventory (run `npx webjsdev ui list` or the MCP `ui` tool for the authoritative, current set)
|
|
82
82
|
|
|
83
83
|
**Tier 1 (class helpers):** accordion, alert, aspect-ratio, avatar, badge,
|
|
84
|
-
breadcrumb, button, card, checkbox, collapsible, input, kbd,
|
|
85
|
-
native-select, pagination, popover, progress, radio-group, separator,
|
|
86
|
-
switch, table, textarea.
|
|
84
|
+
breadcrumb, button, card, checkbox, collapsible, empty, field, input, kbd,
|
|
85
|
+
label, native-select, pagination, popover, progress, radio-group, separator,
|
|
86
|
+
skeleton, spinner, switch, table, textarea.
|
|
87
87
|
|
|
88
88
|
**Tier 2 (custom elements, own their ARIA):** alert-dialog, dialog,
|
|
89
89
|
dropdown-menu, hover-card, sonner, tabs, tooltip, plus toggle and toggle-group
|
|
@@ -93,6 +93,17 @@ dropdown-menu, hover-card, sonner, tabs, tooltip, plus toggle and toggle-group
|
|
|
93
93
|
|
|
94
94
|
- A helper is a function, so compose it: `class=${buttonClass({ variant: 'outline' })}`.
|
|
95
95
|
The unquoted `${...}` is a normal `html` attribute hole.
|
|
96
|
+
- The three states a screen owes its user each have a primitive, so do not hand-roll
|
|
97
|
+
them. A list or table with no rows renders `empty` (title as a real heading, a
|
|
98
|
+
description saying what to do next, and the create action). A form field is
|
|
99
|
+
`field` (`fieldClass()` holding a `<label for>`, the control, `fieldDescriptionClass()`
|
|
100
|
+
text, and on a validation error `data-invalid="true"`, `aria-invalid` on the control,
|
|
101
|
+
and a `fieldErrorClass()` message with `role="alert"` whose id the control's
|
|
102
|
+
`aria-describedby` lists, with the typed value kept). A pending save puts
|
|
103
|
+
`spinner({ decorative: true })` in a `disabled` + `aria-busy="true"` submit button
|
|
104
|
+
that keeps its text. The `fieldClass` / `fieldLabelClass` in `components/ui/field.ts`
|
|
105
|
+
are not the same-named rhythm helpers in `lib/utils/cn.ts`; import each from its own
|
|
106
|
+
module and alias one if a file needs both.
|
|
96
107
|
- Tier-1 helpers assume the design tokens exist; if a component paints unstyled,
|
|
97
108
|
the tokens are missing (re-run `npx webjsdev ui init` or let `add` self-heal them).
|
|
98
109
|
- Custom elements are display-only-safe at SSR and hydrate in the browser, the
|
|
@@ -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
|
);
|