@whatworks/payload-sitemap 0.1.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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 What Works Global
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,201 @@
1
+ # @whatworks/payload-sitemap
2
+
3
+ Chunked, lazily cached XML sitemaps for [Payload](https://payloadcms.com) with hook-driven invalidation and robots.txt helpers.
4
+
5
+ Next.js + Vercel work with near-zero config; every layer — delivery, caching, invalidation, robots output — is overridable for other stacks (decoupled frontends, self-hosted servers, SSG builds).
6
+
7
+ ## How it works
8
+
9
+ - **Lazy generation.** Nothing regenerates on save. Collection hooks only mark a group's cache dirty (deduped per request, run after the response so the transaction has committed). The next sitemap request regenerates that group from the database — slug changes, breadcrumb cascades (e.g. `plugin-nested-docs` re-saving children), publishes, unpublishes, and deletes all come out correct without any change detection.
10
+ - **One sitemap file per collection, chunked.** `/sitemap.xml` is a `<sitemapindex>` referencing `pages-1.xml`, `posts-1.xml`, … split at `chunkSize` (default 25,000; protocol limit 50,000).
11
+ - **Lean queries.** Docs are fetched in pages of 1,000 with `select`, `depth: 0`, and a `_status` filter applied only when the collection actually has drafts enabled.
12
+ - **Caching:** with `'auto'` (default), entries are cached in the Next.js Data Cache with tags (`revalidateTag` invalidation — works across serverless instances on Vercel) and fall back to an in-memory cache outside Next. Responses also carry `Cache-Control: s-maxage` so a CDN absorbs crawler traffic.
13
+
14
+ ## Quick start (Payload inside Next.js)
15
+
16
+ ```ts
17
+ // payload.config.ts
18
+ import { sitemapPlugin } from '@whatworks/payload-sitemap'
19
+
20
+ export default buildConfig({
21
+ plugins: [
22
+ sitemapPlugin({
23
+ collections: {
24
+ pages: {
25
+ path: ({ doc }) => (doc.slug === 'home' ? '/' : `/${doc.slug}`),
26
+ select: { slug: true },
27
+ },
28
+ },
29
+ }),
30
+ ],
31
+ })
32
+ ```
33
+
34
+ ```ts
35
+ // app/sitemap.xml/route.ts
36
+ import config from '@payload-config'
37
+ import { createSitemapIndexRoute } from '@whatworks/payload-sitemap/next'
38
+
39
+ export const dynamic = 'force-dynamic'
40
+ export const { GET } = createSitemapIndexRoute({ config })
41
+ ```
42
+
43
+ ```ts
44
+ // app/sitemaps/[sitemap]/route.ts
45
+ import config from '@payload-config'
46
+ import { createSitemapChunkRoute } from '@whatworks/payload-sitemap/next'
47
+
48
+ export const dynamic = 'force-dynamic'
49
+ export const { GET } = createSitemapChunkRoute({ config })
50
+ ```
51
+
52
+ ```ts
53
+ // app/robots.ts
54
+ import config from '@payload-config'
55
+ import { createRobots } from '@whatworks/payload-sitemap/next'
56
+
57
+ export default createRobots({ config })
58
+ ```
59
+
60
+ This is the entire setup — no env vars required. `robots.txt` disallows everything outside production (`VERCEL_ENV`/`NODE_ENV`), and disallows the admin + API routes in production.
61
+
62
+ ### How `siteUrl` is resolved
63
+
64
+ Every URL in the sitemap is joined onto the first available of:
65
+
66
+ 1. the `siteUrl` plugin option — a string, or a function `({ request }) => string` with full control
67
+ 2. `SITE_URL` → `NEXT_PUBLIC_SERVER_URL` → `https://$VERCEL_PROJECT_PRODUCTION_URL`
68
+ 3. **the incoming request** — `x-forwarded-proto`/`x-forwarded-host`, then `Host`, then the request URL's origin
69
+
70
+ Env vars deliberately outrank the request: a deployment is often reachable via
71
+ non-canonical aliases (`*.vercel.app`, internal hostnames), and an SEO artifact should
72
+ emit the canonical domain when one is declared. Set `SITE_URL` (or the option) in
73
+ production if your app answers on more than one host; skip it entirely and the sitemap
74
+ simply mirrors whatever host it was requested on.
75
+
76
+ Host headers are client-controlled, so request-derived origins are never written to the
77
+ shared cache — entries are cached as site-relative paths and joined onto the resolved
78
+ origin per request. A forged `Host` can only distort the forger's own response.
79
+
80
+ ## Plugin options
81
+
82
+ ```ts
83
+ sitemapPlugin({
84
+ // Required. Per-collection config, typed against your generated types.
85
+ collections: {
86
+ pages: {
87
+ // Return a path (joined to siteUrl) or an absolute URL (used verbatim).
88
+ // Return null/undefined to omit the doc.
89
+ path: ({ doc }) => `/${doc.slug}`,
90
+ // Fields your path()/lastMod() need. id/updatedAt are always included.
91
+ select: { slug: true, breadcrumbs: true },
92
+ lastMod: 'publishedAt', // field name, (doc) => Date, or false. Default 'updatedAt'
93
+ where: {}, // extra query constraints
94
+ chunkSize: 10_000, // per-collection override
95
+ changeFreq: 'weekly', // opt-in; Google ignores it
96
+ priority: 0.5, // opt-in; Google ignores it
97
+ // Override the default invalidation heuristic (invalidate on any change
98
+ // except a draft save with no published transition):
99
+ shouldInvalidate: ({ doc, previousDoc, operation }) => true,
100
+ },
101
+ },
102
+
103
+ // String, or a function with full control (multi-tenant, per-request logic).
104
+ // Default: env chain → incoming request (see “How siteUrl is resolved”).
105
+ siteUrl: ({ request }) => `https://${request.headers.get('host')}`,
106
+ trailingSlash: false,
107
+ chunkSize: 25_000,
108
+
109
+ // Extra routes: static array, or an async function (e.g. read a global).
110
+ routes: async ({ payload }) => [{ path: '/search' }],
111
+
112
+ // Injected admin fields. Default: an `excludeFromSitemap` sidebar checkbox.
113
+ // `group` nests them inside the group field (or named tab) with that name —
114
+ // e.g. an existing `metadata` group. Dot notation reaches nested containers
115
+ // (`'seo.metadata'`); missing segments are created as groups on collections
116
+ // that lack them. The exclude flag then lives at `metadata.excludeFromSitemap`.
117
+ adminFields: { exclude: true, group: 'metadata' },
118
+
119
+ // 'auto' (default) | 'memory' | 'none' | custom { wrap, invalidate } adapter.
120
+ cache: 'auto',
121
+
122
+ // REST endpoints under the API route. DISABLED by default — the Next route
123
+ // handlers above are the primary delivery.
124
+ endpoints: {
125
+ path: '/sitemap', // → /api/sitemap/index.xml, /api/sitemap/pages-1.xml
126
+ access: ({ req }) => true, // XML access; default public once enabled
127
+ json: true, // /api/sitemap/entries.json; default access: req.user
128
+ cacheControl: 'public, max-age=0, s-maxage=600, stale-while-revalidate=86400',
129
+ origin: 'https://cms.example.com', // index chunk-URL origin; default: request origin
130
+ },
131
+
132
+ // Defaults for robots output (createRobots / generateRobotsTxt).
133
+ robots: {
134
+ isProduction: undefined, // default VERCEL_ENV/NODE_ENV
135
+ disallow: ['/drafts/'], // appended to the default rule
136
+ rules: undefined, // replace default rules entirely
137
+ sitemaps: undefined, // default `${siteUrl}/sitemap.xml`
138
+ transform: (robots) => robots, // final say over the computed output
139
+ },
140
+
141
+ disabled: false, // keeps injected fields for schema/migration consistency
142
+ })
143
+ ```
144
+
145
+ ## Decoupled frontends & other stacks
146
+
147
+ Payload standalone as a headless CMS, frontend elsewhere (Astro, SvelteKit, …):
148
+
149
+ 1. **Enable the REST endpoints** (`endpoints: true`). The sitemap serves from
150
+ `https://cms.example.com/api/sitemap/index.xml`.
151
+ 2. Either **proxy** `/sitemap.xml` → the CMS endpoint from your frontend, or declare it
152
+ cross-host in the frontend's robots.txt (sanctioned by sitemaps.org):
153
+ `Sitemap: https://cms.example.com/api/sitemap/index.xml`
154
+ 3. For **build-time SSG**, fetch `/api/sitemap/entries.json` (authenticated by default)
155
+ or call `getSitemapEntries(payload)` in a build script and render however you like.
156
+ 4. `generateRobotsTxt(config, overrides?)` returns a robots.txt string for any server.
157
+
158
+ On a long-running self-hosted server, set `cache: 'memory'` — hooks invalidate it
159
+ in-process. For anything else, implement the two-method `SitemapCache` interface
160
+ (`wrap`, `invalidate`) — e.g. backed by Redis — and pass it as `cache`.
161
+
162
+ ## Public API
163
+
164
+ | Export (`.`) | Purpose |
165
+ | ------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
166
+ | `sitemapPlugin(config)` | The Payload plugin |
167
+ | `getSitemapEntries(payload, opts?)` | All cached entries keyed by group |
168
+ | `invalidateSitemap(payload, groups?)` | Manual invalidation (e.g. from a global's hook that feeds `routes`) |
169
+ | `generateRobotsTxt(config, overrides?)` | robots.txt string for any delivery |
170
+ | `getIndexItems` / `getChunkEntries` / `buildUrlsetXml` / `buildSitemapIndexXml` | Building blocks for custom delivery |
171
+ | `SITEMAP_CACHE_TAG` / `sitemapCacheTag(group)` | Next cache tags, for custom `revalidateTag` calls |
172
+
173
+ | Export (`./next`) | Purpose |
174
+ | ----------------------------------------------------------------- | --------------------------------------------------- |
175
+ | `createSitemapIndexRoute({ config, chunksPath?, cacheControl? })` | `GET` handler for `app/sitemap.xml/route.ts` |
176
+ | `createSitemapChunkRoute({ config, param?, cacheControl? })` | `GET` handler for `app/sitemaps/[sitemap]/route.ts` |
177
+ | `createRobots({ config, ...robotsOverrides })` | Default export for `app/robots.ts` |
178
+
179
+ ## Development
180
+
181
+ `pnpm dev` (from `packages/sitemap`) boots the sandbox in `dev/` — a Next app with the
182
+ admin at [/admin](http://localhost:3000/admin) (login form comes prefilled) and a demo
183
+ page at [/](http://localhost:3000) listing every delivery surface plus the live cached
184
+ entries. It seeds a drafts-enabled `pages` collection, a draftless `legal` collection,
185
+ a draft-only doc and an `excludeFromSitemap` doc into a throwaway SQLite database
186
+ (`dev/.dbs/`, gitignored — delete it to reseed). Publish/delete docs in the admin and
187
+ reload to watch hook-driven invalidation land.
188
+
189
+ ## Notes
190
+
191
+ - **`changefreq`/`priority` are opt-in** because Google ignores both; `lastmod` is the
192
+ only freshness signal it uses. Skipping them keeps the admin sidebar and XML lean.
193
+ - **Invalidation timing:** hooks schedule invalidation via Next's `after()` (post-response,
194
+ post-commit; uses `waitUntil` on Vercel) with a plain macrotask fallback outside Next.
195
+ `revalidateTag` is called with `{ expire: 0 }` for instant expiry on Next 16; Next 15
196
+ ignores the extra argument.
197
+ - **CDN staleness trade-off:** the default `s-maxage=600` means a CDN may serve a
198
+ sitemap up to 10 minutes stale after a publish — regeneration is cheap behind it, so
199
+ tune `cacheControl` if you need tighter freshness.
200
+ - Drafts are never included: the `_status` filter applies exactly when the collection
201
+ has drafts enabled, so collections without drafts also work (no `_status` query error).
@@ -0,0 +1,78 @@
1
+ import { o as RobotsOptions } from "../types-j8T8euN5.js";
2
+ import { SanitizedConfig } from "payload";
3
+ import { MetadataRoute } from "next";
4
+
5
+ //#region src/exports/next.d.ts
6
+ type PayloadConfigInput = Promise<SanitizedConfig> | SanitizedConfig;
7
+ type RouteFactoryArgs = {
8
+ /** `Cache-Control` header for the response. */
9
+ cacheControl?: string;
10
+ config: PayloadConfigInput;
11
+ };
12
+ /**
13
+ * Route handler for the sitemap index.
14
+ *
15
+ * ```ts
16
+ * // app/sitemap.xml/route.ts
17
+ * import config from '@payload-config'
18
+ * import { createSitemapIndexRoute } from '@whatworks/payload-sitemap/next'
19
+ *
20
+ * export const dynamic = 'force-dynamic'
21
+ * export const { GET } = createSitemapIndexRoute({ config })
22
+ * ```
23
+ */
24
+ declare const createSitemapIndexRoute: ({
25
+ cacheControl,
26
+ chunksPath,
27
+ config
28
+ }: {
29
+ /** Public path prefix where the chunk route is mounted. @default '/sitemaps' */
30
+ chunksPath?: string;
31
+ } & RouteFactoryArgs) => {
32
+ GET: (request: Request) => Promise<Response>;
33
+ };
34
+ /**
35
+ * Route handler for individual sitemap chunk files.
36
+ *
37
+ * ```ts
38
+ * // app/sitemaps/[sitemap]/route.ts
39
+ * import config from '@payload-config'
40
+ * import { createSitemapChunkRoute } from '@whatworks/payload-sitemap/next'
41
+ *
42
+ * export const dynamic = 'force-dynamic'
43
+ * export const { GET } = createSitemapChunkRoute({ config })
44
+ * ```
45
+ */
46
+ declare const createSitemapChunkRoute: ({
47
+ cacheControl,
48
+ config,
49
+ param
50
+ }: {
51
+ /** Name of the dynamic segment the route is mounted under. @default 'sitemap' */
52
+ param?: string;
53
+ } & RouteFactoryArgs) => {
54
+ GET: (request: Request, ctx: {
55
+ params: Promise<Record<string, string | string[] | undefined>>;
56
+ }) => Promise<Response>;
57
+ };
58
+ /**
59
+ * Default export for `app/robots.ts`. Plugin-level `robots` options apply first,
60
+ * factory overrides win, and `transform` gets the final say.
61
+ *
62
+ * ```ts
63
+ * // app/robots.ts
64
+ * import config from '@payload-config'
65
+ * import { createRobots } from '@whatworks/payload-sitemap/next'
66
+ *
67
+ * export default createRobots({ config })
68
+ * ```
69
+ */
70
+ declare const createRobots: ({
71
+ config,
72
+ ...overrides
73
+ }: {
74
+ config: PayloadConfigInput;
75
+ } & RobotsOptions) => (() => Promise<MetadataRoute.Robots>);
76
+ //#endregion
77
+ export { createRobots, createSitemapChunkRoute, createSitemapIndexRoute };
78
+ //# sourceMappingURL=next.d.ts.map
@@ -0,0 +1,118 @@
1
+ import { _ as getSitemapConfig, c as getChunkEntries, d as finalizeEntries, h as DEFAULT_CACHE_CONTROL, i as buildRobotsData, l as getIndexItems, n as buildUrlsetXml, t as buildSitemapIndexXml } from "../xml-DSOM2moN.js";
2
+ import { getPayload } from "payload";
3
+
4
+ //#region src/exports/next.ts
5
+ const xmlResponse = (xml, cacheControl) => new Response(xml, { headers: {
6
+ "Cache-Control": cacheControl,
7
+ "Content-Type": "application/xml; charset=utf-8"
8
+ } });
9
+ /**
10
+ * Metadata routes receive no request object, so when the site origin isn't
11
+ * statically configured fall back to the request headers via `next/headers`
12
+ * (which makes the route dynamic).
13
+ */
14
+ const resolveRobotsSiteUrl = async (sitemapConfig) => {
15
+ try {
16
+ return sitemapConfig.siteUrl();
17
+ } catch (staticError) {
18
+ try {
19
+ const { headers } = await import("next/headers");
20
+ return sitemapConfig.siteUrl({ request: { headers: await headers() } });
21
+ } catch {
22
+ throw staticError;
23
+ }
24
+ }
25
+ };
26
+ /**
27
+ * Route handler for the sitemap index.
28
+ *
29
+ * ```ts
30
+ * // app/sitemap.xml/route.ts
31
+ * import config from '@payload-config'
32
+ * import { createSitemapIndexRoute } from '@whatworks/payload-sitemap/next'
33
+ *
34
+ * export const dynamic = 'force-dynamic'
35
+ * export const { GET } = createSitemapIndexRoute({ config })
36
+ * ```
37
+ */
38
+ const createSitemapIndexRoute = ({ cacheControl = DEFAULT_CACHE_CONTROL, chunksPath = "/sitemaps", config }) => ({ GET: async (request) => {
39
+ const payload = await getPayload({ config: await config });
40
+ const sitemapConfig = getSitemapConfig(payload.config);
41
+ const base = `${sitemapConfig.siteUrl({ request })}${chunksPath}`;
42
+ return xmlResponse(buildSitemapIndexXml(await getIndexItems({
43
+ chunkUrl: (file) => `${base}/${file}`,
44
+ config: sitemapConfig,
45
+ payload
46
+ })), cacheControl);
47
+ } });
48
+ /**
49
+ * Route handler for individual sitemap chunk files.
50
+ *
51
+ * ```ts
52
+ * // app/sitemaps/[sitemap]/route.ts
53
+ * import config from '@payload-config'
54
+ * import { createSitemapChunkRoute } from '@whatworks/payload-sitemap/next'
55
+ *
56
+ * export const dynamic = 'force-dynamic'
57
+ * export const { GET } = createSitemapChunkRoute({ config })
58
+ * ```
59
+ */
60
+ const createSitemapChunkRoute = ({ cacheControl = DEFAULT_CACHE_CONTROL, config, param = "sitemap" }) => ({ GET: async (request, ctx) => {
61
+ const rawFile = (await ctx.params)[param];
62
+ const file = Array.isArray(rawFile) ? rawFile[0] : rawFile;
63
+ if (!file) return new Response("Not found", { status: 404 });
64
+ const payload = await getPayload({ config: await config });
65
+ const sitemapConfig = getSitemapConfig(payload.config);
66
+ const chunk = await getChunkEntries({
67
+ config: sitemapConfig,
68
+ file,
69
+ payload
70
+ });
71
+ if (!chunk) return new Response("Not found", { status: 404 });
72
+ const collConfig = sitemapConfig.collections[chunk.group];
73
+ return xmlResponse(buildUrlsetXml(finalizeEntries(chunk.entries, {
74
+ siteUrl: sitemapConfig.siteUrl({ request }),
75
+ trailingSlash: sitemapConfig.trailingSlash
76
+ }), {
77
+ changeFreq: collConfig?.changeFreq,
78
+ priority: collConfig?.priority
79
+ }), cacheControl);
80
+ } });
81
+ /**
82
+ * Default export for `app/robots.ts`. Plugin-level `robots` options apply first,
83
+ * factory overrides win, and `transform` gets the final say.
84
+ *
85
+ * ```ts
86
+ * // app/robots.ts
87
+ * import config from '@payload-config'
88
+ * import { createRobots } from '@whatworks/payload-sitemap/next'
89
+ *
90
+ * export default createRobots({ config })
91
+ * ```
92
+ */
93
+ const createRobots = ({ config, ...overrides }) => {
94
+ return async () => {
95
+ const awaited = await config;
96
+ const sitemapConfig = getSitemapConfig(awaited);
97
+ const options = {
98
+ ...sitemapConfig.robots,
99
+ ...overrides
100
+ };
101
+ const sitemaps = options.sitemaps ?? [`${await resolveRobotsSiteUrl(sitemapConfig)}/sitemap.xml`];
102
+ const data = buildRobotsData({
103
+ adminRoute: awaited.routes.admin,
104
+ apiRoute: awaited.routes.api,
105
+ options,
106
+ sitemaps
107
+ });
108
+ return {
109
+ rules: data.rules,
110
+ ...data.sitemaps.length ? { sitemap: data.sitemaps.length === 1 ? data.sitemaps[0] : data.sitemaps } : {},
111
+ ...data.host ? { host: data.host } : {}
112
+ };
113
+ };
114
+ };
115
+
116
+ //#endregion
117
+ export { createRobots, createSitemapChunkRoute, createSitemapIndexRoute };
118
+ //# sourceMappingURL=next.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next.js","names":[],"sources":["../../src/exports/next.ts"],"sourcesContent":["import type { MetadataRoute } from 'next'\nimport type { SanitizedConfig } from 'payload'\n\nimport { getPayload } from 'payload'\n\nimport type { RobotsOptions } from '../types.js'\n\nimport { getChunkEntries, getIndexItems } from '../core/chunks.js'\nimport { finalizeEntries } from '../core/entries.js'\nimport { DEFAULT_CACHE_CONTROL, getSitemapConfig } from '../core/resolved.js'\nimport { buildRobotsData } from '../core/robots.js'\nimport { buildSitemapIndexXml, buildUrlsetXml } from '../core/xml.js'\n\ntype PayloadConfigInput = Promise<SanitizedConfig> | SanitizedConfig\n\ntype RouteFactoryArgs = {\n /** `Cache-Control` header for the response. */\n cacheControl?: string\n config: PayloadConfigInput\n}\n\nconst xmlResponse = (xml: string, cacheControl: string): Response =>\n new Response(xml, {\n headers: {\n 'Cache-Control': cacheControl,\n 'Content-Type': 'application/xml; charset=utf-8',\n },\n })\n\n/**\n * Metadata routes receive no request object, so when the site origin isn't\n * statically configured fall back to the request headers via `next/headers`\n * (which makes the route dynamic).\n */\nconst resolveRobotsSiteUrl = async (\n sitemapConfig: ReturnType<typeof getSitemapConfig>,\n): Promise<string> => {\n try {\n return sitemapConfig.siteUrl()\n } catch (staticError) {\n try {\n const { headers } = await import('next/headers')\n return sitemapConfig.siteUrl({ request: { headers: await headers() } })\n } catch {\n throw staticError\n }\n }\n}\n\n/**\n * Route handler for the sitemap index.\n *\n * ```ts\n * // app/sitemap.xml/route.ts\n * import config from '@payload-config'\n * import { createSitemapIndexRoute } from '@whatworks/payload-sitemap/next'\n *\n * export const dynamic = 'force-dynamic'\n * export const { GET } = createSitemapIndexRoute({ config })\n * ```\n */\nexport const createSitemapIndexRoute = ({\n cacheControl = DEFAULT_CACHE_CONTROL,\n chunksPath = '/sitemaps',\n config,\n}: {\n /** Public path prefix where the chunk route is mounted. @default '/sitemaps' */\n chunksPath?: string\n} & RouteFactoryArgs): { GET: (request: Request) => Promise<Response> } => ({\n GET: async (request) => {\n const payload = await getPayload({ config: await config })\n const sitemapConfig = getSitemapConfig(payload.config)\n const base = `${sitemapConfig.siteUrl({ request })}${chunksPath}`\n const items = await getIndexItems({\n chunkUrl: (file) => `${base}/${file}`,\n config: sitemapConfig,\n payload,\n })\n return xmlResponse(buildSitemapIndexXml(items), cacheControl)\n },\n})\n\n/**\n * Route handler for individual sitemap chunk files.\n *\n * ```ts\n * // app/sitemaps/[sitemap]/route.ts\n * import config from '@payload-config'\n * import { createSitemapChunkRoute } from '@whatworks/payload-sitemap/next'\n *\n * export const dynamic = 'force-dynamic'\n * export const { GET } = createSitemapChunkRoute({ config })\n * ```\n */\nexport const createSitemapChunkRoute = ({\n cacheControl = DEFAULT_CACHE_CONTROL,\n config,\n param = 'sitemap',\n}: {\n /** Name of the dynamic segment the route is mounted under. @default 'sitemap' */\n param?: string\n} & RouteFactoryArgs): {\n GET: (\n request: Request,\n ctx: { params: Promise<Record<string, string | string[] | undefined>> },\n ) => Promise<Response>\n} => ({\n GET: async (request, ctx) => {\n const params = await ctx.params\n const rawFile = params[param]\n const file = Array.isArray(rawFile) ? rawFile[0] : rawFile\n if (!file) {\n return new Response('Not found', { status: 404 })\n }\n\n const payload = await getPayload({ config: await config })\n const sitemapConfig = getSitemapConfig(payload.config)\n const chunk = await getChunkEntries({ config: sitemapConfig, file, payload })\n if (!chunk) {\n return new Response('Not found', { status: 404 })\n }\n\n const collConfig = sitemapConfig.collections[chunk.group]\n const entries = finalizeEntries(chunk.entries, {\n siteUrl: sitemapConfig.siteUrl({ request }),\n trailingSlash: sitemapConfig.trailingSlash,\n })\n const xml = buildUrlsetXml(entries, {\n changeFreq: collConfig?.changeFreq,\n priority: collConfig?.priority,\n })\n return xmlResponse(xml, cacheControl)\n },\n})\n\n/**\n * Default export for `app/robots.ts`. Plugin-level `robots` options apply first,\n * factory overrides win, and `transform` gets the final say.\n *\n * ```ts\n * // app/robots.ts\n * import config from '@payload-config'\n * import { createRobots } from '@whatworks/payload-sitemap/next'\n *\n * export default createRobots({ config })\n * ```\n */\nexport const createRobots = ({\n config,\n ...overrides\n}: { config: PayloadConfigInput } & RobotsOptions): (() => Promise<MetadataRoute.Robots>) => {\n return async () => {\n const awaited = await config\n const sitemapConfig = getSitemapConfig(awaited)\n const options = { ...sitemapConfig.robots, ...overrides }\n const sitemaps = options.sitemaps ?? [\n `${await resolveRobotsSiteUrl(sitemapConfig)}/sitemap.xml`,\n ]\n\n const data = buildRobotsData({\n adminRoute: awaited.routes.admin,\n apiRoute: awaited.routes.api,\n options,\n sitemaps,\n })\n\n return {\n rules: data.rules,\n ...(data.sitemaps.length\n ? { sitemap: data.sitemaps.length === 1 ? data.sitemaps[0] : data.sitemaps }\n : {}),\n ...(data.host ? { host: data.host } : {}),\n }\n }\n}\n"],"mappings":";;;;AAqBA,MAAM,eAAe,KAAa,iBAChC,IAAI,SAAS,KAAK,EAChB,SAAS;CACP,iBAAiB;CACjB,gBAAgB;CACjB,EACF,CAAC;;;;;;AAOJ,MAAM,uBAAuB,OAC3B,kBACoB;AACpB,KAAI;AACF,SAAO,cAAc,SAAS;UACvB,aAAa;AACpB,MAAI;GACF,MAAM,EAAE,YAAY,MAAM,OAAO;AACjC,UAAO,cAAc,QAAQ,EAAE,SAAS,EAAE,SAAS,MAAM,SAAS,EAAE,EAAE,CAAC;UACjE;AACN,SAAM;;;;;;;;;;;;;;;;AAiBZ,MAAa,2BAA2B,EACtC,eAAe,uBACf,aAAa,aACb,cAI0E,EAC1E,KAAK,OAAO,YAAY;CACtB,MAAM,UAAU,MAAM,WAAW,EAAE,QAAQ,MAAM,QAAQ,CAAC;CAC1D,MAAM,gBAAgB,iBAAiB,QAAQ,OAAO;CACtD,MAAM,OAAO,GAAG,cAAc,QAAQ,EAAE,SAAS,CAAC,GAAG;AAMrD,QAAO,YAAY,qBALL,MAAM,cAAc;EAChC,WAAW,SAAS,GAAG,KAAK,GAAG;EAC/B,QAAQ;EACR;EACD,CAAC,CAC4C,EAAE,aAAa;GAEhE;;;;;;;;;;;;;AAcD,MAAa,2BAA2B,EACtC,eAAe,uBACf,QACA,QAAQ,iBASJ,EACJ,KAAK,OAAO,SAAS,QAAQ;CAE3B,MAAM,WADS,MAAM,IAAI,QACF;CACvB,MAAM,OAAO,MAAM,QAAQ,QAAQ,GAAG,QAAQ,KAAK;AACnD,KAAI,CAAC,KACH,QAAO,IAAI,SAAS,aAAa,EAAE,QAAQ,KAAK,CAAC;CAGnD,MAAM,UAAU,MAAM,WAAW,EAAE,QAAQ,MAAM,QAAQ,CAAC;CAC1D,MAAM,gBAAgB,iBAAiB,QAAQ,OAAO;CACtD,MAAM,QAAQ,MAAM,gBAAgB;EAAE,QAAQ;EAAe;EAAM;EAAS,CAAC;AAC7E,KAAI,CAAC,MACH,QAAO,IAAI,SAAS,aAAa,EAAE,QAAQ,KAAK,CAAC;CAGnD,MAAM,aAAa,cAAc,YAAY,MAAM;AASnD,QAAO,YAJK,eAJI,gBAAgB,MAAM,SAAS;EAC7C,SAAS,cAAc,QAAQ,EAAE,SAAS,CAAC;EAC3C,eAAe,cAAc;EAC9B,CAAC,EACkC;EAClC,YAAY,YAAY;EACxB,UAAU,YAAY;EACvB,CAAC,EACsB,aAAa;GAExC;;;;;;;;;;;;;AAcD,MAAa,gBAAgB,EAC3B,QACA,GAAG,gBACwF;AAC3F,QAAO,YAAY;EACjB,MAAM,UAAU,MAAM;EACtB,MAAM,gBAAgB,iBAAiB,QAAQ;EAC/C,MAAM,UAAU;GAAE,GAAG,cAAc;GAAQ,GAAG;GAAW;EACzD,MAAM,WAAW,QAAQ,YAAY,CACnC,GAAG,MAAM,qBAAqB,cAAc,CAAC,cAC9C;EAED,MAAM,OAAO,gBAAgB;GAC3B,YAAY,QAAQ,OAAO;GAC3B,UAAU,QAAQ,OAAO;GACzB;GACA;GACD,CAAC;AAEF,SAAO;GACL,OAAO,KAAK;GACZ,GAAI,KAAK,SAAS,SACd,EAAE,SAAS,KAAK,SAAS,WAAW,IAAI,KAAK,SAAS,KAAK,KAAK,UAAU,GAC1E,EAAE;GACN,GAAI,KAAK,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,EAAE;GACzC"}
@@ -0,0 +1,147 @@
1
+ import { _ as SitemapRoute, a as RobotsData, b as resolveSiteUrl, c as SitemapCache, d as SitemapEndpointAccess, f as SitemapEndpointsConfig, g as SitemapPluginConfig, h as SitemapPathArgs, i as ResolvedSitemapEndpoints, l as SitemapCollectionConfig, m as SitemapInvalidateArgs, n as InternalSitemapCollectionConfig, o as RobotsOptions, p as SitemapEntry, r as ResolvedSitemapConfig, s as RobotsRule, t as ChangeFrequency, u as SitemapCollections, v as SitemapRoutesFn, x as siteUrlFromRequest, y as SiteUrlContext } from "./types-j8T8euN5.js";
2
+ import { Payload, PayloadRequest, Plugin, SanitizedConfig } from "payload";
3
+
4
+ //#region src/core/cache.d.ts
5
+ declare const SITEMAP_CACHE_TAG = "payload-sitemap";
6
+ declare const sitemapCacheTag: (group: string) => string;
7
+ declare const createMemoryCache: () => SitemapCache;
8
+ declare const noopCache: SitemapCache;
9
+ /**
10
+ * Caches entries in the Next.js Data Cache tagged `payload-sitemap:<group>`, so
11
+ * invalidation is `revalidateTag` — shared across serverless instances on Vercel.
12
+ * Degrades to uncached execution when `next/cache` is unavailable or errors
13
+ * (standalone Payload, seed scripts, non-request contexts).
14
+ */
15
+ declare const createNextTagsCache: () => SitemapCache;
16
+ //#endregion
17
+ //#region src/core/chunks.d.ts
18
+ declare const chunkFileName: (group: string, index: number) => string;
19
+ /**
20
+ * Matches a chunk filename against the known groups by prefix, so group slugs
21
+ * containing hyphens or digits parse unambiguously.
22
+ */
23
+ declare const matchChunkFile: (file: string, groups: string[]) => {
24
+ group: string;
25
+ index: number;
26
+ } | null;
27
+ type BaseArgs = {
28
+ config: ResolvedSitemapConfig;
29
+ payload: Payload;
30
+ req?: PayloadRequest;
31
+ };
32
+ /** Items for the `<sitemapindex>`: one per chunk of each non-empty group. */
33
+ declare const getIndexItems: (args: {
34
+ chunkUrl: (file: string) => string;
35
+ } & BaseArgs) => Promise<Array<{
36
+ lastmod?: string;
37
+ loc: string;
38
+ }>>;
39
+ /**
40
+ * Entries for one chunk file, or `null` when the filename matches no group/range.
41
+ * `loc` values are site-relative — pass through `finalizeEntries` before rendering.
42
+ */
43
+ declare const getChunkEntries: (args: {
44
+ file: string;
45
+ } & BaseArgs) => Promise<{
46
+ entries: SitemapEntry[];
47
+ group: string;
48
+ } | null>;
49
+ //#endregion
50
+ //#region src/core/entries.d.ts
51
+ declare const formatLoc: (path: string, siteUrl: string, trailingSlash: boolean) => string;
52
+ /**
53
+ * Joins cached site-relative entries onto the resolved site origin. Entries are
54
+ * cached with relative `loc` paths so cached data is host-independent — a
55
+ * request-derived siteUrl can never leak into the shared cache.
56
+ */
57
+ declare const finalizeEntries: (entries: SitemapEntry[], {
58
+ siteUrl,
59
+ trailingSlash
60
+ }: {
61
+ siteUrl: string;
62
+ trailingSlash: boolean;
63
+ }) => SitemapEntry[];
64
+ type FetchArgs = {
65
+ config: ResolvedSitemapConfig;
66
+ payload: Payload;
67
+ req?: PayloadRequest;
68
+ };
69
+ /**
70
+ * Cached entries for one group (a collection slug or `ROUTES_GROUP`).
71
+ * `loc` values are site-relative — pass through `finalizeEntries` before rendering.
72
+ */
73
+ declare const getGroupEntries: (args: {
74
+ group: string;
75
+ } & FetchArgs) => Promise<SitemapEntry[]>;
76
+ /**
77
+ * All sitemap entries with absolute URLs, keyed by group. Public API for SSG
78
+ * frontends and custom delivery — reads the plugin config from the Payload
79
+ * instance. Pass `req` (or `request` — a Fetch Request, or
80
+ * `{ headers: await headers() }` in an RSC) so the site origin can be derived
81
+ * from the request when it isn't configured statically.
82
+ */
83
+ declare const getSitemapEntries: (payload: Payload, options?: {
84
+ groups?: string[];
85
+ req?: PayloadRequest;
86
+ request?: {
87
+ headers?: Headers;
88
+ url?: null | string;
89
+ };
90
+ }) => Promise<Record<string, SitemapEntry[]>>;
91
+ //#endregion
92
+ //#region src/core/invalidate.d.ts
93
+ /**
94
+ * Manually invalidate cached sitemap groups — e.g. from a hook on a global that
95
+ * feeds the `routes` option. Invalidates every group when none are given.
96
+ */
97
+ declare const invalidateSitemap: (payload: Payload, groups?: string[]) => Promise<void>;
98
+ //#endregion
99
+ //#region src/core/resolved.d.ts
100
+ /** Reserved group name for entries from the `routes` option. */
101
+ declare const ROUTES_GROUP = "_routes";
102
+ /** Reads the resolved plugin config stashed on the Payload config by `sitemapPlugin`. */
103
+ declare const getSitemapConfig: (config: SanitizedConfig) => ResolvedSitemapConfig;
104
+ //#endregion
105
+ //#region src/core/robots.d.ts
106
+ type BuildRobotsArgs = {
107
+ /** Payload admin route, used for the default disallow list. @default '/admin' */
108
+ adminRoute?: string;
109
+ /** Payload API route, used for the default disallow list. @default '/api' */
110
+ apiRoute?: string;
111
+ options?: RobotsOptions;
112
+ /** Absolute sitemap URL(s) advertised in the output. */
113
+ sitemaps: string[];
114
+ };
115
+ declare const buildRobotsData: ({
116
+ adminRoute,
117
+ apiRoute,
118
+ options,
119
+ sitemaps
120
+ }: BuildRobotsArgs) => RobotsData;
121
+ declare const renderRobotsTxt: (data: RobotsData) => string;
122
+ /**
123
+ * robots.txt for any delivery mechanism. Defaults come from the plugin config;
124
+ * `overrides` win field-by-field, and `transform` gets the final say. Pass
125
+ * `request` so the default sitemap URL can derive its origin from the incoming
126
+ * request when `siteUrl` isn't configured statically.
127
+ */
128
+ declare const generateRobotsTxt: (configInput: Promise<SanitizedConfig> | SanitizedConfig, overrides?: {
129
+ request?: SiteUrlContext["request"];
130
+ } & RobotsOptions) => Promise<string>;
131
+ //#endregion
132
+ //#region src/core/xml.d.ts
133
+ declare const escapeXml: (value: string) => string;
134
+ declare const buildUrlsetXml: (entries: SitemapEntry[], defaults?: {
135
+ changeFreq?: ChangeFrequency;
136
+ priority?: number;
137
+ }) => string;
138
+ declare const buildSitemapIndexXml: (items: Array<{
139
+ lastmod?: string;
140
+ loc: string;
141
+ }>) => string;
142
+ //#endregion
143
+ //#region src/plugin.d.ts
144
+ declare const sitemapPlugin: (pluginConfig: SitemapPluginConfig) => Plugin;
145
+ //#endregion
146
+ export { ChangeFrequency, InternalSitemapCollectionConfig, ROUTES_GROUP, ResolvedSitemapConfig, ResolvedSitemapEndpoints, RobotsData, RobotsOptions, RobotsRule, SITEMAP_CACHE_TAG, type SiteUrlContext, SitemapCache, SitemapCollectionConfig, SitemapCollections, SitemapEndpointAccess, SitemapEndpointsConfig, SitemapEntry, SitemapInvalidateArgs, SitemapPathArgs, SitemapPluginConfig, SitemapRoute, SitemapRoutesFn, buildRobotsData, buildSitemapIndexXml, buildUrlsetXml, chunkFileName, createMemoryCache, createNextTagsCache, escapeXml, finalizeEntries, formatLoc, generateRobotsTxt, getChunkEntries, getGroupEntries, getIndexItems, getSitemapConfig, getSitemapEntries, invalidateSitemap, matchChunkFile, noopCache, renderRobotsTxt, resolveSiteUrl, siteUrlFromRequest, sitemapCacheTag, sitemapPlugin };
147
+ //# sourceMappingURL=index.d.ts.map