@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 +21 -0
- package/README.md +201 -0
- package/dist/exports/next.d.ts +78 -0
- package/dist/exports/next.js +118 -0
- package/dist/exports/next.js.map +1 -0
- package/dist/index.d.ts +147 -0
- package/dist/index.js +281 -0
- package/dist/index.js.map +1 -0
- package/dist/types-j8T8euN5.d.ts +271 -0
- package/dist/xml-DSOM2moN.js +487 -0
- package/dist/xml-DSOM2moN.js.map +1 -0
- package/package.json +90 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { C as createNextTagsCache, S as createMemoryCache, T as sitemapCacheTag, _ as getSitemapConfig, a as generateRobotsTxt, b as siteUrlFromRequest, c as getChunkEntries, d as finalizeEntries, f as formatLoc, g as ROUTES_GROUP, i as buildRobotsData, l as getIndexItems, m as getSitemapEntries, n as buildUrlsetXml, o as renderRobotsTxt, p as getGroupEntries, r as escapeXml, s as chunkFileName, t as buildSitemapIndexXml, u as matchChunkFile, v as resolveSitemapConfig, w as noopCache, x as SITEMAP_CACHE_TAG, y as resolveSiteUrl } from "./xml-DSOM2moN.js";
|
|
2
|
+
|
|
3
|
+
//#region src/core/invalidate.ts
|
|
4
|
+
/** Groups already scheduled for invalidation within one request (bulk-operation dedupe). */
|
|
5
|
+
const scheduledPerRequest = /* @__PURE__ */ new WeakMap();
|
|
6
|
+
/**
|
|
7
|
+
* Runs `fn` after the current request settles. `afterChange` hooks execute inside
|
|
8
|
+
* the operation's transaction — invalidating immediately could regenerate from a
|
|
9
|
+
* pre-commit snapshot. Next's `after()` fires post-response (post-commit); outside
|
|
10
|
+
* a Next request scope we fall back to a macrotask, which the local API's awaited
|
|
11
|
+
* operation has committed by.
|
|
12
|
+
*/
|
|
13
|
+
const runAfterRequest = (fn) => {
|
|
14
|
+
import("next/server").then((mod) => {
|
|
15
|
+
try {
|
|
16
|
+
if (mod.after) {
|
|
17
|
+
mod.after(fn);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
} catch {}
|
|
21
|
+
setTimeout(fn, 0);
|
|
22
|
+
}, () => setTimeout(fn, 0));
|
|
23
|
+
};
|
|
24
|
+
const scheduleInvalidation = (req, config, group) => {
|
|
25
|
+
let scheduled = scheduledPerRequest.get(req);
|
|
26
|
+
if (!scheduled) {
|
|
27
|
+
scheduled = /* @__PURE__ */ new Set();
|
|
28
|
+
scheduledPerRequest.set(req, scheduled);
|
|
29
|
+
}
|
|
30
|
+
if (scheduled.has(group)) return;
|
|
31
|
+
scheduled.add(group);
|
|
32
|
+
const logger = req.payload.logger;
|
|
33
|
+
runAfterRequest(() => {
|
|
34
|
+
Promise.resolve(config.cache.invalidate([group])).catch((err) => {
|
|
35
|
+
logger.error({ err }, `[payload-sitemap] Failed to invalidate group "${group}"`);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
const defaultShouldInvalidate = (doc, previousDoc) => {
|
|
40
|
+
const status = doc._status;
|
|
41
|
+
if (typeof status !== "string") return true;
|
|
42
|
+
return status === "published" || previousDoc?._status === "published";
|
|
43
|
+
};
|
|
44
|
+
const createAfterChangeHook = (slug, collConfig, config) => {
|
|
45
|
+
return ({ doc, operation, previousDoc, req }) => {
|
|
46
|
+
if (collConfig.shouldInvalidate ? collConfig.shouldInvalidate({
|
|
47
|
+
doc,
|
|
48
|
+
operation,
|
|
49
|
+
previousDoc
|
|
50
|
+
}) : defaultShouldInvalidate(doc, previousDoc)) scheduleInvalidation(req, config, slug);
|
|
51
|
+
return doc;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
const createAfterDeleteHook = (slug, config) => {
|
|
55
|
+
return ({ doc, req }) => {
|
|
56
|
+
scheduleInvalidation(req, config, slug);
|
|
57
|
+
return doc;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Manually invalidate cached sitemap groups — e.g. from a hook on a global that
|
|
62
|
+
* feeds the `routes` option. Invalidates every group when none are given.
|
|
63
|
+
*/
|
|
64
|
+
const invalidateSitemap = async (payload, groups) => {
|
|
65
|
+
const config = getSitemapConfig(payload.config);
|
|
66
|
+
await config.cache.invalidate(groups ?? config.groups);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/endpoints/createEndpoints.ts
|
|
71
|
+
const checkAccess = async (req, access) => access ? await access({ req }) : true;
|
|
72
|
+
const forbidden = () => new Response("Forbidden", { status: 403 });
|
|
73
|
+
const xmlResponse = (xml, cacheControl) => new Response(xml, { headers: {
|
|
74
|
+
"Cache-Control": cacheControl,
|
|
75
|
+
"Content-Type": "application/xml; charset=utf-8"
|
|
76
|
+
} });
|
|
77
|
+
/**
|
|
78
|
+
* The origin the index uses to reference its own chunk files. These endpoints live
|
|
79
|
+
* on the CMS origin (which may differ from `siteUrl` in decoupled setups), so it
|
|
80
|
+
* is derived from the request unless explicitly configured.
|
|
81
|
+
*/
|
|
82
|
+
const endpointOrigin = (req, endpoints) => endpoints.origin ?? new URL(req.url ?? "http://localhost").origin;
|
|
83
|
+
const createSitemapEndpoints = (config) => {
|
|
84
|
+
const endpoints = config.endpoints;
|
|
85
|
+
if (!endpoints) return [];
|
|
86
|
+
const built = [{
|
|
87
|
+
handler: async (req) => {
|
|
88
|
+
if (!await checkAccess(req, endpoints.access)) return forbidden();
|
|
89
|
+
const base = `${endpointOrigin(req, endpoints)}${req.payload.config.routes.api}${endpoints.path}`;
|
|
90
|
+
return xmlResponse(buildSitemapIndexXml(await getIndexItems({
|
|
91
|
+
chunkUrl: (file) => `${base}/${file}`,
|
|
92
|
+
config,
|
|
93
|
+
payload: req.payload,
|
|
94
|
+
req
|
|
95
|
+
})), endpoints.cacheControl);
|
|
96
|
+
},
|
|
97
|
+
method: "get",
|
|
98
|
+
path: `${endpoints.path}/index.xml`
|
|
99
|
+
}];
|
|
100
|
+
if (endpoints.json) {
|
|
101
|
+
const jsonAccess = endpoints.json.access;
|
|
102
|
+
built.push({
|
|
103
|
+
handler: async (req) => {
|
|
104
|
+
if (!await checkAccess(req, jsonAccess)) return forbidden();
|
|
105
|
+
const entries = await getSitemapEntries(req.payload, { req });
|
|
106
|
+
return Response.json({ entries }, { headers: { "Cache-Control": endpoints.cacheControl } });
|
|
107
|
+
},
|
|
108
|
+
method: "get",
|
|
109
|
+
path: `${endpoints.path}/entries.json`
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
built.push({
|
|
113
|
+
handler: async (req) => {
|
|
114
|
+
if (!await checkAccess(req, endpoints.access)) return forbidden();
|
|
115
|
+
const file = req.routeParams?.file;
|
|
116
|
+
if (typeof file !== "string") return new Response("Not found", { status: 404 });
|
|
117
|
+
const chunk = await getChunkEntries({
|
|
118
|
+
config,
|
|
119
|
+
file,
|
|
120
|
+
payload: req.payload,
|
|
121
|
+
req
|
|
122
|
+
});
|
|
123
|
+
if (!chunk) return new Response("Not found", { status: 404 });
|
|
124
|
+
const collConfig = config.collections[chunk.group];
|
|
125
|
+
return xmlResponse(buildUrlsetXml(finalizeEntries(chunk.entries, {
|
|
126
|
+
siteUrl: config.siteUrl({ request: req }),
|
|
127
|
+
trailingSlash: config.trailingSlash
|
|
128
|
+
}), {
|
|
129
|
+
changeFreq: collConfig?.changeFreq,
|
|
130
|
+
priority: collConfig?.priority
|
|
131
|
+
}), endpoints.cacheControl);
|
|
132
|
+
},
|
|
133
|
+
method: "get",
|
|
134
|
+
path: `${endpoints.path}/:file`
|
|
135
|
+
});
|
|
136
|
+
return built;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/plugin.ts
|
|
141
|
+
/** Sidebar positioning only applies at the collection root; nested fields render inline. */
|
|
142
|
+
const excludeFromSitemapField = (nested) => ({
|
|
143
|
+
name: "excludeFromSitemap",
|
|
144
|
+
type: "checkbox",
|
|
145
|
+
...nested ? {} : { admin: { position: "sidebar" } },
|
|
146
|
+
label: "Exclude from sitemap"
|
|
147
|
+
});
|
|
148
|
+
/** Missing path segments become nested group fields, with `injected` innermost. */
|
|
149
|
+
const buildGroupChain = (segments, injected) => {
|
|
150
|
+
const [head, ...rest] = segments;
|
|
151
|
+
return {
|
|
152
|
+
name: head,
|
|
153
|
+
type: "group",
|
|
154
|
+
fields: rest.length ? [buildGroupChain(rest, injected)] : injected
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Appends `injected` to the container at `segments` — each segment a group
|
|
159
|
+
* field or named tab. Descends into layout-only containers (rows, collapsibles,
|
|
160
|
+
* unnamed groups and tabs) since they don't affect the data path, but only
|
|
161
|
+
* follows named groups/tabs when they match the next segment — a match inside
|
|
162
|
+
* any other named container would live at a different query path than the
|
|
163
|
+
* `<group>.excludeFromSitemap` filter used at generation time. Once a segment
|
|
164
|
+
* matches, missing deeper segments are created inside it as group fields.
|
|
165
|
+
*/
|
|
166
|
+
const injectAtPath = (fields, segments, injected, collectionSlug) => {
|
|
167
|
+
const [segment, ...rest] = segments;
|
|
168
|
+
let found = false;
|
|
169
|
+
/** Children of a matched container: inject directly, descend, or create the missing tail. */
|
|
170
|
+
const withInjected = (children) => {
|
|
171
|
+
if (!rest.length) return [...children, ...injected];
|
|
172
|
+
const result = injectAtPath(children, rest, injected, collectionSlug);
|
|
173
|
+
return result.injected ? result.fields : [...children, buildGroupChain(rest, injected)];
|
|
174
|
+
};
|
|
175
|
+
return {
|
|
176
|
+
fields: fields.map((field) => {
|
|
177
|
+
if (found) return field;
|
|
178
|
+
if ("name" in field && field.name === segment) {
|
|
179
|
+
if (field.type !== "group") throw new Error(`[payload-sitemap] adminFields.group segment "${segment}" matches a "${field.type}" field on collection "${collectionSlug}" — each segment must be a group field or a named tab.`);
|
|
180
|
+
found = true;
|
|
181
|
+
return {
|
|
182
|
+
...field,
|
|
183
|
+
fields: withInjected(field.fields)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (field.type === "tabs") {
|
|
187
|
+
const tabs = field.tabs.map((tab) => {
|
|
188
|
+
if (found) return tab;
|
|
189
|
+
if ("name" in tab && tab.name === segment) {
|
|
190
|
+
found = true;
|
|
191
|
+
return {
|
|
192
|
+
...tab,
|
|
193
|
+
fields: withInjected(tab.fields)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (!("name" in tab)) {
|
|
197
|
+
const result = injectAtPath(tab.fields, segments, injected, collectionSlug);
|
|
198
|
+
if (result.injected) {
|
|
199
|
+
found = true;
|
|
200
|
+
return {
|
|
201
|
+
...tab,
|
|
202
|
+
fields: result.fields
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return tab;
|
|
207
|
+
});
|
|
208
|
+
return found ? {
|
|
209
|
+
...field,
|
|
210
|
+
tabs
|
|
211
|
+
} : field;
|
|
212
|
+
}
|
|
213
|
+
if (field.type === "row" || field.type === "collapsible" || field.type === "group" && !("name" in field)) {
|
|
214
|
+
const result = injectAtPath(field.fields, segments, injected, collectionSlug);
|
|
215
|
+
if (result.injected) {
|
|
216
|
+
found = true;
|
|
217
|
+
return {
|
|
218
|
+
...field,
|
|
219
|
+
fields: result.fields
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return field;
|
|
224
|
+
}),
|
|
225
|
+
injected: found
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
const sitemapPlugin = (pluginConfig) => (config) => {
|
|
229
|
+
const slugs = Object.keys(pluginConfig.collections);
|
|
230
|
+
/**
|
|
231
|
+
* Fields are injected even when the plugin is disabled so the database schema
|
|
232
|
+
* stays consistent for migrations.
|
|
233
|
+
*/
|
|
234
|
+
if (pluginConfig.adminFields?.exclude !== false) {
|
|
235
|
+
const groupPath = pluginConfig.adminFields?.group;
|
|
236
|
+
const segments = groupPath ? groupPath.split(".") : [];
|
|
237
|
+
if (segments.some((segment) => !segment)) throw new Error(`[payload-sitemap] adminFields.group "${groupPath}" is not a valid field path.`);
|
|
238
|
+
const injected = [excludeFromSitemapField(segments.length > 0)];
|
|
239
|
+
config.collections = (config.collections ?? []).map((collection) => {
|
|
240
|
+
if (!slugs.includes(collection.slug)) return collection;
|
|
241
|
+
if (!segments.length) return {
|
|
242
|
+
...collection,
|
|
243
|
+
fields: [...collection.fields, ...injected]
|
|
244
|
+
};
|
|
245
|
+
const result = injectAtPath(collection.fields, segments, injected, collection.slug);
|
|
246
|
+
if (result.injected) return {
|
|
247
|
+
...collection,
|
|
248
|
+
fields: result.fields
|
|
249
|
+
};
|
|
250
|
+
return {
|
|
251
|
+
...collection,
|
|
252
|
+
fields: [...collection.fields, buildGroupChain(segments, injected)]
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (pluginConfig.disabled) return config;
|
|
257
|
+
for (const slug of slugs) if (!config.collections?.some((collection) => collection.slug === slug)) console.warn(`[payload-sitemap] Collection "${slug}" is configured for the sitemap but does not exist.`);
|
|
258
|
+
const resolved = resolveSitemapConfig(pluginConfig);
|
|
259
|
+
config.custom = {
|
|
260
|
+
...config.custom,
|
|
261
|
+
sitemap: resolved
|
|
262
|
+
};
|
|
263
|
+
config.collections = (config.collections ?? []).map((collection) => {
|
|
264
|
+
const collConfig = resolved.collections[collection.slug];
|
|
265
|
+
if (!collConfig) return collection;
|
|
266
|
+
return {
|
|
267
|
+
...collection,
|
|
268
|
+
hooks: {
|
|
269
|
+
...collection.hooks,
|
|
270
|
+
afterChange: [...collection.hooks?.afterChange ?? [], createAfterChangeHook(collection.slug, collConfig, resolved)],
|
|
271
|
+
afterDelete: [...collection.hooks?.afterDelete ?? [], createAfterDeleteHook(collection.slug, resolved)]
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
if (resolved.endpoints) config.endpoints = [...config.endpoints ?? [], ...createSitemapEndpoints(resolved)];
|
|
276
|
+
return config;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
//#endregion
|
|
280
|
+
export { ROUTES_GROUP, SITEMAP_CACHE_TAG, buildRobotsData, buildSitemapIndexXml, buildUrlsetXml, chunkFileName, createMemoryCache, createNextTagsCache, escapeXml, finalizeEntries, formatLoc, generateRobotsTxt, getChunkEntries, getGroupEntries, getIndexItems, getSitemapConfig, getSitemapEntries, invalidateSitemap, matchChunkFile, noopCache, renderRobotsTxt, resolveSiteUrl, siteUrlFromRequest, sitemapCacheTag, sitemapPlugin };
|
|
281
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["built: Endpoint[]","injected: Field[]"],"sources":["../src/core/invalidate.ts","../src/endpoints/createEndpoints.ts","../src/plugin.ts"],"sourcesContent":["import type {\n CollectionAfterChangeHook,\n CollectionAfterDeleteHook,\n JsonObject,\n Payload,\n PayloadRequest,\n} from 'payload'\n\nimport type { InternalSitemapCollectionConfig, ResolvedSitemapConfig } from '../types.js'\n\nimport { getSitemapConfig } from './resolved.js'\n\n/** Groups already scheduled for invalidation within one request (bulk-operation dedupe). */\nconst scheduledPerRequest = new WeakMap<PayloadRequest, Set<string>>()\n\ntype NextServerModule = { after?: (fn: () => void) => void }\n\n/**\n * Runs `fn` after the current request settles. `afterChange` hooks execute inside\n * the operation's transaction — invalidating immediately could regenerate from a\n * pre-commit snapshot. Next's `after()` fires post-response (post-commit); outside\n * a Next request scope we fall back to a macrotask, which the local API's awaited\n * operation has committed by.\n */\nconst runAfterRequest = (fn: () => void): void => {\n void import('next/server').then(\n (mod: NextServerModule) => {\n try {\n if (mod.after) {\n mod.after(fn)\n return\n }\n } catch {\n // `after()` throws outside a Next request scope (scripts, workers).\n }\n setTimeout(fn, 0)\n },\n () => setTimeout(fn, 0),\n )\n}\n\nexport const scheduleInvalidation = (\n req: PayloadRequest,\n config: ResolvedSitemapConfig,\n group: string,\n): void => {\n let scheduled = scheduledPerRequest.get(req)\n if (!scheduled) {\n scheduled = new Set()\n scheduledPerRequest.set(req, scheduled)\n }\n if (scheduled.has(group)) {\n return\n }\n scheduled.add(group)\n\n const logger = req.payload.logger\n runAfterRequest(() => {\n Promise.resolve(config.cache.invalidate([group])).catch((err: unknown) => {\n logger.error({ err }, `[payload-sitemap] Failed to invalidate group \"${group}\"`)\n })\n })\n}\n\nconst defaultShouldInvalidate = (doc: JsonObject, previousDoc?: JsonObject): boolean => {\n const status = doc._status\n // No drafts on this collection — every change is live.\n if (typeof status !== 'string') {\n return true\n }\n // Skip draft saves unless they transition a published doc (unpublish).\n return status === 'published' || previousDoc?._status === 'published'\n}\n\nexport const createAfterChangeHook = (\n slug: string,\n collConfig: InternalSitemapCollectionConfig,\n config: ResolvedSitemapConfig,\n): CollectionAfterChangeHook => {\n return ({ doc, operation, previousDoc, req }) => {\n const shouldInvalidate = collConfig.shouldInvalidate\n ? collConfig.shouldInvalidate({ doc, operation, previousDoc })\n : defaultShouldInvalidate(doc, previousDoc)\n if (shouldInvalidate) {\n scheduleInvalidation(req, config, slug)\n }\n return doc\n }\n}\n\nexport const createAfterDeleteHook = (\n slug: string,\n config: ResolvedSitemapConfig,\n): CollectionAfterDeleteHook => {\n return ({ doc, req }) => {\n scheduleInvalidation(req, config, slug)\n return doc\n }\n}\n\n/**\n * Manually invalidate cached sitemap groups — e.g. from a hook on a global that\n * feeds the `routes` option. Invalidates every group when none are given.\n */\nexport const invalidateSitemap = async (payload: Payload, groups?: string[]): Promise<void> => {\n const config = getSitemapConfig(payload.config)\n await config.cache.invalidate(groups ?? config.groups)\n}\n","import type { Endpoint, PayloadRequest } from 'payload'\n\nimport type {\n ResolvedSitemapConfig,\n ResolvedSitemapEndpoints,\n SitemapEndpointAccess,\n} from '../types.js'\n\nimport { getChunkEntries, getIndexItems } from '../core/chunks.js'\nimport { finalizeEntries, getSitemapEntries } from '../core/entries.js'\nimport { buildSitemapIndexXml, buildUrlsetXml } from '../core/xml.js'\n\nconst checkAccess = async (\n req: PayloadRequest,\n access: SitemapEndpointAccess | undefined,\n): Promise<boolean> => (access ? await access({ req }) : true)\n\nconst forbidden = (): Response => new Response('Forbidden', { status: 403 })\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 * The origin the index uses to reference its own chunk files. These endpoints live\n * on the CMS origin (which may differ from `siteUrl` in decoupled setups), so it\n * is derived from the request unless explicitly configured.\n */\nconst endpointOrigin = (req: PayloadRequest, endpoints: ResolvedSitemapEndpoints): string =>\n endpoints.origin ?? new URL(req.url ?? 'http://localhost').origin\n\nexport const createSitemapEndpoints = (config: ResolvedSitemapConfig): Endpoint[] => {\n const endpoints = config.endpoints\n if (!endpoints) {\n return []\n }\n\n const built: Endpoint[] = [\n {\n handler: async (req) => {\n if (!(await checkAccess(req, endpoints.access))) {\n return forbidden()\n }\n const base = `${endpointOrigin(req, endpoints)}${req.payload.config.routes.api}${endpoints.path}`\n const items = await getIndexItems({\n chunkUrl: (file) => `${base}/${file}`,\n config,\n payload: req.payload,\n req,\n })\n return xmlResponse(buildSitemapIndexXml(items), endpoints.cacheControl)\n },\n method: 'get',\n path: `${endpoints.path}/index.xml`,\n },\n ]\n\n if (endpoints.json) {\n const jsonAccess = endpoints.json.access\n built.push({\n handler: async (req) => {\n if (!(await checkAccess(req, jsonAccess))) {\n return forbidden()\n }\n const entries = await getSitemapEntries(req.payload, { req })\n return Response.json({ entries }, { headers: { 'Cache-Control': endpoints.cacheControl } })\n },\n method: 'get',\n path: `${endpoints.path}/entries.json`,\n })\n }\n\n // The `:file` catch-all must be registered last — Payload matches endpoints in\n // order, so it would otherwise swallow `entries.json` (and `index.xml`).\n built.push({\n handler: async (req) => {\n if (!(await checkAccess(req, endpoints.access))) {\n return forbidden()\n }\n const file = req.routeParams?.file\n if (typeof file !== 'string') {\n return new Response('Not found', { status: 404 })\n }\n const chunk = await getChunkEntries({ config, file, payload: req.payload, req })\n if (!chunk) {\n return new Response('Not found', { status: 404 })\n }\n const collConfig = config.collections[chunk.group]\n const entries = finalizeEntries(chunk.entries, {\n siteUrl: config.siteUrl({ request: req }),\n trailingSlash: config.trailingSlash,\n })\n const xml = buildUrlsetXml(entries, {\n changeFreq: collConfig?.changeFreq,\n priority: collConfig?.priority,\n })\n return xmlResponse(xml, endpoints.cacheControl)\n },\n method: 'get',\n path: `${endpoints.path}/:file`,\n })\n\n return built\n}\n","import type { CheckboxField, Config, Field, Plugin } from 'payload'\n\nimport type { SitemapPluginConfig } from './types.js'\n\nimport { createAfterChangeHook, createAfterDeleteHook } from './core/invalidate.js'\nimport { resolveSitemapConfig } from './core/resolved.js'\nimport { createSitemapEndpoints } from './endpoints/createEndpoints.js'\n\n/** Sidebar positioning only applies at the collection root; nested fields render inline. */\nconst excludeFromSitemapField = (nested: boolean): CheckboxField => ({\n name: 'excludeFromSitemap',\n type: 'checkbox',\n ...(nested ? {} : { admin: { position: 'sidebar' } }),\n label: 'Exclude from sitemap',\n})\n\ntype InjectResult = { fields: Field[]; injected: boolean }\n\n/** Missing path segments become nested group fields, with `injected` innermost. */\nconst buildGroupChain = (segments: string[], injected: Field[]): Field => {\n const [head, ...rest] = segments\n return {\n name: head,\n type: 'group',\n fields: rest.length ? [buildGroupChain(rest, injected)] : injected,\n }\n}\n\n/**\n * Appends `injected` to the container at `segments` — each segment a group\n * field or named tab. Descends into layout-only containers (rows, collapsibles,\n * unnamed groups and tabs) since they don't affect the data path, but only\n * follows named groups/tabs when they match the next segment — a match inside\n * any other named container would live at a different query path than the\n * `<group>.excludeFromSitemap` filter used at generation time. Once a segment\n * matches, missing deeper segments are created inside it as group fields.\n */\nconst injectAtPath = (\n fields: Field[],\n segments: string[],\n injected: Field[],\n collectionSlug: string,\n): InjectResult => {\n const [segment, ...rest] = segments\n let found = false\n\n /** Children of a matched container: inject directly, descend, or create the missing tail. */\n const withInjected = (children: Field[]): Field[] => {\n if (!rest.length) {\n return [...children, ...injected]\n }\n const result = injectAtPath(children, rest, injected, collectionSlug)\n return result.injected ? result.fields : [...children, buildGroupChain(rest, injected)]\n }\n\n const mapped = fields.map((field): Field => {\n if (found) {\n return field\n }\n\n if ('name' in field && field.name === segment) {\n if (field.type !== 'group') {\n throw new Error(\n `[payload-sitemap] adminFields.group segment \"${segment}\" matches a \"${field.type}\" field on collection \"${collectionSlug}\" — each segment must be a group field or a named tab.`,\n )\n }\n found = true\n return { ...field, fields: withInjected(field.fields) }\n }\n\n if (field.type === 'tabs') {\n const tabs = field.tabs.map((tab) => {\n if (found) {\n return tab\n }\n if ('name' in tab && tab.name === segment) {\n found = true\n return { ...tab, fields: withInjected(tab.fields) }\n }\n if (!('name' in tab)) {\n const result = injectAtPath(tab.fields, segments, injected, collectionSlug)\n if (result.injected) {\n found = true\n return { ...tab, fields: result.fields }\n }\n }\n return tab\n })\n return found ? { ...field, tabs } : field\n }\n\n if (\n field.type === 'row' ||\n field.type === 'collapsible' ||\n (field.type === 'group' && !('name' in field))\n ) {\n const result = injectAtPath(field.fields, segments, injected, collectionSlug)\n if (result.injected) {\n found = true\n return { ...field, fields: result.fields }\n }\n }\n\n return field\n })\n\n return { fields: mapped, injected: found }\n}\n\nexport const sitemapPlugin =\n (pluginConfig: SitemapPluginConfig): Plugin =>\n (config: Config): Config => {\n const slugs = Object.keys(pluginConfig.collections)\n\n /**\n * Fields are injected even when the plugin is disabled so the database schema\n * stays consistent for migrations.\n */\n if (pluginConfig.adminFields?.exclude !== false) {\n const groupPath = pluginConfig.adminFields?.group\n const segments = groupPath ? groupPath.split('.') : []\n if (segments.some((segment) => !segment)) {\n throw new Error(\n `[payload-sitemap] adminFields.group \"${groupPath}\" is not a valid field path.`,\n )\n }\n const injected: Field[] = [excludeFromSitemapField(segments.length > 0)]\n\n config.collections = (config.collections ?? []).map((collection) => {\n if (!slugs.includes(collection.slug)) {\n return collection\n }\n if (!segments.length) {\n return { ...collection, fields: [...collection.fields, ...injected] }\n }\n const result = injectAtPath(collection.fields, segments, injected, collection.slug)\n if (result.injected) {\n return { ...collection, fields: result.fields }\n }\n // No matching container on this collection — create the chain, so the exclude\n // flag lives at the same `<group>.excludeFromSitemap` path on every configured\n // collection.\n return {\n ...collection,\n fields: [...collection.fields, buildGroupChain(segments, injected)],\n }\n })\n }\n\n if (pluginConfig.disabled) {\n return config\n }\n\n for (const slug of slugs) {\n if (!config.collections?.some((collection) => collection.slug === slug)) {\n // eslint-disable-next-line no-console\n console.warn(\n `[payload-sitemap] Collection \"${slug}\" is configured for the sitemap but does not exist.`,\n )\n }\n }\n\n const resolved = resolveSitemapConfig(pluginConfig)\n config.custom = { ...config.custom, sitemap: resolved }\n\n config.collections = (config.collections ?? []).map((collection) => {\n const collConfig = resolved.collections[collection.slug]\n if (!collConfig) {\n return collection\n }\n return {\n ...collection,\n hooks: {\n ...collection.hooks,\n afterChange: [\n ...(collection.hooks?.afterChange ?? []),\n createAfterChangeHook(collection.slug, collConfig, resolved),\n ],\n afterDelete: [\n ...(collection.hooks?.afterDelete ?? []),\n createAfterDeleteHook(collection.slug, resolved),\n ],\n },\n }\n })\n\n if (resolved.endpoints) {\n config.endpoints = [...(config.endpoints ?? []), ...createSitemapEndpoints(resolved)]\n }\n\n return config\n }\n"],"mappings":"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"}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { CollectionSlug, DataFromCollectionSlug, JsonObject, Payload, PayloadRequest, SelectType, Where } from "payload";
|
|
2
|
+
|
|
3
|
+
//#region src/core/siteUrl.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Context a site URL can be derived from when nothing is configured. `request`
|
|
7
|
+
* structurally accepts a Fetch `Request`, a `PayloadRequest`, or — in contexts
|
|
8
|
+
* without a request object (e.g. Next metadata routes) — a bare
|
|
9
|
+
* `{ headers: await headers() }`.
|
|
10
|
+
*/
|
|
11
|
+
type SiteUrlContext = {
|
|
12
|
+
/** The incoming request. */
|
|
13
|
+
request?: {
|
|
14
|
+
headers?: Headers;
|
|
15
|
+
url?: null | string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Origin of the incoming request: proxy headers first (they carry the public host
|
|
20
|
+
* when the server sits behind one), then the request URL. Host headers are
|
|
21
|
+
* client-controlled: responses built from them must never be written to a shared
|
|
22
|
+
* cache (entries are cached as site-relative paths for this reason).
|
|
23
|
+
*/
|
|
24
|
+
declare const siteUrlFromRequest: (ctx: SiteUrlContext) => string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Resolves the canonical frontend origin: explicit option → SITE_URL →
|
|
27
|
+
* NEXT_PUBLIC_SERVER_URL → VERCEL_PROJECT_PRODUCTION_URL → the incoming
|
|
28
|
+
* request, when available.
|
|
29
|
+
*/
|
|
30
|
+
declare const resolveSiteUrl: (configured?: ((ctx: SiteUrlContext) => string) | string, ctx?: SiteUrlContext) => string;
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/types.d.ts
|
|
33
|
+
/** A single sitemap URL entry. */
|
|
34
|
+
type SitemapEntry = {
|
|
35
|
+
/** Per-entry change frequency. Ignored by Google; opt-in. */
|
|
36
|
+
changefreq?: ChangeFrequency;
|
|
37
|
+
/** W3C datetime string. Google uses this; keep it accurate. */
|
|
38
|
+
lastmod?: string;
|
|
39
|
+
/** Absolute URL of the page. */
|
|
40
|
+
loc: string;
|
|
41
|
+
/** Per-entry priority (0.0–1.0). Ignored by Google; opt-in. */
|
|
42
|
+
priority?: number;
|
|
43
|
+
};
|
|
44
|
+
type ChangeFrequency = 'always' | 'daily' | 'hourly' | 'monthly' | 'never' | 'weekly' | 'yearly';
|
|
45
|
+
type SitemapPathArgs<TSlug extends CollectionSlug = CollectionSlug> = {
|
|
46
|
+
doc: DataFromCollectionSlug<TSlug>;
|
|
47
|
+
/** Present when generation runs inside a Payload request (REST endpoints). */
|
|
48
|
+
req?: PayloadRequest;
|
|
49
|
+
};
|
|
50
|
+
type SitemapInvalidateArgs<TSlug extends CollectionSlug = CollectionSlug> = {
|
|
51
|
+
doc: DataFromCollectionSlug<TSlug>;
|
|
52
|
+
operation: 'create' | 'update';
|
|
53
|
+
previousDoc?: DataFromCollectionSlug<TSlug>;
|
|
54
|
+
};
|
|
55
|
+
type SitemapCollectionConfig<TSlug extends CollectionSlug = CollectionSlug> = {
|
|
56
|
+
/** Fixed `<changefreq>` for every entry in this collection. Ignored by Google; opt-in. */
|
|
57
|
+
changeFreq?: ChangeFrequency;
|
|
58
|
+
/** Per-collection override of the plugin-level `chunkSize`. */
|
|
59
|
+
chunkSize?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Source for `<lastmod>`: a field name, a function of the doc, or `false` to omit.
|
|
62
|
+
* @default 'updatedAt'
|
|
63
|
+
*/
|
|
64
|
+
lastMod?: ((doc: DataFromCollectionSlug<TSlug>) => Date | null | string | undefined) | false | string;
|
|
65
|
+
/**
|
|
66
|
+
* Return the document's path (`/about/team`) — it is joined onto `siteUrl`.
|
|
67
|
+
* An absolute `http(s)://` return value is used verbatim (multi-domain escape hatch).
|
|
68
|
+
* Return `null`/`undefined` to omit the document from the sitemap.
|
|
69
|
+
*/
|
|
70
|
+
path: (args: SitemapPathArgs<TSlug>) => null | Promise<null | string | undefined> | string | undefined;
|
|
71
|
+
/** Fixed `<priority>` for every entry in this collection. Ignored by Google; opt-in. */
|
|
72
|
+
priority?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Fields fetched for `path` (and `lastMod` when it is a function).
|
|
75
|
+
* `id`, `updatedAt`, and a string `lastMod` field are always included.
|
|
76
|
+
* @default { slug: true }
|
|
77
|
+
*/
|
|
78
|
+
select?: SelectType;
|
|
79
|
+
/**
|
|
80
|
+
* Override the default cache-invalidation heuristic for this collection.
|
|
81
|
+
* Default: invalidate on any change except a draft save with no published transition.
|
|
82
|
+
*/
|
|
83
|
+
shouldInvalidate?: (args: SitemapInvalidateArgs<TSlug>) => boolean;
|
|
84
|
+
/** Extra query constraints AND-ed into the sitemap query. */
|
|
85
|
+
where?: Where;
|
|
86
|
+
};
|
|
87
|
+
/** Loosely-typed view of a collection config used internally. */
|
|
88
|
+
type InternalSitemapCollectionConfig = {
|
|
89
|
+
changeFreq?: ChangeFrequency;
|
|
90
|
+
chunkSize?: number;
|
|
91
|
+
lastMod?: ((doc: JsonObject) => Date | null | string | undefined) | false | string;
|
|
92
|
+
path: (args: {
|
|
93
|
+
doc: JsonObject;
|
|
94
|
+
req?: PayloadRequest;
|
|
95
|
+
}) => null | Promise<null | string | undefined> | string | undefined;
|
|
96
|
+
priority?: number;
|
|
97
|
+
select?: SelectType;
|
|
98
|
+
shouldInvalidate?: (args: {
|
|
99
|
+
doc: JsonObject;
|
|
100
|
+
operation: 'create' | 'update';
|
|
101
|
+
previousDoc?: JsonObject;
|
|
102
|
+
}) => boolean;
|
|
103
|
+
where?: Where;
|
|
104
|
+
};
|
|
105
|
+
/** A non-collection route to include in the sitemap (static pages, home from a global, …). */
|
|
106
|
+
type SitemapRoute = {
|
|
107
|
+
changeFreq?: ChangeFrequency;
|
|
108
|
+
lastMod?: Date | string;
|
|
109
|
+
/** Path (`/search`) joined onto `siteUrl`, or an absolute URL used verbatim. */
|
|
110
|
+
path: string;
|
|
111
|
+
priority?: number;
|
|
112
|
+
};
|
|
113
|
+
type SitemapRoutesFn = (args: {
|
|
114
|
+
payload: Payload;
|
|
115
|
+
req?: PayloadRequest;
|
|
116
|
+
}) => Promise<SitemapRoute[]> | SitemapRoute[];
|
|
117
|
+
type SitemapEndpointAccess = (args: {
|
|
118
|
+
req: PayloadRequest;
|
|
119
|
+
}) => boolean | Promise<boolean>;
|
|
120
|
+
type SitemapEndpointsConfig = {
|
|
121
|
+
/**
|
|
122
|
+
* Access control for the XML endpoints.
|
|
123
|
+
* @default public — sitemaps exist to be crawled once you opt in
|
|
124
|
+
*/
|
|
125
|
+
access?: SitemapEndpointAccess;
|
|
126
|
+
/** `Cache-Control` header for endpoint responses. */
|
|
127
|
+
cacheControl?: string;
|
|
128
|
+
/**
|
|
129
|
+
* Also expose raw entries at `<path>/entries.json` for SSG frontends.
|
|
130
|
+
* @default false — when `true`, access defaults to authenticated users only
|
|
131
|
+
*/
|
|
132
|
+
json?: {
|
|
133
|
+
access?: SitemapEndpointAccess;
|
|
134
|
+
} | boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Public origin used when the index references its chunk files.
|
|
137
|
+
* @default derived from the incoming request URL
|
|
138
|
+
*/
|
|
139
|
+
origin?: string;
|
|
140
|
+
/**
|
|
141
|
+
* Base path under the Payload API route.
|
|
142
|
+
* @default '/sitemap' → `/api/sitemap/index.xml`, `/api/sitemap/:file`
|
|
143
|
+
*/
|
|
144
|
+
path?: string;
|
|
145
|
+
};
|
|
146
|
+
type ResolvedSitemapEndpoints = {
|
|
147
|
+
access?: SitemapEndpointAccess;
|
|
148
|
+
cacheControl: string;
|
|
149
|
+
json: {
|
|
150
|
+
access: SitemapEndpointAccess;
|
|
151
|
+
} | false;
|
|
152
|
+
origin?: string;
|
|
153
|
+
path: string;
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Cache for generated entries, keyed by group (collection slug or the routes group).
|
|
157
|
+
* `wrap` returns cached entries or runs `fn` and stores the result;
|
|
158
|
+
* `invalidate` marks groups dirty so the next request regenerates.
|
|
159
|
+
* Cached `loc` values are site-relative paths (unless `path()` returned an absolute
|
|
160
|
+
* URL) — they are joined onto the resolved `siteUrl` per request, so cached data is
|
|
161
|
+
* never influenced by a request's Host header.
|
|
162
|
+
*/
|
|
163
|
+
interface SitemapCache {
|
|
164
|
+
invalidate: (keys: string[]) => Promise<void> | void;
|
|
165
|
+
wrap: (key: string, fn: () => Promise<SitemapEntry[]>) => Promise<SitemapEntry[]>;
|
|
166
|
+
}
|
|
167
|
+
type RobotsRule = {
|
|
168
|
+
allow?: string | string[];
|
|
169
|
+
crawlDelay?: number;
|
|
170
|
+
disallow?: string | string[];
|
|
171
|
+
userAgent: string | string[];
|
|
172
|
+
};
|
|
173
|
+
type RobotsData = {
|
|
174
|
+
host?: string;
|
|
175
|
+
rules: RobotsRule[];
|
|
176
|
+
sitemaps: string[];
|
|
177
|
+
};
|
|
178
|
+
type RobotsOptions = {
|
|
179
|
+
/** Extra disallow paths appended to the default rule. Ignored when `rules` is set. */
|
|
180
|
+
disallow?: string[];
|
|
181
|
+
/**
|
|
182
|
+
* Force production behavior. Non-production output disallows everything.
|
|
183
|
+
* @default VERCEL_ENV === 'production', falling back to NODE_ENV === 'production'
|
|
184
|
+
*/
|
|
185
|
+
isProduction?: boolean;
|
|
186
|
+
/** Replace the default rules entirely (default: allow all, disallow admin + API routes). */
|
|
187
|
+
rules?: RobotsRule[];
|
|
188
|
+
/** Sitemap URL(s). @default `${siteUrl}/sitemap.xml` */
|
|
189
|
+
sitemaps?: string[];
|
|
190
|
+
/** Final say over the computed output — receives the built data, returns what ships. */
|
|
191
|
+
transform?: (robots: RobotsData) => RobotsData;
|
|
192
|
+
};
|
|
193
|
+
type SitemapCollections = { [K in CollectionSlug]?: SitemapCollectionConfig<K> };
|
|
194
|
+
interface SitemapPluginConfig {
|
|
195
|
+
/** Control the admin fields injected into configured collections. */
|
|
196
|
+
adminFields?: {
|
|
197
|
+
/** Inject an `excludeFromSitemap` sidebar checkbox. @default true */
|
|
198
|
+
exclude?: boolean;
|
|
199
|
+
/**
|
|
200
|
+
* Nest the injected fields inside the group field (or named tab) with this
|
|
201
|
+
* name — e.g. an existing `metadata` group — instead of the collection
|
|
202
|
+
* root. Dot notation reaches nested containers (`'seo.metadata'` = a
|
|
203
|
+
* `metadata` group inside a named `seo` tab or group). Missing segments
|
|
204
|
+
* are created as group fields on each configured collection, so the
|
|
205
|
+
* exclude flag always lives at `<group>.excludeFromSitemap` — changing
|
|
206
|
+
* this on a live project moves the data path (migration required).
|
|
207
|
+
*/
|
|
208
|
+
group?: string;
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* Entry cache strategy. `'auto'` uses Next.js tag-based caching when `next/cache`
|
|
212
|
+
* is importable and falls back to an in-memory cache otherwise.
|
|
213
|
+
* @default 'auto'
|
|
214
|
+
*/
|
|
215
|
+
cache?: 'auto' | 'memory' | 'none' | SitemapCache;
|
|
216
|
+
/** Max URLs per sitemap file (protocol limit is 50,000). @default 25000 */
|
|
217
|
+
chunkSize?: number;
|
|
218
|
+
/** Collections to include, keyed by slug. */
|
|
219
|
+
collections: SitemapCollections;
|
|
220
|
+
/**
|
|
221
|
+
* Disables generation, hooks, and endpoints while keeping injected fields so the
|
|
222
|
+
* database schema stays consistent for migrations.
|
|
223
|
+
* @default false
|
|
224
|
+
*/
|
|
225
|
+
disabled?: boolean;
|
|
226
|
+
/**
|
|
227
|
+
* REST endpoints under the Payload API route.
|
|
228
|
+
* @default false — Next.js route handlers (`@whatworks/payload-sitemap/next`) are
|
|
229
|
+
* the primary delivery; enable these for decoupled frontends or proxy setups.
|
|
230
|
+
*/
|
|
231
|
+
endpoints?: boolean | SitemapEndpointsConfig;
|
|
232
|
+
/** Defaults for `generateRobotsTxt` / `createRobots`. */
|
|
233
|
+
robots?: RobotsOptions;
|
|
234
|
+
/** Extra routes to include (static pages, home page from a global, …). */
|
|
235
|
+
routes?: SitemapRoute[] | SitemapRoutesFn;
|
|
236
|
+
/**
|
|
237
|
+
* Canonical origin of the site frontend, e.g. `https://example.com`, or a
|
|
238
|
+
* function with full control (it receives the incoming request's headers when
|
|
239
|
+
* one is available).
|
|
240
|
+
* @default SITE_URL → NEXT_PUBLIC_SERVER_URL → https://$VERCEL_PROJECT_PRODUCTION_URL
|
|
241
|
+
* → derived from the incoming request's `x-forwarded-proto`/`x-forwarded-host`/`host`
|
|
242
|
+
* headers. Env vars win over headers so deployments reachable via non-canonical
|
|
243
|
+
* aliases still emit the canonical domain.
|
|
244
|
+
*/
|
|
245
|
+
siteUrl?: ((ctx: SiteUrlContext) => string) | string;
|
|
246
|
+
/** Append a trailing slash to generated paths. @default false */
|
|
247
|
+
trailingSlash?: boolean;
|
|
248
|
+
}
|
|
249
|
+
type ResolvedSitemapConfig = {
|
|
250
|
+
cache: SitemapCache;
|
|
251
|
+
chunkSize: number;
|
|
252
|
+
collections: Record<string, InternalSitemapCollectionConfig>;
|
|
253
|
+
endpoints: false | ResolvedSitemapEndpoints;
|
|
254
|
+
/**
|
|
255
|
+
* Query path of the exclude checkbox (`excludeFromSitemap`, prefixed with
|
|
256
|
+
* `adminFields.group` when set), or `undefined` when the field is disabled.
|
|
257
|
+
*/
|
|
258
|
+
excludeFieldPath?: string;
|
|
259
|
+
groups: string[];
|
|
260
|
+
robots: RobotsOptions;
|
|
261
|
+
routes?: SitemapRoute[] | SitemapRoutesFn;
|
|
262
|
+
/**
|
|
263
|
+
* Resolves the site origin. Static sources (option, env) are memoized;
|
|
264
|
+
* otherwise it derives from the request headers passed in `ctx`.
|
|
265
|
+
*/
|
|
266
|
+
siteUrl: (ctx?: SiteUrlContext) => string;
|
|
267
|
+
trailingSlash: boolean;
|
|
268
|
+
};
|
|
269
|
+
//#endregion
|
|
270
|
+
export { SitemapRoute as _, RobotsData as a, resolveSiteUrl as b, SitemapCache as c, SitemapEndpointAccess as d, SitemapEndpointsConfig as f, SitemapPluginConfig as g, SitemapPathArgs as h, ResolvedSitemapEndpoints as i, SitemapCollectionConfig as l, SitemapInvalidateArgs as m, InternalSitemapCollectionConfig as n, RobotsOptions as o, SitemapEntry as p, ResolvedSitemapConfig as r, RobotsRule as s, ChangeFrequency as t, SitemapCollections as u, SitemapRoutesFn as v, siteUrlFromRequest as x, SiteUrlContext as y };
|
|
271
|
+
//# sourceMappingURL=types-j8T8euN5.d.ts.map
|