@djangocfg/nextjs 2.1.543 → 2.1.545

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.
@@ -36,6 +36,15 @@ interface SitemapFeedPage {
36
36
  next_cursor: string | null;
37
37
  entries: SitemapEntry[];
38
38
  }
39
+ /**
40
+ * Which backend sources a frontend claims: a list of names, or a predicate.
41
+ *
42
+ * Omit for every source in the index — right whenever the backend serves
43
+ * this frontend alone. Pass one when several frontends share a backend, so
44
+ * each emits only the URLs its own host can resolve. See `tenantSources`
45
+ * for the `<source>-<tenant>` convention.
46
+ */
47
+ type SourceSelector = string[] | ((name: string) => boolean);
39
48
  interface StaticRoute {
40
49
  path: string;
41
50
  changeFrequency?: ChangeFreq;
@@ -51,6 +60,9 @@ interface CreateDjangoSitemapOptions {
51
60
  /** Frontend-only routes the backend doesn't know about. Auth/account
52
61
  * pages should NOT be listed here. */
53
62
  staticRoutes?: StaticRoute[];
63
+ /** Backend sources to emit. Default: every source the index reports.
64
+ * Keep in step with the same app's `createSitemapIndex`. */
65
+ sources?: SourceSelector;
54
66
  /** Index revalidate window in seconds. Default: 600. */
55
67
  indexRevalidate?: number;
56
68
  /** Feed (chunk) revalidate window in seconds. Default: 3600. */
@@ -66,6 +78,10 @@ interface CreateSitemapIndexOptions {
66
78
  /** Revalidate window in seconds for both the upstream `index/` fetch
67
79
  * and the emitted XML's `s-maxage`. Default: 600. */
68
80
  indexRevalidate?: number;
81
+ /** Backend sources to list. Default: every source the index reports.
82
+ * MUST match the same app's `createDjangoSitemap` — a mismatch
83
+ * advertises chunks the sitemap route will not build. */
84
+ sources?: SourceSelector;
69
85
  /** Include the `static` chunk id at the top of the index. Turn off if
70
86
  * the app's `sitemap.ts` opts out of `staticRoutes`. Default: true. */
71
87
  includeStatic?: boolean;
@@ -220,6 +236,57 @@ declare function decodeChunkId(id: string): {
220
236
  cursor: string | null;
221
237
  };
222
238
 
239
+ /**
240
+ * Which backend sources a frontend claims.
241
+ *
242
+ * `GET /cfg/sitemap/index/` answers for the whole Django project — every
243
+ * source any installed app registered. One frontend is not always entitled
244
+ * to all of them: a backend serving several brands registers a source per
245
+ * brand (`properties-caribbean`, `properties-bali`), and each brand's app
246
+ * must emit only its own, or it advertises the other's URLs under its own
247
+ * host — every one of them a 404, and the sitemap that carries them is
248
+ * discredited whole.
249
+ *
250
+ * The selector lives here, shared, for the same reason `STATIC_ID` does:
251
+ * `createDjangoSitemap` enumerates the chunks and `createSitemapIndex`
252
+ * enumerates them again for the index document. If the two applied
253
+ * different filters, the index would advertise shards the chunk route
254
+ * refuses to build — and that mismatch is invisible until a crawler reads
255
+ * both. One function, called by both, cannot drift.
256
+ *
257
+ * Source NAMES are the contract. The module ships no notion of a brand or
258
+ * a tenant, and it should not: the backend chose what its source names
259
+ * mean, and only the app knows which of them are its own.
260
+ */
261
+
262
+ /**
263
+ * A predicate from whatever the caller passed.
264
+ *
265
+ * Omitted means every source — the single-tenant default, and what every
266
+ * caller before this option existed already relied on.
267
+ */
268
+ declare function sourceFilter(select: SourceSelector | undefined): (name: string) => boolean;
269
+ /** The index's sources, narrowed to those the caller claims. */
270
+ declare function selectSources(index: SitemapIndex, select: SourceSelector | undefined): SitemapSourceInfo[];
271
+ /**
272
+ * A selector for one tenant of a multi-brand backend.
273
+ *
274
+ * Matches the `<source>-<tenant>` names that a backend registering per
275
+ * tenant emits. It splits on the LAST hyphen and compares the tail exactly,
276
+ * rather than testing a suffix: `endsWith('-bali')` is correct for
277
+ * `properties-bali` but the same shape reads `pricing-plans-premium` as
278
+ * belonging to a `premium` tenant, and a substring match is not a contract.
279
+ *
280
+ * ```ts
281
+ * sources: tenantSources('bali') // properties-bali, communities-bali, …
282
+ * ```
283
+ *
284
+ * Sources with no hyphen never match: an untenanted source belongs to the
285
+ * project rather than to a brand, and claiming it for one is the error this
286
+ * function exists to prevent.
287
+ */
288
+ declare function tenantSources(tenant: string): (name: string) => boolean;
289
+
223
290
  /**
224
291
  * Sitemap fetch helpers — never throw.
225
292
  *
@@ -231,4 +298,4 @@ declare function decodeChunkId(id: string): {
231
298
  declare function fetchSitemapIndex(apiUrl: string, revalidate: number): Promise<SitemapIndex>;
232
299
  declare function fetchSitemapFeed(apiUrl: string, source: string, cursor: string | null, revalidate: number): Promise<SitemapFeedPage>;
233
300
 
234
- export { type CreateDjangoSitemapOptions, type CreateRobotsOptions, type CreateSitemapIndexOptions, type SitemapChunkInfo, type SitemapEntry, type SitemapFeedPage, type SitemapIndex, type SitemapSourceInfo, type StaticRoute, createDjangoSitemap, createRobots, createSitemapIndex, decodeChunkId, encodeChunkId, fetchSitemapFeed, fetchSitemapIndex };
301
+ export { type CreateDjangoSitemapOptions, type CreateRobotsOptions, type CreateSitemapIndexOptions, type SitemapChunkInfo, type SitemapEntry, type SitemapFeedPage, type SitemapIndex, type SitemapSourceInfo, type SourceSelector, type StaticRoute, createDjangoSitemap, createRobots, createSitemapIndex, decodeChunkId, encodeChunkId, fetchSitemapFeed, fetchSitemapIndex, selectSources, sourceFilter, tenantSources };
@@ -62,12 +62,40 @@ function decodeChunkId(id) {
62
62
  return { source, cursor: cursorRaw === "" ? null : cursorRaw };
63
63
  }
64
64
 
65
+ // src/sitemap/select.ts
66
+ function sourceFilter(select) {
67
+ if (select === void 0) return () => true;
68
+ if (typeof select === "function") return select;
69
+ const allowed = new Set(select);
70
+ return (name) => allowed.has(name);
71
+ }
72
+ function selectSources(index, select) {
73
+ const keep = sourceFilter(select);
74
+ return index.sources.filter((source) => keep(source.name));
75
+ }
76
+ function selectedChunkIds(index, select, encode) {
77
+ const ids = [];
78
+ for (const source of selectSources(index, select)) {
79
+ for (const chunk of source.chunks) {
80
+ ids.push(encode(source.name, chunk.cursor_to));
81
+ }
82
+ }
83
+ return ids;
84
+ }
85
+ function tenantSources(tenant) {
86
+ return (name) => {
87
+ const sep = name.lastIndexOf("-");
88
+ return sep !== -1 && name.slice(sep + 1) === tenant;
89
+ };
90
+ }
91
+
65
92
  // src/sitemap/sitemap.ts
66
93
  function createDjangoSitemap(opts) {
67
94
  const {
68
95
  host,
69
96
  apiUrl,
70
97
  staticRoutes = [],
98
+ sources,
71
99
  indexRevalidate = DEFAULT_INDEX_REVALIDATE,
72
100
  feedRevalidate = DEFAULT_FEED_REVALIDATE
73
101
  } = opts;
@@ -76,10 +104,8 @@ function createDjangoSitemap(opts) {
76
104
  const ids = [{ id: STATIC_ID }];
77
105
  if (!apiUrl) return ids;
78
106
  const index = await fetchSitemapIndex(apiUrl, indexRevalidate);
79
- for (const s of index.sources) {
80
- for (const c of s.chunks) {
81
- ids.push({ id: encodeChunkId(s.name, c.cursor_to) });
82
- }
107
+ for (const id of selectedChunkIds(index, sources, encodeChunkId)) {
108
+ ids.push({ id });
83
109
  }
84
110
  return ids;
85
111
  },
@@ -90,6 +116,7 @@ function createDjangoSitemap(opts) {
90
116
  }
91
117
  if (!apiUrl) return [];
92
118
  const { source, cursor } = decodeChunkId(id);
119
+ if (!sourceFilter(sources)(source)) return [];
93
120
  const feed = await fetchSitemapFeed(apiUrl, source, cursor, feedRevalidate);
94
121
  return feed.entries.map((e) => ({
95
122
  url: `${host}${e.loc}`,
@@ -111,6 +138,7 @@ function createSitemapIndex(opts) {
111
138
  const {
112
139
  host,
113
140
  apiUrl,
141
+ sources,
114
142
  indexRevalidate = DEFAULT_INDEX_REVALIDATE,
115
143
  includeStatic = true
116
144
  } = opts;
@@ -119,11 +147,7 @@ function createSitemapIndex(opts) {
119
147
  const ids = includeStatic ? [STATIC_ID] : [];
120
148
  if (apiUrl) {
121
149
  const index = await fetchSitemapIndex(apiUrl, indexRevalidate);
122
- for (const s of index.sources) {
123
- for (const c of s.chunks) {
124
- ids.push(encodeChunkId(s.name, c.cursor_to));
125
- }
126
- }
150
+ ids.push(...selectedChunkIds(index, sources, encodeChunkId));
127
151
  }
128
152
  const lastmod = (/* @__PURE__ */ new Date()).toISOString();
129
153
  const body = renderIndex(host, ids, lastmod);
@@ -173,6 +197,9 @@ export {
173
197
  decodeChunkId,
174
198
  encodeChunkId,
175
199
  fetchSitemapFeed,
176
- fetchSitemapIndex
200
+ fetchSitemapIndex,
201
+ selectSources,
202
+ sourceFilter,
203
+ tenantSources
177
204
  };
178
205
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sitemap/constants.ts","../../src/sitemap/fetch.ts","../../src/sitemap/ids.ts","../../src/sitemap/sitemap.ts","../../src/sitemap/index-route.ts","../../src/sitemap/robots.ts"],"sourcesContent":["/**\n * Shared sitemap constants.\n *\n * Pulled out so both the chunk factory (`createDjangoSitemap`) and the\n * index Route Handler (`createSitemapIndex`) agree on the id of the\n * frontend-only static-routes chunk.\n */\n\nexport const STATIC_ID = 'static';\nexport const DEFAULT_INDEX_REVALIDATE = 600;\nexport const DEFAULT_FEED_REVALIDATE = 3600;\n","/**\n * Sitemap fetch helpers — never throw.\n *\n * Failures are converted to empty payloads so a transient Django outage\n * doesn't break `next build`. The next ISR cycle (`revalidate`) recovers\n * automatically.\n */\n\nimport type { SitemapFeedPage, SitemapIndex } from './types';\n\nconst EMPTY_INDEX: SitemapIndex = {\n sources: [],\n generated_at: new Date(0).toISOString(),\n ttl_seconds: 60,\n};\n\nconst emptyFeed = (source: string, cursor: string | null): SitemapFeedPage => ({\n source,\n chunk_id: `${source}-empty`,\n count: 0,\n has_more: false,\n next_cursor: cursor,\n entries: [],\n});\n\nexport async function fetchSitemapIndex(\n apiUrl: string,\n revalidate: number,\n): Promise<SitemapIndex> {\n try {\n const r = await fetch(`${apiUrl}/cfg/sitemap/index/`, {\n next: { revalidate },\n });\n if (!r.ok) {\n console.warn(`[sitemap] index fetch returned ${r.status}; falling back to empty`);\n return EMPTY_INDEX;\n }\n return (await r.json()) as SitemapIndex;\n } catch (err) {\n console.warn('[sitemap] index fetch failed:', err);\n return EMPTY_INDEX;\n }\n}\n\nexport async function fetchSitemapFeed(\n apiUrl: string,\n source: string,\n cursor: string | null,\n revalidate: number,\n): Promise<SitemapFeedPage> {\n const params = new URLSearchParams({ source });\n if (cursor) params.set('cursor', cursor);\n try {\n const r = await fetch(`${apiUrl}/cfg/sitemap/feed/?${params}`, {\n next: { revalidate },\n });\n if (!r.ok) {\n console.warn(`[sitemap] feed ${source} returned ${r.status}; falling back to empty`);\n return emptyFeed(source, cursor);\n }\n return (await r.json()) as SitemapFeedPage;\n } catch (err) {\n console.warn(`[sitemap] feed ${source} fetch failed:`, err);\n return emptyFeed(source, cursor);\n }\n}\n","/**\n * Sitemap chunk id encoding.\n *\n * Each Next.js sitemap file is addressed by an opaque string id. We\n * round-trip a `(source, cursor)` pair through that id so the default\n * sitemap handler knows which Django chunk to fetch.\n *\n * Separator `--` is unambiguous: backend cursors use URL-safe base64 (`-_`).\n */\n\nexport function encodeChunkId(source: string, cursor: string | null): string {\n return `${source}--${cursor ?? ''}`;\n}\n\nexport function decodeChunkId(id: string): { source: string; cursor: string | null } {\n const sep = id.indexOf('--');\n if (sep === -1) return { source: id, cursor: null };\n const source = id.slice(0, sep);\n const cursorRaw = id.slice(sep + 2);\n return { source, cursor: cursorRaw === '' ? null : cursorRaw };\n}\n","/**\n * createDjangoSitemap — factory for Next.js `app/sitemap.ts`.\n *\n * Backed by `django_cfg.modules.django_sitemap`: the backend exposes a\n * paginated JSON feed, this factory turns each chunk into a Next.js\n * sitemap file via `generateSitemaps()`.\n *\n * Usage:\n *\n * ```ts\n * // apps/<app>/app/sitemap.ts\n * import { createDjangoSitemap } from '@djangocfg/nextjs/sitemap';\n *\n * const { generateSitemaps, sitemap } = createDjangoSitemap({\n * host: process.env.NEXT_PUBLIC_SITE_URL!,\n * apiUrl: process.env.NEXT_PUBLIC_API_URL!,\n * staticRoutes: [\n * { path: '/', changeFrequency: 'daily', priority: 1.0 },\n * { path: '/catalog', changeFrequency: 'daily', priority: 0.9 },\n * ],\n * });\n *\n * export { generateSitemaps, sitemap as default };\n * export const revalidate = 3600;\n * ```\n *\n * Without `apiUrl`, the sitemap emits only the static routes — handy for\n * marketing sites that don't have a backend-driven URL space.\n */\n\nimport type { MetadataRoute } from 'next';\n\nimport {\n DEFAULT_FEED_REVALIDATE,\n DEFAULT_INDEX_REVALIDATE,\n STATIC_ID,\n} from './constants';\nimport { fetchSitemapFeed, fetchSitemapIndex } from './fetch';\nimport { decodeChunkId, encodeChunkId } from './ids';\n\nimport type { CreateDjangoSitemapOptions, StaticRoute } from './types';\n\ninterface SitemapApi {\n generateSitemaps: () => Promise<Array<{ id: string }>>;\n sitemap: (props: { id: Promise<string> }) => Promise<MetadataRoute.Sitemap>;\n}\n\nexport function createDjangoSitemap(opts: CreateDjangoSitemapOptions): SitemapApi {\n const {\n host,\n apiUrl,\n staticRoutes = [],\n indexRevalidate = DEFAULT_INDEX_REVALIDATE,\n feedRevalidate = DEFAULT_FEED_REVALIDATE,\n } = opts;\n\n return {\n async generateSitemaps() {\n const ids: Array<{ id: string }> = [{ id: STATIC_ID }];\n if (!apiUrl) return ids;\n const index = await fetchSitemapIndex(apiUrl, indexRevalidate);\n for (const s of index.sources) {\n for (const c of s.chunks) {\n ids.push({ id: encodeChunkId(s.name, c.cursor_to) });\n }\n }\n return ids;\n },\n\n async sitemap({ id: idPromise }) {\n const id = await idPromise;\n\n if (id === STATIC_ID) {\n return renderStatic(host, staticRoutes);\n }\n\n if (!apiUrl) return [];\n\n const { source, cursor } = decodeChunkId(id);\n const feed = await fetchSitemapFeed(apiUrl, source, cursor, feedRevalidate);\n return feed.entries.map((e) => ({\n url: `${host}${e.loc}`,\n lastModified: e.lastmod ? new Date(e.lastmod) : undefined,\n }));\n },\n };\n}\n\nfunction renderStatic(host: string, routes: StaticRoute[]): MetadataRoute.Sitemap {\n return routes.map((r) => ({\n url: `${host}${r.path}`,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n}\n","/**\n * createSitemapIndex — Route Handler factory for the sitemap-index XML.\n *\n * Next.js's `generateSitemaps()` API ships chunks at `/sitemap/<id>.xml`\n * but does NOT emit a single sitemap-index document. Without one,\n * robots.txt + crawlers point at a 404 and never discover the chunks.\n * This factory closes that gap: it returns a `GET` Route Handler that\n * mints a `<sitemapindex>` XML listing every chunk the matching\n * `createDjangoSitemap` would produce.\n *\n * Mount it at **`app/sitemap_index.xml/route.ts`** — NOT `sitemap.xml`.\n * `app/sitemap.ts` (where `createDjangoSitemap` lives) already owns\n * `/sitemap.xml` in Next.js's metadata routing, even when its output is\n * fan-out via `generateSitemaps()` and the singular URL serves 404.\n * Putting a route handler at `app/sitemap.xml/route.ts` throws at build\n * time: *Conflicting route and metadata at /sitemap.xml*.\n *\n * Usage — the full pattern with the matching chunk + robots factories:\n *\n * ```ts\n * // apps/<app>/app/sitemap.ts — chunks\n * import { createDjangoSitemap } from '@djangocfg/nextjs/sitemap';\n * const { generateSitemaps, sitemap } = createDjangoSitemap({ ... });\n * export { generateSitemaps, sitemap as default };\n *\n * // apps/<app>/app/sitemap_index.xml/route.ts — the index\n * import { createSitemapIndex } from '@djangocfg/nextjs/sitemap';\n * export const { GET } = createSitemapIndex({\n * host: process.env.NEXT_PUBLIC_SITE_URL!,\n * apiUrl: process.env.NEXT_PUBLIC_API_URL!,\n * });\n * export const revalidate = 600;\n *\n * // apps/<app>/app/robots.ts — point crawlers at the index URL above\n * import { createRobots } from '@djangocfg/nextjs/sitemap';\n * const host = process.env.NEXT_PUBLIC_SITE_URL!;\n * export default createRobots({\n * host,\n * sitemap: `${host}/sitemap_index.xml`,\n * });\n * ```\n *\n * Keep `host` / `apiUrl` in sync with the same app's `app/sitemap.ts`\n * — the index just enumerates the chunks the other file emits.\n */\n\nimport {\n DEFAULT_INDEX_REVALIDATE,\n STATIC_ID,\n} from './constants';\nimport { fetchSitemapIndex } from './fetch';\nimport { encodeChunkId } from './ids';\n\nimport type { CreateSitemapIndexOptions } from './types';\n\ninterface SitemapIndexRoute {\n GET: () => Promise<Response>;\n}\n\nexport function createSitemapIndex(opts: CreateSitemapIndexOptions): SitemapIndexRoute {\n const {\n host,\n apiUrl,\n indexRevalidate = DEFAULT_INDEX_REVALIDATE,\n includeStatic = true,\n } = opts;\n\n return {\n async GET() {\n const ids: string[] = includeStatic ? [STATIC_ID] : [];\n\n if (apiUrl) {\n const index = await fetchSitemapIndex(apiUrl, indexRevalidate);\n for (const s of index.sources) {\n for (const c of s.chunks) {\n ids.push(encodeChunkId(s.name, c.cursor_to));\n }\n }\n }\n\n const lastmod = new Date().toISOString();\n const body = renderIndex(host, ids, lastmod);\n\n return new Response(body, {\n status: 200,\n headers: {\n 'content-type': 'application/xml; charset=utf-8',\n // Mirror the per-chunk revalidate so a cached CDN edge expires\n // at the same cadence as the index it points at.\n 'cache-control': `public, s-maxage=${indexRevalidate}, stale-while-revalidate=${indexRevalidate}`,\n },\n });\n },\n };\n}\n\nfunction renderIndex(host: string, ids: string[], lastmod: string): string {\n const lines: string[] = [];\n lines.push('<?xml version=\"1.0\" encoding=\"UTF-8\"?>');\n lines.push('<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">');\n for (const id of ids) {\n lines.push(' <sitemap>');\n lines.push(` <loc>${escapeXml(`${host}/sitemap/${id}.xml`)}</loc>`);\n lines.push(` <lastmod>${lastmod}</lastmod>`);\n lines.push(' </sitemap>');\n }\n lines.push('</sitemapindex>');\n return lines.join('\\n');\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n","/**\n * createRobots — factory for Next.js `app/robots.ts`.\n *\n * Emits a single `User-Agent: *` block plus a Sitemap pointer. Disallow\n * defaults cover the common cases (auth, account, API surface) — override\n * per app if needed.\n *\n * The `sitemap` default `${host}/sitemap.xml` only makes sense for the\n * single-file case. When the app uses `createDjangoSitemap`\n * (`generateSitemaps()` → chunked output) **plus** `createSitemapIndex`\n * — the canonical pairing — pass an explicit\n * `sitemap: \\`${host}/sitemap_index.xml\\`` to point crawlers at the\n * index. The default `/sitemap.xml` will serve a 404 in that setup\n * (Next.js reserves it for the metadata route but doesn't emit content\n * there).\n *\n * Usage:\n *\n * ```ts\n * // apps/<app>/app/robots.ts\n * import { createRobots } from '@djangocfg/nextjs/sitemap';\n *\n * const host = process.env.NEXT_PUBLIC_SITE_URL!;\n *\n * export default createRobots({\n * host,\n * disallow: ['/account/', '/auth', '/api/', '/apix/'],\n * sitemap: `${host}/sitemap_index.xml`, // matches createSitemapIndex\n * });\n * ```\n */\n\nimport type { MetadataRoute } from 'next';\n\nimport type { CreateRobotsOptions } from './types';\n\nconst DEFAULT_DISALLOW = ['/account/', '/auth', '/api/'];\n\nexport function createRobots(opts: CreateRobotsOptions): () => MetadataRoute.Robots {\n const { host, disallow = DEFAULT_DISALLOW, sitemap } = opts;\n return () => ({\n rules: [{ userAgent: '*', allow: '/', disallow }],\n sitemap: sitemap ?? `${host}/sitemap.xml`,\n host,\n });\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/sitemap/constants.ts","../../src/sitemap/fetch.ts","../../src/sitemap/ids.ts","../../src/sitemap/select.ts","../../src/sitemap/sitemap.ts","../../src/sitemap/index-route.ts","../../src/sitemap/robots.ts"],"sourcesContent":["/**\n * Shared sitemap constants.\n *\n * Pulled out so both the chunk factory (`createDjangoSitemap`) and the\n * index Route Handler (`createSitemapIndex`) agree on the id of the\n * frontend-only static-routes chunk.\n */\n\nexport const STATIC_ID = 'static';\nexport const DEFAULT_INDEX_REVALIDATE = 600;\nexport const DEFAULT_FEED_REVALIDATE = 3600;\n","/**\n * Sitemap fetch helpers — never throw.\n *\n * Failures are converted to empty payloads so a transient Django outage\n * doesn't break `next build`. The next ISR cycle (`revalidate`) recovers\n * automatically.\n */\n\nimport type { SitemapFeedPage, SitemapIndex } from './types';\n\nconst EMPTY_INDEX: SitemapIndex = {\n sources: [],\n generated_at: new Date(0).toISOString(),\n ttl_seconds: 60,\n};\n\nconst emptyFeed = (source: string, cursor: string | null): SitemapFeedPage => ({\n source,\n chunk_id: `${source}-empty`,\n count: 0,\n has_more: false,\n next_cursor: cursor,\n entries: [],\n});\n\nexport async function fetchSitemapIndex(\n apiUrl: string,\n revalidate: number,\n): Promise<SitemapIndex> {\n try {\n const r = await fetch(`${apiUrl}/cfg/sitemap/index/`, {\n next: { revalidate },\n });\n if (!r.ok) {\n console.warn(`[sitemap] index fetch returned ${r.status}; falling back to empty`);\n return EMPTY_INDEX;\n }\n return (await r.json()) as SitemapIndex;\n } catch (err) {\n console.warn('[sitemap] index fetch failed:', err);\n return EMPTY_INDEX;\n }\n}\n\nexport async function fetchSitemapFeed(\n apiUrl: string,\n source: string,\n cursor: string | null,\n revalidate: number,\n): Promise<SitemapFeedPage> {\n const params = new URLSearchParams({ source });\n if (cursor) params.set('cursor', cursor);\n try {\n const r = await fetch(`${apiUrl}/cfg/sitemap/feed/?${params}`, {\n next: { revalidate },\n });\n if (!r.ok) {\n console.warn(`[sitemap] feed ${source} returned ${r.status}; falling back to empty`);\n return emptyFeed(source, cursor);\n }\n return (await r.json()) as SitemapFeedPage;\n } catch (err) {\n console.warn(`[sitemap] feed ${source} fetch failed:`, err);\n return emptyFeed(source, cursor);\n }\n}\n","/**\n * Sitemap chunk id encoding.\n *\n * Each Next.js sitemap file is addressed by an opaque string id. We\n * round-trip a `(source, cursor)` pair through that id so the default\n * sitemap handler knows which Django chunk to fetch.\n *\n * Separator `--` is unambiguous: backend cursors use URL-safe base64 (`-_`).\n */\n\nexport function encodeChunkId(source: string, cursor: string | null): string {\n return `${source}--${cursor ?? ''}`;\n}\n\nexport function decodeChunkId(id: string): { source: string; cursor: string | null } {\n const sep = id.indexOf('--');\n if (sep === -1) return { source: id, cursor: null };\n const source = id.slice(0, sep);\n const cursorRaw = id.slice(sep + 2);\n return { source, cursor: cursorRaw === '' ? null : cursorRaw };\n}\n","/**\n * Which backend sources a frontend claims.\n *\n * `GET /cfg/sitemap/index/` answers for the whole Django project — every\n * source any installed app registered. One frontend is not always entitled\n * to all of them: a backend serving several brands registers a source per\n * brand (`properties-caribbean`, `properties-bali`), and each brand's app\n * must emit only its own, or it advertises the other's URLs under its own\n * host — every one of them a 404, and the sitemap that carries them is\n * discredited whole.\n *\n * The selector lives here, shared, for the same reason `STATIC_ID` does:\n * `createDjangoSitemap` enumerates the chunks and `createSitemapIndex`\n * enumerates them again for the index document. If the two applied\n * different filters, the index would advertise shards the chunk route\n * refuses to build — and that mismatch is invisible until a crawler reads\n * both. One function, called by both, cannot drift.\n *\n * Source NAMES are the contract. The module ships no notion of a brand or\n * a tenant, and it should not: the backend chose what its source names\n * mean, and only the app knows which of them are its own.\n */\n\nimport type { SitemapIndex, SitemapSourceInfo, SourceSelector } from './types';\n\n/**\n * A predicate from whatever the caller passed.\n *\n * Omitted means every source — the single-tenant default, and what every\n * caller before this option existed already relied on.\n */\nexport function sourceFilter(select: SourceSelector | undefined): (name: string) => boolean {\n if (select === undefined) return () => true;\n if (typeof select === 'function') return select;\n const allowed = new Set(select);\n return (name) => allowed.has(name);\n}\n\n/** The index's sources, narrowed to those the caller claims. */\nexport function selectSources(\n index: SitemapIndex,\n select: SourceSelector | undefined,\n): SitemapSourceInfo[] {\n const keep = sourceFilter(select);\n return index.sources.filter((source) => keep(source.name));\n}\n\n/**\n * Every chunk id the selected sources produce, in index order.\n *\n * The one place `(source, chunk)` becomes an id, so the chunk route and the\n * index document enumerate the same set by construction.\n */\nexport function selectedChunkIds(\n index: SitemapIndex,\n select: SourceSelector | undefined,\n encode: (source: string, cursor: string | null) => string,\n): string[] {\n const ids: string[] = [];\n for (const source of selectSources(index, select)) {\n for (const chunk of source.chunks) {\n ids.push(encode(source.name, chunk.cursor_to));\n }\n }\n return ids;\n}\n\n/**\n * A selector for one tenant of a multi-brand backend.\n *\n * Matches the `<source>-<tenant>` names that a backend registering per\n * tenant emits. It splits on the LAST hyphen and compares the tail exactly,\n * rather than testing a suffix: `endsWith('-bali')` is correct for\n * `properties-bali` but the same shape reads `pricing-plans-premium` as\n * belonging to a `premium` tenant, and a substring match is not a contract.\n *\n * ```ts\n * sources: tenantSources('bali') // properties-bali, communities-bali, …\n * ```\n *\n * Sources with no hyphen never match: an untenanted source belongs to the\n * project rather than to a brand, and claiming it for one is the error this\n * function exists to prevent.\n */\nexport function tenantSources(tenant: string): (name: string) => boolean {\n return (name) => {\n const sep = name.lastIndexOf('-');\n return sep !== -1 && name.slice(sep + 1) === tenant;\n };\n}\n","/**\n * createDjangoSitemap — factory for Next.js `app/sitemap.ts`.\n *\n * Backed by `django_cfg.modules.django_sitemap`: the backend exposes a\n * paginated JSON feed, this factory turns each chunk into a Next.js\n * sitemap file via `generateSitemaps()`.\n *\n * Usage:\n *\n * ```ts\n * // apps/<app>/app/sitemap.ts\n * import { createDjangoSitemap } from '@djangocfg/nextjs/sitemap';\n *\n * const { generateSitemaps, sitemap } = createDjangoSitemap({\n * host: process.env.NEXT_PUBLIC_SITE_URL!,\n * apiUrl: process.env.NEXT_PUBLIC_API_URL!,\n * staticRoutes: [\n * { path: '/', changeFrequency: 'daily', priority: 1.0 },\n * { path: '/catalog', changeFrequency: 'daily', priority: 0.9 },\n * ],\n * });\n *\n * export { generateSitemaps, sitemap as default };\n * export const revalidate = 3600;\n * ```\n *\n * Without `apiUrl`, the sitemap emits only the static routes — handy for\n * marketing sites that don't have a backend-driven URL space.\n */\n\nimport type { MetadataRoute } from 'next';\n\nimport {\n DEFAULT_FEED_REVALIDATE,\n DEFAULT_INDEX_REVALIDATE,\n STATIC_ID,\n} from './constants';\nimport { fetchSitemapFeed, fetchSitemapIndex } from './fetch';\nimport { decodeChunkId, encodeChunkId } from './ids';\nimport { selectedChunkIds, sourceFilter } from './select';\n\nimport type { CreateDjangoSitemapOptions, StaticRoute } from './types';\n\ninterface SitemapApi {\n generateSitemaps: () => Promise<Array<{ id: string }>>;\n sitemap: (props: { id: Promise<string> }) => Promise<MetadataRoute.Sitemap>;\n}\n\nexport function createDjangoSitemap(opts: CreateDjangoSitemapOptions): SitemapApi {\n const {\n host,\n apiUrl,\n staticRoutes = [],\n sources,\n indexRevalidate = DEFAULT_INDEX_REVALIDATE,\n feedRevalidate = DEFAULT_FEED_REVALIDATE,\n } = opts;\n\n return {\n async generateSitemaps() {\n const ids: Array<{ id: string }> = [{ id: STATIC_ID }];\n if (!apiUrl) return ids;\n const index = await fetchSitemapIndex(apiUrl, indexRevalidate);\n for (const id of selectedChunkIds(index, sources, encodeChunkId)) {\n ids.push({ id });\n }\n return ids;\n },\n\n async sitemap({ id: idPromise }) {\n const id = await idPromise;\n\n if (id === STATIC_ID) {\n return renderStatic(host, staticRoutes);\n }\n\n if (!apiUrl) return [];\n\n const { source, cursor } = decodeChunkId(id);\n // A chunk this app does not claim renders empty rather than fetching.\n // `generateSitemaps` never mints such an id, but the route stays\n // reachable by hand, and serving another tenant's URLs from a guessed\n // address would undo the filter above.\n if (!sourceFilter(sources)(source)) return [];\n\n const feed = await fetchSitemapFeed(apiUrl, source, cursor, feedRevalidate);\n return feed.entries.map((e) => ({\n url: `${host}${e.loc}`,\n lastModified: e.lastmod ? new Date(e.lastmod) : undefined,\n }));\n },\n };\n}\n\nfunction renderStatic(host: string, routes: StaticRoute[]): MetadataRoute.Sitemap {\n return routes.map((r) => ({\n url: `${host}${r.path}`,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n}\n","/**\n * createSitemapIndex — Route Handler factory for the sitemap-index XML.\n *\n * Next.js's `generateSitemaps()` API ships chunks at `/sitemap/<id>.xml`\n * but does NOT emit a single sitemap-index document. Without one,\n * robots.txt + crawlers point at a 404 and never discover the chunks.\n * This factory closes that gap: it returns a `GET` Route Handler that\n * mints a `<sitemapindex>` XML listing every chunk the matching\n * `createDjangoSitemap` would produce.\n *\n * Mount it at **`app/sitemap_index.xml/route.ts`** — NOT `sitemap.xml`.\n * `app/sitemap.ts` (where `createDjangoSitemap` lives) already owns\n * `/sitemap.xml` in Next.js's metadata routing, even when its output is\n * fan-out via `generateSitemaps()` and the singular URL serves 404.\n * Putting a route handler at `app/sitemap.xml/route.ts` throws at build\n * time: *Conflicting route and metadata at /sitemap.xml*.\n *\n * Usage — the full pattern with the matching chunk + robots factories:\n *\n * ```ts\n * // apps/<app>/app/sitemap.ts — chunks\n * import { createDjangoSitemap } from '@djangocfg/nextjs/sitemap';\n * const { generateSitemaps, sitemap } = createDjangoSitemap({ ... });\n * export { generateSitemaps, sitemap as default };\n *\n * // apps/<app>/app/sitemap_index.xml/route.ts — the index\n * import { createSitemapIndex } from '@djangocfg/nextjs/sitemap';\n * export const { GET } = createSitemapIndex({\n * host: process.env.NEXT_PUBLIC_SITE_URL!,\n * apiUrl: process.env.NEXT_PUBLIC_API_URL!,\n * });\n * export const revalidate = 600;\n *\n * // apps/<app>/app/robots.ts — point crawlers at the index URL above\n * import { createRobots } from '@djangocfg/nextjs/sitemap';\n * const host = process.env.NEXT_PUBLIC_SITE_URL!;\n * export default createRobots({\n * host,\n * sitemap: `${host}/sitemap_index.xml`,\n * });\n * ```\n *\n * Keep `host` / `apiUrl` in sync with the same app's `app/sitemap.ts`\n * — the index just enumerates the chunks the other file emits.\n */\n\nimport {\n DEFAULT_INDEX_REVALIDATE,\n STATIC_ID,\n} from './constants';\nimport { fetchSitemapIndex } from './fetch';\nimport { encodeChunkId } from './ids';\nimport { selectedChunkIds } from './select';\n\nimport type { CreateSitemapIndexOptions } from './types';\n\ninterface SitemapIndexRoute {\n GET: () => Promise<Response>;\n}\n\nexport function createSitemapIndex(opts: CreateSitemapIndexOptions): SitemapIndexRoute {\n const {\n host,\n apiUrl,\n sources,\n indexRevalidate = DEFAULT_INDEX_REVALIDATE,\n includeStatic = true,\n } = opts;\n\n return {\n async GET() {\n const ids: string[] = includeStatic ? [STATIC_ID] : [];\n\n if (apiUrl) {\n const index = await fetchSitemapIndex(apiUrl, indexRevalidate);\n ids.push(...selectedChunkIds(index, sources, encodeChunkId));\n }\n\n const lastmod = new Date().toISOString();\n const body = renderIndex(host, ids, lastmod);\n\n return new Response(body, {\n status: 200,\n headers: {\n 'content-type': 'application/xml; charset=utf-8',\n // Mirror the per-chunk revalidate so a cached CDN edge expires\n // at the same cadence as the index it points at.\n 'cache-control': `public, s-maxage=${indexRevalidate}, stale-while-revalidate=${indexRevalidate}`,\n },\n });\n },\n };\n}\n\nfunction renderIndex(host: string, ids: string[], lastmod: string): string {\n const lines: string[] = [];\n lines.push('<?xml version=\"1.0\" encoding=\"UTF-8\"?>');\n lines.push('<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">');\n for (const id of ids) {\n lines.push(' <sitemap>');\n lines.push(` <loc>${escapeXml(`${host}/sitemap/${id}.xml`)}</loc>`);\n lines.push(` <lastmod>${lastmod}</lastmod>`);\n lines.push(' </sitemap>');\n }\n lines.push('</sitemapindex>');\n return lines.join('\\n');\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n","/**\n * createRobots — factory for Next.js `app/robots.ts`.\n *\n * Emits a single `User-Agent: *` block plus a Sitemap pointer. Disallow\n * defaults cover the common cases (auth, account, API surface) — override\n * per app if needed.\n *\n * The `sitemap` default `${host}/sitemap.xml` only makes sense for the\n * single-file case. When the app uses `createDjangoSitemap`\n * (`generateSitemaps()` → chunked output) **plus** `createSitemapIndex`\n * — the canonical pairing — pass an explicit\n * `sitemap: \\`${host}/sitemap_index.xml\\`` to point crawlers at the\n * index. The default `/sitemap.xml` will serve a 404 in that setup\n * (Next.js reserves it for the metadata route but doesn't emit content\n * there).\n *\n * Usage:\n *\n * ```ts\n * // apps/<app>/app/robots.ts\n * import { createRobots } from '@djangocfg/nextjs/sitemap';\n *\n * const host = process.env.NEXT_PUBLIC_SITE_URL!;\n *\n * export default createRobots({\n * host,\n * disallow: ['/account/', '/auth', '/api/', '/apix/'],\n * sitemap: `${host}/sitemap_index.xml`, // matches createSitemapIndex\n * });\n * ```\n */\n\nimport type { MetadataRoute } from 'next';\n\nimport type { CreateRobotsOptions } from './types';\n\nconst DEFAULT_DISALLOW = ['/account/', '/auth', '/api/'];\n\nexport function createRobots(opts: CreateRobotsOptions): () => MetadataRoute.Robots {\n const { host, disallow = DEFAULT_DISALLOW, sitemap } = opts;\n return () => ({\n rules: [{ userAgent: '*', allow: '/', disallow }],\n sitemap: sitemap ?? `${host}/sitemap.xml`,\n host,\n });\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/nextjs",
3
- "version": "2.1.543",
3
+ "version": "2.1.545",
4
4
  "description": "Next.js server utilities: sitemap, health, OG images, contact forms, navigation, config",
5
5
  "keywords": [
6
6
  "nextjs",
@@ -149,9 +149,9 @@
149
149
  "ai-docs": "tsx src/ai/cli.ts"
150
150
  },
151
151
  "peerDependencies": {
152
- "@djangocfg/devtools": "^2.1.543",
153
- "@djangocfg/i18n": "^2.1.543",
154
- "@djangocfg/ui-core": "^2.1.543",
152
+ "@djangocfg/devtools": "^2.1.545",
153
+ "@djangocfg/i18n": "^2.1.545",
154
+ "@djangocfg/ui-core": "^2.1.545",
155
155
  "next": "^16.2.2"
156
156
  },
157
157
  "peerDependenciesMeta": {
@@ -170,11 +170,11 @@
170
170
  "semver": "^7.8.5"
171
171
  },
172
172
  "devDependencies": {
173
- "@djangocfg/devtools": "^2.1.543",
174
- "@djangocfg/i18n": "^2.1.543",
175
- "@djangocfg/layouts": "^2.1.543",
176
- "@djangocfg/typescript-config": "^2.1.543",
177
- "@djangocfg/ui-core": "^2.1.543",
173
+ "@djangocfg/devtools": "^2.1.545",
174
+ "@djangocfg/i18n": "^2.1.545",
175
+ "@djangocfg/layouts": "^2.1.545",
176
+ "@djangocfg/typescript-config": "^2.1.545",
177
+ "@djangocfg/ui-core": "^2.1.545",
178
178
  "@types/node": "^25.9.5",
179
179
  "@types/react": "19.2.15",
180
180
  "@types/react-dom": "19.2.3",
@@ -258,6 +258,7 @@ expects: named `generateSitemaps` + default-exported `sitemap`.
258
258
  | `host` | `string` | (required) | Canonical host (e.g. `https://example.com`). URLs are joined as `${host}${loc}`. |
259
259
  | `apiUrl` | `string` | `undefined` | Django backend base URL. Omit to skip the backend feed entirely. |
260
260
  | `staticRoutes` | `StaticRoute[]` | `[]` | Frontend-only routes. Auth/account routes should NOT be listed here. |
261
+ | `sources` | `string[] \| (name) => boolean` | every source | Which backend sources this frontend claims. See [Several frontends, one backend](#several-frontends-one-backend). |
261
262
  | `indexRevalidate` | `number` | `600` | Seconds — `next: { revalidate }` on the index fetch. |
262
263
  | `feedRevalidate` | `number` | `3600` | Seconds — `next: { revalidate }` on each chunk fetch. |
263
264
 
@@ -284,6 +285,55 @@ it from `app/robots.ts`.
284
285
  | `disallow` | `string[]` | `['/account/', '/auth', '/api/']` | Path prefixes to block. |
285
286
  | `sitemap` | `string` | `${host}/sitemap.xml` | Override the sitemap pointer. |
286
287
 
288
+ ## Several frontends, one backend
289
+
290
+ `GET /cfg/sitemap/index/` answers for the whole Django project — every
291
+ source any installed app registered. When one backend serves several
292
+ brands, register a source per brand and give each frontend its own:
293
+
294
+ ```python
295
+ # apps/catalog/sitemap_sources.py
296
+ for portal in ("caribbean", "bali"):
297
+ register(SitemapSource(name=f"properties-{portal}", ...))
298
+ ```
299
+
300
+ ```ts
301
+ // apps/web-bali/app/sitemap.ts
302
+ import { createDjangoSitemap, tenantSources } from '@djangocfg/nextjs/sitemap';
303
+
304
+ const { generateSitemaps, sitemap } = createDjangoSitemap({
305
+ host, apiUrl,
306
+ sources: tenantSources('bali'), // properties-bali, communities-bali, …
307
+ });
308
+ ```
309
+
310
+ ```ts
311
+ // apps/web-bali/app/sitemap_index.xml/route.ts — the SAME selector
312
+ export const { GET } = createSitemapIndex({
313
+ host, apiUrl,
314
+ sources: tenantSources('bali'),
315
+ });
316
+ ```
317
+
318
+ Pass the same selector to both. The index lists chunks, `sitemap.ts`
319
+ builds them; a mismatch advertises shards that render empty, and nothing
320
+ fails loudly when it happens.
321
+
322
+ **Why the frontend filters and not the backend.** The index view is
323
+ cached under a key with no tenant dimension, so a `?tenant=` parameter
324
+ would pass every cold-cache test and then serve one brand's chunk list
325
+ to the other an hour later. Source names carry the distinction instead:
326
+ an unknown source 404s, which is a loud failure rather than a silent
327
+ wrong one.
328
+
329
+ `tenantSources` splits the source name on its LAST hyphen and compares
330
+ the tail exactly. Prefer it over `endsWith('-bali')`, which also matches
331
+ a `pricing-plans-premium` for a tenant named `premium`. Names without a
332
+ hyphen never match — an untenanted source belongs to the project, not to
333
+ a brand.
334
+
335
+ Omit `sources` entirely when the backend serves this frontend alone.
336
+
287
337
  ### Lower-level utilities
288
338
 
289
339
  Exposed for advanced use cases (custom Next route handlers, debugging):
@@ -294,6 +344,9 @@ import {
294
344
  fetchSitemapFeed,
295
345
  encodeChunkId,
296
346
  decodeChunkId,
347
+ selectSources,
348
+ sourceFilter,
349
+ tenantSources,
297
350
  } from '@djangocfg/nextjs/sitemap';
298
351
  ```
299
352
 
@@ -50,6 +50,7 @@ import {
50
50
  } from './constants';
51
51
  import { fetchSitemapIndex } from './fetch';
52
52
  import { encodeChunkId } from './ids';
53
+ import { selectedChunkIds } from './select';
53
54
 
54
55
  import type { CreateSitemapIndexOptions } from './types';
55
56
 
@@ -61,6 +62,7 @@ export function createSitemapIndex(opts: CreateSitemapIndexOptions): SitemapInde
61
62
  const {
62
63
  host,
63
64
  apiUrl,
65
+ sources,
64
66
  indexRevalidate = DEFAULT_INDEX_REVALIDATE,
65
67
  includeStatic = true,
66
68
  } = opts;
@@ -71,11 +73,7 @@ export function createSitemapIndex(opts: CreateSitemapIndexOptions): SitemapInde
71
73
 
72
74
  if (apiUrl) {
73
75
  const index = await fetchSitemapIndex(apiUrl, indexRevalidate);
74
- for (const s of index.sources) {
75
- for (const c of s.chunks) {
76
- ids.push(encodeChunkId(s.name, c.cursor_to));
77
- }
78
- }
76
+ ids.push(...selectedChunkIds(index, sources, encodeChunkId));
79
77
  }
80
78
 
81
79
  const lastmod = new Date().toISOString();
@@ -13,6 +13,7 @@ export { createDjangoSitemap } from './sitemap';
13
13
  export { createSitemapIndex } from './index-route';
14
14
  export { createRobots } from './robots';
15
15
  export { encodeChunkId, decodeChunkId } from './ids';
16
+ export { selectSources, sourceFilter, tenantSources } from './select';
16
17
  export { fetchSitemapIndex, fetchSitemapFeed } from './fetch';
17
18
 
18
19
  export type {
@@ -24,5 +25,6 @@ export type {
24
25
  CreateDjangoSitemapOptions,
25
26
  CreateSitemapIndexOptions,
26
27
  CreateRobotsOptions,
28
+ SourceSelector,
27
29
  StaticRoute,
28
30
  } from './types';
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Which backend sources a frontend claims.
3
+ *
4
+ * `GET /cfg/sitemap/index/` answers for the whole Django project — every
5
+ * source any installed app registered. One frontend is not always entitled
6
+ * to all of them: a backend serving several brands registers a source per
7
+ * brand (`properties-caribbean`, `properties-bali`), and each brand's app
8
+ * must emit only its own, or it advertises the other's URLs under its own
9
+ * host — every one of them a 404, and the sitemap that carries them is
10
+ * discredited whole.
11
+ *
12
+ * The selector lives here, shared, for the same reason `STATIC_ID` does:
13
+ * `createDjangoSitemap` enumerates the chunks and `createSitemapIndex`
14
+ * enumerates them again for the index document. If the two applied
15
+ * different filters, the index would advertise shards the chunk route
16
+ * refuses to build — and that mismatch is invisible until a crawler reads
17
+ * both. One function, called by both, cannot drift.
18
+ *
19
+ * Source NAMES are the contract. The module ships no notion of a brand or
20
+ * a tenant, and it should not: the backend chose what its source names
21
+ * mean, and only the app knows which of them are its own.
22
+ */
23
+
24
+ import type { SitemapIndex, SitemapSourceInfo, SourceSelector } from './types';
25
+
26
+ /**
27
+ * A predicate from whatever the caller passed.
28
+ *
29
+ * Omitted means every source — the single-tenant default, and what every
30
+ * caller before this option existed already relied on.
31
+ */
32
+ export function sourceFilter(select: SourceSelector | undefined): (name: string) => boolean {
33
+ if (select === undefined) return () => true;
34
+ if (typeof select === 'function') return select;
35
+ const allowed = new Set(select);
36
+ return (name) => allowed.has(name);
37
+ }
38
+
39
+ /** The index's sources, narrowed to those the caller claims. */
40
+ export function selectSources(
41
+ index: SitemapIndex,
42
+ select: SourceSelector | undefined,
43
+ ): SitemapSourceInfo[] {
44
+ const keep = sourceFilter(select);
45
+ return index.sources.filter((source) => keep(source.name));
46
+ }
47
+
48
+ /**
49
+ * Every chunk id the selected sources produce, in index order.
50
+ *
51
+ * The one place `(source, chunk)` becomes an id, so the chunk route and the
52
+ * index document enumerate the same set by construction.
53
+ */
54
+ export function selectedChunkIds(
55
+ index: SitemapIndex,
56
+ select: SourceSelector | undefined,
57
+ encode: (source: string, cursor: string | null) => string,
58
+ ): string[] {
59
+ const ids: string[] = [];
60
+ for (const source of selectSources(index, select)) {
61
+ for (const chunk of source.chunks) {
62
+ ids.push(encode(source.name, chunk.cursor_to));
63
+ }
64
+ }
65
+ return ids;
66
+ }
67
+
68
+ /**
69
+ * A selector for one tenant of a multi-brand backend.
70
+ *
71
+ * Matches the `<source>-<tenant>` names that a backend registering per
72
+ * tenant emits. It splits on the LAST hyphen and compares the tail exactly,
73
+ * rather than testing a suffix: `endsWith('-bali')` is correct for
74
+ * `properties-bali` but the same shape reads `pricing-plans-premium` as
75
+ * belonging to a `premium` tenant, and a substring match is not a contract.
76
+ *
77
+ * ```ts
78
+ * sources: tenantSources('bali') // properties-bali, communities-bali, …
79
+ * ```
80
+ *
81
+ * Sources with no hyphen never match: an untenanted source belongs to the
82
+ * project rather than to a brand, and claiming it for one is the error this
83
+ * function exists to prevent.
84
+ */
85
+ export function tenantSources(tenant: string): (name: string) => boolean {
86
+ return (name) => {
87
+ const sep = name.lastIndexOf('-');
88
+ return sep !== -1 && name.slice(sep + 1) === tenant;
89
+ };
90
+ }
@@ -37,6 +37,7 @@ import {
37
37
  } from './constants';
38
38
  import { fetchSitemapFeed, fetchSitemapIndex } from './fetch';
39
39
  import { decodeChunkId, encodeChunkId } from './ids';
40
+ import { selectedChunkIds, sourceFilter } from './select';
40
41
 
41
42
  import type { CreateDjangoSitemapOptions, StaticRoute } from './types';
42
43
 
@@ -50,6 +51,7 @@ export function createDjangoSitemap(opts: CreateDjangoSitemapOptions): SitemapAp
50
51
  host,
51
52
  apiUrl,
52
53
  staticRoutes = [],
54
+ sources,
53
55
  indexRevalidate = DEFAULT_INDEX_REVALIDATE,
54
56
  feedRevalidate = DEFAULT_FEED_REVALIDATE,
55
57
  } = opts;
@@ -59,10 +61,8 @@ export function createDjangoSitemap(opts: CreateDjangoSitemapOptions): SitemapAp
59
61
  const ids: Array<{ id: string }> = [{ id: STATIC_ID }];
60
62
  if (!apiUrl) return ids;
61
63
  const index = await fetchSitemapIndex(apiUrl, indexRevalidate);
62
- for (const s of index.sources) {
63
- for (const c of s.chunks) {
64
- ids.push({ id: encodeChunkId(s.name, c.cursor_to) });
65
- }
64
+ for (const id of selectedChunkIds(index, sources, encodeChunkId)) {
65
+ ids.push({ id });
66
66
  }
67
67
  return ids;
68
68
  },
@@ -77,6 +77,12 @@ export function createDjangoSitemap(opts: CreateDjangoSitemapOptions): SitemapAp
77
77
  if (!apiUrl) return [];
78
78
 
79
79
  const { source, cursor } = decodeChunkId(id);
80
+ // A chunk this app does not claim renders empty rather than fetching.
81
+ // `generateSitemaps` never mints such an id, but the route stays
82
+ // reachable by hand, and serving another tenant's URLs from a guessed
83
+ // address would undo the filter above.
84
+ if (!sourceFilter(sources)(source)) return [];
85
+
80
86
  const feed = await fetchSitemapFeed(apiUrl, source, cursor, feedRevalidate);
81
87
  return feed.entries.map((e) => ({
82
88
  url: `${host}${e.loc}`,
@@ -44,6 +44,16 @@ export interface SitemapFeedPage {
44
44
 
45
45
  // ── Public option types ─────────────────────────────────────────────────
46
46
 
47
+ /**
48
+ * Which backend sources a frontend claims: a list of names, or a predicate.
49
+ *
50
+ * Omit for every source in the index — right whenever the backend serves
51
+ * this frontend alone. Pass one when several frontends share a backend, so
52
+ * each emits only the URLs its own host can resolve. See `tenantSources`
53
+ * for the `<source>-<tenant>` convention.
54
+ */
55
+ export type SourceSelector = string[] | ((name: string) => boolean);
56
+
47
57
  export interface StaticRoute {
48
58
  path: string;
49
59
  changeFrequency?: ChangeFreq;
@@ -63,6 +73,10 @@ export interface CreateDjangoSitemapOptions {
63
73
  * pages should NOT be listed here. */
64
74
  staticRoutes?: StaticRoute[];
65
75
 
76
+ /** Backend sources to emit. Default: every source the index reports.
77
+ * Keep in step with the same app's `createSitemapIndex`. */
78
+ sources?: SourceSelector;
79
+
66
80
  /** Index revalidate window in seconds. Default: 600. */
67
81
  indexRevalidate?: number;
68
82
 
@@ -83,6 +97,11 @@ export interface CreateSitemapIndexOptions {
83
97
  * and the emitted XML's `s-maxage`. Default: 600. */
84
98
  indexRevalidate?: number;
85
99
 
100
+ /** Backend sources to list. Default: every source the index reports.
101
+ * MUST match the same app's `createDjangoSitemap` — a mismatch
102
+ * advertises chunks the sitemap route will not build. */
103
+ sources?: SourceSelector;
104
+
86
105
  /** Include the `static` chunk id at the top of the index. Turn off if
87
106
  * the app's `sitemap.ts` opts out of `staticRoutes`. Default: true. */
88
107
  includeStatic?: boolean;