@djangocfg/nextjs 2.1.413 → 2.1.416

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.
@@ -56,6 +56,20 @@ interface CreateDjangoSitemapOptions {
56
56
  /** Feed (chunk) revalidate window in seconds. Default: 3600. */
57
57
  feedRevalidate?: number;
58
58
  }
59
+ interface CreateSitemapIndexOptions {
60
+ /** Canonical site host, e.g. `https://vamcar.com`. Chunk URLs in the
61
+ * index are emitted as `${host}/sitemap/<id>.xml`. */
62
+ host: string;
63
+ /** Django backend base URL. When omitted, the index lists only the
64
+ * static chunk (mirrors `createDjangoSitemap` without `apiUrl`). */
65
+ apiUrl?: string;
66
+ /** Revalidate window in seconds for both the upstream `index/` fetch
67
+ * and the emitted XML's `s-maxage`. Default: 600. */
68
+ indexRevalidate?: number;
69
+ /** Include the `static` chunk id at the top of the index. Turn off if
70
+ * the app's `sitemap.ts` opts out of `staticRoutes`. Default: true. */
71
+ includeStatic?: boolean;
72
+ }
59
73
  interface CreateRobotsOptions {
60
74
  host: string;
61
75
  /** Disallow patterns. Default: ['/account/', '/auth', '/api/']. */
@@ -104,6 +118,39 @@ interface SitemapApi {
104
118
  }
105
119
  declare function createDjangoSitemap(opts: CreateDjangoSitemapOptions): SitemapApi;
106
120
 
121
+ /**
122
+ * createSitemapIndex — Route Handler factory for `app/sitemap.xml/route.ts`.
123
+ *
124
+ * Next.js's `generateSitemaps()` API ships chunks at `/sitemap/<id>.xml`
125
+ * but does NOT emit a single `/sitemap.xml` index. Without one,
126
+ * robots.txt + crawlers point at a 404 and never discover the chunks.
127
+ * This factory closes that gap: it returns a `GET` Route Handler that
128
+ * mints a `<sitemapindex>` XML listing every chunk the matching
129
+ * `createDjangoSitemap` would produce.
130
+ *
131
+ * Usage:
132
+ *
133
+ * ```ts
134
+ * // apps/<app>/app/sitemap.xml/route.ts
135
+ * import { createSitemapIndex } from '@djangocfg/nextjs/sitemap';
136
+ *
137
+ * export const { GET } = createSitemapIndex({
138
+ * host: process.env.NEXT_PUBLIC_SITE_URL!,
139
+ * apiUrl: process.env.NEXT_PUBLIC_API_URL!,
140
+ * });
141
+ *
142
+ * export const revalidate = 600;
143
+ * ```
144
+ *
145
+ * Keep `host` / `apiUrl` in sync with the same app's `app/sitemap.ts`
146
+ * — the index just enumerates the chunks the other file emits.
147
+ */
148
+
149
+ interface SitemapIndexRoute {
150
+ GET: () => Promise<Response>;
151
+ }
152
+ declare function createSitemapIndex(opts: CreateSitemapIndexOptions): SitemapIndexRoute;
153
+
107
154
  /**
108
155
  * createRobots — factory for Next.js `app/robots.ts`.
109
156
  *
@@ -152,4 +199,4 @@ declare function decodeChunkId(id: string): {
152
199
  declare function fetchSitemapIndex(apiUrl: string, revalidate: number): Promise<SitemapIndex>;
153
200
  declare function fetchSitemapFeed(apiUrl: string, source: string, cursor: string | null, revalidate: number): Promise<SitemapFeedPage>;
154
201
 
155
- export { type CreateDjangoSitemapOptions, type CreateRobotsOptions, type SitemapChunkInfo, type SitemapEntry, type SitemapFeedPage, type SitemapIndex, type SitemapSourceInfo, type StaticRoute, createDjangoSitemap, createRobots, decodeChunkId, encodeChunkId, fetchSitemapFeed, fetchSitemapIndex };
202
+ 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 };
@@ -1,3 +1,8 @@
1
+ // src/sitemap/constants.ts
2
+ var STATIC_ID = "static";
3
+ var DEFAULT_INDEX_REVALIDATE = 600;
4
+ var DEFAULT_FEED_REVALIDATE = 3600;
5
+
1
6
  // src/sitemap/fetch.ts
2
7
  var EMPTY_INDEX = {
3
8
  sources: [],
@@ -58,9 +63,6 @@ function decodeChunkId(id) {
58
63
  }
59
64
 
60
65
  // src/sitemap/sitemap.ts
61
- var STATIC_ID = "static";
62
- var DEFAULT_INDEX_REVALIDATE = 600;
63
- var DEFAULT_FEED_REVALIDATE = 3600;
64
66
  function createDjangoSitemap(opts) {
65
67
  const {
66
68
  host,
@@ -104,6 +106,56 @@ function renderStatic(host, routes) {
104
106
  }));
105
107
  }
106
108
 
109
+ // src/sitemap/index-route.ts
110
+ function createSitemapIndex(opts) {
111
+ const {
112
+ host,
113
+ apiUrl,
114
+ indexRevalidate = DEFAULT_INDEX_REVALIDATE,
115
+ includeStatic = true
116
+ } = opts;
117
+ return {
118
+ async GET() {
119
+ const ids = includeStatic ? [STATIC_ID] : [];
120
+ if (apiUrl) {
121
+ 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
+ }
127
+ }
128
+ const lastmod = (/* @__PURE__ */ new Date()).toISOString();
129
+ const body = renderIndex(host, ids, lastmod);
130
+ return new Response(body, {
131
+ status: 200,
132
+ headers: {
133
+ "content-type": "application/xml; charset=utf-8",
134
+ // Mirror the per-chunk revalidate so a cached CDN edge expires
135
+ // at the same cadence as the index it points at.
136
+ "cache-control": `public, s-maxage=${indexRevalidate}, stale-while-revalidate=${indexRevalidate}`
137
+ }
138
+ });
139
+ }
140
+ };
141
+ }
142
+ function renderIndex(host, ids, lastmod) {
143
+ const lines = [];
144
+ lines.push('<?xml version="1.0" encoding="UTF-8"?>');
145
+ lines.push('<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">');
146
+ for (const id of ids) {
147
+ lines.push(" <sitemap>");
148
+ lines.push(` <loc>${escapeXml(`${host}/sitemap/${id}.xml`)}</loc>`);
149
+ lines.push(` <lastmod>${lastmod}</lastmod>`);
150
+ lines.push(" </sitemap>");
151
+ }
152
+ lines.push("</sitemapindex>");
153
+ return lines.join("\n");
154
+ }
155
+ function escapeXml(s) {
156
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
157
+ }
158
+
107
159
  // src/sitemap/robots.ts
108
160
  var DEFAULT_DISALLOW = ["/account/", "/auth", "/api/"];
109
161
  function createRobots(opts) {
@@ -117,6 +169,7 @@ function createRobots(opts) {
117
169
  export {
118
170
  createDjangoSitemap,
119
171
  createRobots,
172
+ createSitemapIndex,
120
173
  decodeChunkId,
121
174
  encodeChunkId,
122
175
  fetchSitemapFeed,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sitemap/fetch.ts","../../src/sitemap/ids.ts","../../src/sitemap/sitemap.ts","../../src/sitemap/robots.ts"],"sourcesContent":["/**\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 { fetchSitemapFeed, fetchSitemapIndex } from './fetch';\nimport { decodeChunkId, encodeChunkId } from './ids';\n\nimport type { CreateDjangoSitemapOptions, StaticRoute } from './types';\n\nconst STATIC_ID = 'static';\nconst DEFAULT_INDEX_REVALIDATE = 600;\nconst DEFAULT_FEED_REVALIDATE = 3600;\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 * 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 * Usage:\n *\n * ```ts\n * // apps/<app>/app/robots.ts\n * import { createRobots } from '@djangocfg/nextjs/sitemap';\n *\n * export default createRobots({\n * host: process.env.NEXT_PUBLIC_SITE_URL!,\n * disallow: ['/account/', '/auth', '/api/', '/apix/'],\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/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 `app/sitemap.xml/route.ts`.\n *\n * Next.js's `generateSitemaps()` API ships chunks at `/sitemap/<id>.xml`\n * but does NOT emit a single `/sitemap.xml` index. 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 * Usage:\n *\n * ```ts\n * // apps/<app>/app/sitemap.xml/route.ts\n * import { createSitemapIndex } from '@djangocfg/nextjs/sitemap';\n *\n * export const { GET } = createSitemapIndex({\n * host: process.env.NEXT_PUBLIC_SITE_URL!,\n * apiUrl: process.env.NEXT_PUBLIC_API_URL!,\n * });\n *\n * export const revalidate = 600;\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 * Usage:\n *\n * ```ts\n * // apps/<app>/app/robots.ts\n * import { createRobots } from '@djangocfg/nextjs/sitemap';\n *\n * export default createRobots({\n * host: process.env.NEXT_PUBLIC_SITE_URL!,\n * disallow: ['/account/', '/auth', '/api/', '/apix/'],\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.413",
3
+ "version": "2.1.416",
4
4
  "description": "Next.js server utilities: sitemap, health, OG images, contact forms, navigation, config",
5
5
  "keywords": [
6
6
  "nextjs",
@@ -143,9 +143,9 @@
143
143
  "ai-docs": "tsx src/ai/cli.ts"
144
144
  },
145
145
  "peerDependencies": {
146
- "@djangocfg/i18n": "^2.1.413",
147
- "@djangocfg/monitor": "^2.1.413",
148
- "@djangocfg/ui-core": "^2.1.413",
146
+ "@djangocfg/i18n": "^2.1.416",
147
+ "@djangocfg/monitor": "^2.1.416",
148
+ "@djangocfg/ui-core": "^2.1.416",
149
149
  "next": "^16.2.2"
150
150
  },
151
151
  "peerDependenciesMeta": {
@@ -167,11 +167,11 @@
167
167
  "serwist": "^9.2.3"
168
168
  },
169
169
  "devDependencies": {
170
- "@djangocfg/i18n": "^2.1.413",
171
- "@djangocfg/monitor": "^2.1.413",
172
- "@djangocfg/ui-core": "^2.1.413",
173
- "@djangocfg/layouts": "^2.1.413",
174
- "@djangocfg/typescript-config": "^2.1.413",
170
+ "@djangocfg/i18n": "^2.1.416",
171
+ "@djangocfg/monitor": "^2.1.416",
172
+ "@djangocfg/ui-core": "^2.1.416",
173
+ "@djangocfg/layouts": "^2.1.416",
174
+ "@djangocfg/typescript-config": "^2.1.416",
175
175
  "@types/node": "^25.2.3",
176
176
  "@types/react": "^19.2.15",
177
177
  "@types/react-dom": "^19.2.3",
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Shared sitemap constants.
3
+ *
4
+ * Pulled out so both the chunk factory (`createDjangoSitemap`) and the
5
+ * index Route Handler (`createSitemapIndex`) agree on the id of the
6
+ * frontend-only static-routes chunk.
7
+ */
8
+
9
+ export const STATIC_ID = 'static';
10
+ export const DEFAULT_INDEX_REVALIDATE = 600;
11
+ export const DEFAULT_FEED_REVALIDATE = 3600;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * createSitemapIndex — Route Handler factory for `app/sitemap.xml/route.ts`.
3
+ *
4
+ * Next.js's `generateSitemaps()` API ships chunks at `/sitemap/<id>.xml`
5
+ * but does NOT emit a single `/sitemap.xml` index. Without one,
6
+ * robots.txt + crawlers point at a 404 and never discover the chunks.
7
+ * This factory closes that gap: it returns a `GET` Route Handler that
8
+ * mints a `<sitemapindex>` XML listing every chunk the matching
9
+ * `createDjangoSitemap` would produce.
10
+ *
11
+ * Usage:
12
+ *
13
+ * ```ts
14
+ * // apps/<app>/app/sitemap.xml/route.ts
15
+ * import { createSitemapIndex } from '@djangocfg/nextjs/sitemap';
16
+ *
17
+ * export const { GET } = createSitemapIndex({
18
+ * host: process.env.NEXT_PUBLIC_SITE_URL!,
19
+ * apiUrl: process.env.NEXT_PUBLIC_API_URL!,
20
+ * });
21
+ *
22
+ * export const revalidate = 600;
23
+ * ```
24
+ *
25
+ * Keep `host` / `apiUrl` in sync with the same app's `app/sitemap.ts`
26
+ * — the index just enumerates the chunks the other file emits.
27
+ */
28
+
29
+ import {
30
+ DEFAULT_INDEX_REVALIDATE,
31
+ STATIC_ID,
32
+ } from './constants';
33
+ import { fetchSitemapIndex } from './fetch';
34
+ import { encodeChunkId } from './ids';
35
+
36
+ import type { CreateSitemapIndexOptions } from './types';
37
+
38
+ interface SitemapIndexRoute {
39
+ GET: () => Promise<Response>;
40
+ }
41
+
42
+ export function createSitemapIndex(opts: CreateSitemapIndexOptions): SitemapIndexRoute {
43
+ const {
44
+ host,
45
+ apiUrl,
46
+ indexRevalidate = DEFAULT_INDEX_REVALIDATE,
47
+ includeStatic = true,
48
+ } = opts;
49
+
50
+ return {
51
+ async GET() {
52
+ const ids: string[] = includeStatic ? [STATIC_ID] : [];
53
+
54
+ if (apiUrl) {
55
+ const index = await fetchSitemapIndex(apiUrl, indexRevalidate);
56
+ for (const s of index.sources) {
57
+ for (const c of s.chunks) {
58
+ ids.push(encodeChunkId(s.name, c.cursor_to));
59
+ }
60
+ }
61
+ }
62
+
63
+ const lastmod = new Date().toISOString();
64
+ const body = renderIndex(host, ids, lastmod);
65
+
66
+ return new Response(body, {
67
+ status: 200,
68
+ headers: {
69
+ 'content-type': 'application/xml; charset=utf-8',
70
+ // Mirror the per-chunk revalidate so a cached CDN edge expires
71
+ // at the same cadence as the index it points at.
72
+ 'cache-control': `public, s-maxage=${indexRevalidate}, stale-while-revalidate=${indexRevalidate}`,
73
+ },
74
+ });
75
+ },
76
+ };
77
+ }
78
+
79
+ function renderIndex(host: string, ids: string[], lastmod: string): string {
80
+ const lines: string[] = [];
81
+ lines.push('<?xml version="1.0" encoding="UTF-8"?>');
82
+ lines.push('<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">');
83
+ for (const id of ids) {
84
+ lines.push(' <sitemap>');
85
+ lines.push(` <loc>${escapeXml(`${host}/sitemap/${id}.xml`)}</loc>`);
86
+ lines.push(` <lastmod>${lastmod}</lastmod>`);
87
+ lines.push(' </sitemap>');
88
+ }
89
+ lines.push('</sitemapindex>');
90
+ return lines.join('\n');
91
+ }
92
+
93
+ function escapeXml(s: string): string {
94
+ return s
95
+ .replace(/&/g, '&amp;')
96
+ .replace(/</g, '&lt;')
97
+ .replace(/>/g, '&gt;')
98
+ .replace(/"/g, '&quot;')
99
+ .replace(/'/g, '&apos;');
100
+ }
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  export { createDjangoSitemap } from './sitemap';
13
+ export { createSitemapIndex } from './index-route';
13
14
  export { createRobots } from './robots';
14
15
  export { encodeChunkId, decodeChunkId } from './ids';
15
16
  export { fetchSitemapIndex, fetchSitemapFeed } from './fetch';
@@ -21,6 +22,7 @@ export type {
21
22
  SitemapIndex,
22
23
  SitemapSourceInfo,
23
24
  CreateDjangoSitemapOptions,
25
+ CreateSitemapIndexOptions,
24
26
  CreateRobotsOptions,
25
27
  StaticRoute,
26
28
  } from './types';
@@ -30,15 +30,16 @@
30
30
 
31
31
  import type { MetadataRoute } from 'next';
32
32
 
33
+ import {
34
+ DEFAULT_FEED_REVALIDATE,
35
+ DEFAULT_INDEX_REVALIDATE,
36
+ STATIC_ID,
37
+ } from './constants';
33
38
  import { fetchSitemapFeed, fetchSitemapIndex } from './fetch';
34
39
  import { decodeChunkId, encodeChunkId } from './ids';
35
40
 
36
41
  import type { CreateDjangoSitemapOptions, StaticRoute } from './types';
37
42
 
38
- const STATIC_ID = 'static';
39
- const DEFAULT_INDEX_REVALIDATE = 600;
40
- const DEFAULT_FEED_REVALIDATE = 3600;
41
-
42
43
  interface SitemapApi {
43
44
  generateSitemaps: () => Promise<Array<{ id: string }>>;
44
45
  sitemap: (props: { id: Promise<string> }) => Promise<MetadataRoute.Sitemap>;
@@ -70,6 +70,24 @@ export interface CreateDjangoSitemapOptions {
70
70
  feedRevalidate?: number;
71
71
  }
72
72
 
73
+ export interface CreateSitemapIndexOptions {
74
+ /** Canonical site host, e.g. `https://vamcar.com`. Chunk URLs in the
75
+ * index are emitted as `${host}/sitemap/<id>.xml`. */
76
+ host: string;
77
+
78
+ /** Django backend base URL. When omitted, the index lists only the
79
+ * static chunk (mirrors `createDjangoSitemap` without `apiUrl`). */
80
+ apiUrl?: string;
81
+
82
+ /** Revalidate window in seconds for both the upstream `index/` fetch
83
+ * and the emitted XML's `s-maxage`. Default: 600. */
84
+ indexRevalidate?: number;
85
+
86
+ /** Include the `static` chunk id at the top of the index. Turn off if
87
+ * the app's `sitemap.ts` opts out of `staticRoutes`. Default: true. */
88
+ includeStatic?: boolean;
89
+ }
90
+
73
91
  export interface CreateRobotsOptions {
74
92
  host: string;
75
93
  /** Disallow patterns. Default: ['/account/', '/auth', '/api/']. */