Add sitemap generation and bunext sitemap command
- Add generateSitemap util (exported as bunext.bunextGenerateSitemap) that builds sitemap.xml from static pages, with exclude patterns, additional paths (array or sync/async function), lastmod from file mtime, trailing slash, and default changefreq/priority options - Let page .server.ts files export a `sitemap` function to list URLs for dynamic routes, or override a static page's entry - Add `bunext sitemap` command reading `sitemap` from bunext.config.ts, writing public/sitemap.xml by default, with --output/--origin flags - Fix handle-routes test leaking a grab-router module mock into bunext-req-handler tests; set global.BUNEXT_ROUTER instead - Ignore /.tmp - Document sitemap usage in README and CLAUDE.md Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -36,6 +36,10 @@ The goal is a framework that is:
|
||||
- [API Routes](#api-routes)
|
||||
- [Route Config (Body Size Limit)](#route-config-body-size-limit)
|
||||
- [Error Pages](#error-pages)
|
||||
- [Sitemap](#sitemap)
|
||||
- [Page `sitemap` Export](#page-sitemap-export)
|
||||
- [`bunext sitemap` Command](#bunext-sitemap-command)
|
||||
- [Generating at Runtime](#generating-at-runtime)
|
||||
- [Static Files](#static-files)
|
||||
- [Caching](#caching)
|
||||
- [Enabling Cache Per Page](#enabling-cache-per-page)
|
||||
@@ -155,6 +159,7 @@ bun run dev
|
||||
| `bunext dev` | Start the development server with HMR and file watching. |
|
||||
| `bunext build` | Bundle all pages for production. Outputs artifacts to `.bunext/public/pages/` using paths relative to the project root. |
|
||||
| `bunext start` | Start the production server using pre-built artifacts. |
|
||||
| `bunext sitemap` | Generate `public/sitemap.xml` from your pages, using the `sitemap` options in `bunext.config.ts`. See [Sitemap](#sitemap). |
|
||||
|
||||
### Running the CLI
|
||||
|
||||
@@ -325,6 +330,8 @@ export default function ProfilePage({ props, url }: Props) {
|
||||
}
|
||||
```
|
||||
|
||||
> Server files can also export a `sitemap` function to list a page's URLs in the sitemap — see [Page `sitemap` Export](#page-sitemap-export).
|
||||
|
||||
> **Why separate files?** Bundling server code (Bun APIs, database clients, `fs`, secrets) into the same file as a React component causes TypeScript compilation errors because the bundler processes the page file for the browser. The `.server.ts` companion file is loaded only by the server at request time and is never included in the client bundle.
|
||||
|
||||
The server function receives a `ctx` object (type `BunxRouteParams`) with:
|
||||
@@ -624,6 +631,117 @@ If no custom error pages exist, Bunext uses built-in preset components.
|
||||
|
||||
---
|
||||
|
||||
## Sitemap
|
||||
|
||||
Bunext builds a sitemap from every static page in `src/pages/`. API routes, `404`/`500` pages and ignored directories are left out. Dynamic routes (`[slug]`) cannot be discovered from the file system, so they are only included when their server file exports a `sitemap` function (see below), or when you pass their URLs through `additional_paths`.
|
||||
|
||||
### Page `sitemap` Export
|
||||
|
||||
A page's `.server.ts` file can export a `sitemap` function returning that page's entries. This is the way to list URLs for a dynamic route:
|
||||
|
||||
```ts
|
||||
// src/pages/blog/[slug].server.ts
|
||||
import type { BunextPageServerFn, BunextPageSitemapFn } from "@moduletrace/bunext/types";
|
||||
|
||||
const server: BunextPageServerFn = async (ctx) => {
|
||||
// ...
|
||||
};
|
||||
|
||||
export default server;
|
||||
|
||||
export const sitemap: BunextPageSitemapFn = async ({ route }) => {
|
||||
// route === "/blog/[slug]"
|
||||
const posts = await getPublishedPosts();
|
||||
|
||||
return posts.map((post) => ({
|
||||
path: `/blog/${post.slug}`,
|
||||
lastmod: post.updated_at,
|
||||
priority: 0.7,
|
||||
}));
|
||||
};
|
||||
```
|
||||
|
||||
- It can be sync or async, and returns strings or `BunextSitemapEntry` objects, like `additional_paths`.
|
||||
- On a static page, the returned entries **replace** the page's default entry, so you can set its `priority`/`changefreq`, or return `[]` to leave the page out.
|
||||
- `exclude` and the `default_*` options still apply to the returned entries.
|
||||
- Generating a sitemap imports every page's server file, so their top-level code runs. If a `sitemap` function throws, generation fails with the file name in the error.
|
||||
|
||||
### `bunext sitemap` Command
|
||||
|
||||
Add a `sitemap` key to `bunext.config.ts`, then run `bunext sitemap`. By default the file is written to `public/sitemap.xml`, which is served at `/sitemap.xml`. Run it after `bunext build` in your deploy step to keep it current.
|
||||
|
||||
```ts
|
||||
// bunext.config.ts
|
||||
import type { BunextConfig } from "@moduletrace/bunext/types";
|
||||
|
||||
const config: BunextConfig = {
|
||||
origin: "https://example.com",
|
||||
sitemap: {
|
||||
exclude: ["/login", "/admin/*"],
|
||||
default_changefreq: "weekly",
|
||||
// A function (sync or async) lets you load dynamic routes
|
||||
additional_paths: async () => {
|
||||
const posts = await getPublishedPosts();
|
||||
return posts.map((post) => ({
|
||||
path: `/blog/${post.slug}`,
|
||||
lastmod: post.updated_at,
|
||||
}));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
```bash
|
||||
bunext sitemap # writes public/sitemap.xml
|
||||
bunext sitemap -o public/sitemaps/main.xml # custom output path
|
||||
bunext sitemap --origin https://staging.example.com
|
||||
```
|
||||
|
||||
`--output` and `--origin` override `sitemap.output` and `sitemap.origin` from the config.
|
||||
|
||||
### Generating at Runtime
|
||||
|
||||
`bunextGenerateSitemap()` resolves to the same XML as a string, so you can serve it from a route instead of a file:
|
||||
|
||||
```ts
|
||||
// src/pages/api/sitemap.ts
|
||||
import bunext from "@moduletrace/bunext";
|
||||
|
||||
export default async function handler(req: Request) {
|
||||
const xml = await bunext.bunextGenerateSitemap({
|
||||
exclude: ["/login", "/admin/*", /^\/drafts/],
|
||||
additional_paths: async () => {
|
||||
const posts = await getPublishedPosts();
|
||||
return posts.map((post) => ({
|
||||
path: `/blog/${post.slug}`,
|
||||
lastmod: post.updated_at,
|
||||
}));
|
||||
},
|
||||
default_changefreq: "weekly",
|
||||
});
|
||||
|
||||
return new Response(xml, {
|
||||
headers: { "Content-Type": "application/xml" },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Type | Description |
|
||||
| -------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `origin` | `string` | Base URL for each `<loc>`. Defaults to `config.origin`, then `http://localhost:<port>`. |
|
||||
| `exclude` | `(string \| RegExp)[]` | Exact paths, subtrees ending in `/*` (`/admin/*` also excludes `/admin`), or regexes tested on the path. |
|
||||
| `additional_paths` | `entries \| () => entries \| Promise<entries>` | Extra paths or entries (`path`, `lastmod`, `changefreq`, `priority`), or a sync/async function returning them, e.g. for dynamic routes. Each entry is a `string` or `BunextSitemapEntry`. |
|
||||
| `use_file_lastmod` | `boolean` | Use each page file's modification time as `lastmod`. Default `true`. |
|
||||
| `trailing_slash` | `boolean` | Append `/` to every non-root path. Default `false`. |
|
||||
| `default_changefreq` | `BunextSitemapChangeFreq` | `changefreq` for entries that do not set one. |
|
||||
| `default_priority` | `number` | `priority` (0.0 to 1.0) for entries that do not set one. |
|
||||
|
||||
The `sitemap` config key takes the same options, plus `output`: the file path relative to the project root (default `public/sitemap.xml`).
|
||||
|
||||
---
|
||||
|
||||
## Static Files
|
||||
|
||||
Place static files in the `public/` directory. They are served at the `/public/` URL path:
|
||||
@@ -764,6 +882,7 @@ export default config;
|
||||
| `middleware` | `(params: BunextConfigMiddlewareParams) => Response \| undefined \| Promise<...>` | — | Global middleware — see [Middleware](#middleware) |
|
||||
| `websocket` | `WebSocketHandler<any>` | — | Bun WebSocket handler — see [WebSocket](#websocket) |
|
||||
| `serverOptions` | `ServeOptions` | — | Extra options passed to `Bun.serve()` (excluding `fetch`) — see [Server Options](#server-options) |
|
||||
| `sitemap` | `BunextSitemapConfig` | — | Options for `bunext sitemap` — see [Sitemap](#sitemap) |
|
||||
|
||||
### Middleware
|
||||
|
||||
|
||||
Reference in New Issue
Block a user