@djangocfg/nextjs 2.1.412 → 2.1.415

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.
@@ -0,0 +1,315 @@
1
+ # @djangocfg/nextjs/sitemap
2
+
3
+ Frontend half of the djangocfg ↔ Next.js sitemap integration.
4
+
5
+ The Django-side backend (`django_cfg.modules.django_sitemap`) exposes a
6
+ paginated JSON feed. This subpackage turns that feed into a Google-compliant
7
+ XML sitemap-index via Next.js's native `generateSitemaps()` API.
8
+
9
+ ## Why this exists
10
+
11
+ Two naive approaches fail at scale:
12
+
13
+ 1. **Django serves XML directly** — wrong host (api.example.com vs
14
+ www.example.com), can't include frontend-only routes, hard to add
15
+ per-locale alternates, awkward sitemap-index dance.
16
+ 2. **Frontend queries the DB** — frontends don't have DB access; falling
17
+ back to public REST endpoints means N round trips and no streaming.
18
+
19
+ This package solves both. Django returns path-only JSON; the frontend
20
+ joins the host and emits sitemap-index XML. Same backend can serve
21
+ multiple frontends (preview, staging, EU domain) — they each bring their
22
+ own host.
23
+
24
+ ## The big picture
25
+
26
+ ```
27
+ ┌──────────────────────────────────────────────────────────────────────┐
28
+ │ Django app (e.g. apps/catalog) │
29
+ │ sitemap_sources.py │
30
+ │ register(SitemapSource( │
31
+ │ name="vehicles", │
32
+ │ queryset_factory=lambda: Vehicle.objects.sitemap_eligible(), │
33
+ │ url_template="/catalog/{id}", │
34
+ │ lastmod_field="last_seen_at", │
35
+ │ cursor_fields=("last_seen_at", "id"), │
36
+ │ order="-last_seen_at,-id", │
37
+ │ page_size=50_000, │
38
+ │ )) │
39
+ └──────────────────────────┬───────────────────────────────────────────┘
40
+ │ AppConfig.ready() → in-process registry
41
+
42
+ ┌──────────────────────────────────────────────────────────────────────┐
43
+ │ django_cfg.modules.django_sitemap (HTTP layer) │
44
+ │ │
45
+ │ GET /cfg/sitemap/index/ │
46
+ │ → list of chunks per source, with opaque keyset cursors │
47
+ │ │
48
+ │ GET /cfg/sitemap/feed/?source=<name>&cursor=<opaque> │
49
+ │ → 50 000 entries: { loc, lastmod } │
50
+ │ │
51
+ │ Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400 │
52
+ └──────────────────────────┬───────────────────────────────────────────┘
53
+ │ HTTP (server-side fetch from Next)
54
+
55
+ ┌──────────────────────────────────────────────────────────────────────┐
56
+ │ Next.js app — apps/<app>/app/ │
57
+ │ │
58
+ │ sitemap.ts │
59
+ │ createDjangoSitemap({ host, apiUrl, staticRoutes }) │
60
+ │ ├ generateSitemaps() — reads /cfg/sitemap/index/, returns ids │
61
+ │ └ default sitemap() — fetches one /cfg/sitemap/feed/ per id │
62
+ │ │
63
+ │ robots.ts │
64
+ │ createRobots({ host, disallow }) │
65
+ └──────────────────────────────────────────────────────────────────────┘
66
+ ```
67
+
68
+ Next emits:
69
+
70
+ - `/sitemap.xml` — auto-generated `<sitemapindex>` listing every chunk.
71
+ - `/sitemap/<id>.xml` — one `<urlset>` per chunk (50k URLs max, per
72
+ Google's cap).
73
+ - `/robots.txt` — points crawlers at `/sitemap.xml`.
74
+
75
+ ## Quick start
76
+
77
+ ### 1. Register sources in Django
78
+
79
+ ```python
80
+ # apps/<your-app>/sitemap_sources.py
81
+ from django_cfg.modules.django_sitemap import SitemapSource, register
82
+ from apps.catalog.models import Vehicle, Brand
83
+
84
+ register(SitemapSource(
85
+ name="vehicles",
86
+ url_template="/catalog/{id}",
87
+ queryset_factory=lambda: Vehicle.objects.get_queryset().sitemap_eligible(),
88
+ fields={"id": "id"},
89
+ lastmod_field="last_seen_at",
90
+ cursor_fields=("last_seen_at", "id"),
91
+ order="-last_seen_at,-id",
92
+ page_size=50_000,
93
+ ))
94
+
95
+ register(SitemapSource(
96
+ name="brands",
97
+ url_template="/catalog?brand={slug}",
98
+ queryset_factory=lambda: Brand.objects.filter(is_active=True),
99
+ fields={"slug": "slug"},
100
+ lastmod_field="updated_at",
101
+ cursor_fields=("slug",),
102
+ order="slug",
103
+ ))
104
+ ```
105
+
106
+ `SitemapAppConfig.ready()` autoloads every installed app's
107
+ `sitemap_sources.py` — no host-side wiring needed. Verify with
108
+ `python manage.py sitemap_inspect`.
109
+
110
+ ### 2. Wire up the frontend
111
+
112
+ ```tsx
113
+ // apps/<your-app>/app/sitemap.ts
114
+ import { createDjangoSitemap } from '@djangocfg/nextjs/sitemap';
115
+
116
+ const { generateSitemaps, sitemap } = createDjangoSitemap({
117
+ host: process.env.NEXT_PUBLIC_SITE_URL!,
118
+ apiUrl: process.env.NEXT_PUBLIC_API_URL!,
119
+ staticRoutes: [
120
+ { path: '/', changeFrequency: 'daily', priority: 1.0 },
121
+ { path: '/catalog', changeFrequency: 'daily', priority: 0.9 },
122
+ { path: '/ai-search', changeFrequency: 'weekly', priority: 0.7 },
123
+ ],
124
+ });
125
+
126
+ export { generateSitemaps, sitemap as default };
127
+ export const revalidate = 3600;
128
+ ```
129
+
130
+ ```tsx
131
+ // apps/<your-app>/app/robots.ts
132
+ import { createRobots } from '@djangocfg/nextjs/sitemap';
133
+
134
+ export default createRobots({
135
+ host: process.env.NEXT_PUBLIC_SITE_URL!,
136
+ disallow: ['/account/', '/auth', '/api/', '/apix/'],
137
+ });
138
+ ```
139
+
140
+ That's the whole app-side integration.
141
+
142
+ ## Backend without backend
143
+
144
+ Marketing sites and documentation portals usually don't have a Django
145
+ catalog to paginate. Omit `apiUrl` — the frontend then emits a
146
+ single-chunk sitemap from `staticRoutes` only.
147
+
148
+ ```tsx
149
+ const { generateSitemaps, sitemap } = createDjangoSitemap({
150
+ host: 'https://example.com',
151
+ staticRoutes: [
152
+ { path: '/', priority: 1.0 },
153
+ { path: '/about', priority: 0.8 },
154
+ { path: '/pricing', priority: 0.9 },
155
+ ],
156
+ });
157
+ ```
158
+
159
+ ## What gets emitted
160
+
161
+ For a 1 200 000-vehicle catalog + 6 static pages:
162
+
163
+ - `/sitemap.xml` — index with ~25 child entries (24 vehicle chunks +
164
+ brands + models + static).
165
+ - `/sitemap/static.xml` — 6 static URLs with `<changefreq>`/`<priority>`.
166
+ - `/sitemap/vehicles--<cursor>.xml` × 24 — 50 000 vehicle URLs each
167
+ (sorted by `last_seen_at DESC` so the freshest listings live in chunk 1
168
+ and Googlebot re-crawls them first).
169
+ - `/sitemap/brands--.xml` — one chunk.
170
+ - `/sitemap/models--.xml` — one chunk.
171
+
172
+ **Only `loc` and `lastmod`** are emitted from backend entries: Google
173
+ ignores `<changefreq>` and `<priority>` on dynamic URLs, and omitting
174
+ them keeps the payload small. Static routes still emit them — non-Google
175
+ crawlers (Bing, Yandex) read those fields, and they're free.
176
+
177
+ ## Cache layers
178
+
179
+ Three layers cooperate; each TTL aligned with how often the data
180
+ *actually* changes.
181
+
182
+ | Layer | Default TTL | Tuned via |
183
+ |---|---|---|
184
+ | Django Redis (sitemap-index JSON) | 5 min | `SitemapConfig.cache_index_seconds` |
185
+ | Django Redis (per-chunk JSON) | 1 h | `SitemapConfig.cache_feed_seconds` |
186
+ | Next.js ISR (XML) | 1 h | `createDjangoSitemap({ feedRevalidate })` + `export const revalidate = ...` |
187
+ | HTTP `Cache-Control` | `s-maxage=3600, swr=86400` | Django sets, CDN respects |
188
+
189
+ Result: Googlebot fetching `/sitemap.xml` rarely touches Django — most
190
+ hits land on the CDN edge or Next ISR. Django only does real work once
191
+ per TTL.
192
+
193
+ To force a cache bust without `FLUSHDB`, bump `SitemapConfig.cache_version`
194
+ on the Django side; every Redis key includes it.
195
+
196
+ ## Failure mode
197
+
198
+ `fetchSitemapIndex` and `fetchSitemapFeed` **never throw**. A transient
199
+ Django outage during `next build` is downgraded to:
200
+
201
+ - empty index → only the static chunk is emitted
202
+ - empty feed → that chunk renders an empty `<urlset/>`
203
+
204
+ `next build` completes, the bad cache entry expires on the next ISR
205
+ cycle, and the sitemap heals automatically. No build-time coupling to
206
+ Django uptime.
207
+
208
+ ## Deployment
209
+
210
+ ### Env vars
211
+
212
+ ```
213
+ NEXT_PUBLIC_SITE_URL=https://example.com # canonical host
214
+ NEXT_PUBLIC_API_URL=https://api.example.com # Django backend
215
+ ```
216
+
217
+ Both are public — sitemap.ts runs on the server but the same `host` is
218
+ also embedded in client-side metadata.
219
+
220
+ ### Docker
221
+
222
+ Nothing sitemap-specific is required in the container — the package is a
223
+ plain TypeScript dependency, no native modules, no extra ports. Just pass
224
+ the two env vars above via `env_file` or `environment:`.
225
+
226
+ The Next service's existing healthcheck on `/` is enough; no separate
227
+ sitemap probe needed (a missing sitemap doesn't break the app).
228
+
229
+ ### CDN / Cloudflare
230
+
231
+ Cache by URL, respect origin `Cache-Control`:
232
+
233
+ | URL pattern | TTL fresh | TTL stale |
234
+ |---|---|---|
235
+ | `/sitemap.xml` | 1 h | 1 day |
236
+ | `/sitemap/*.xml` | 1 h | 1 day |
237
+ | `/robots.txt` | 24 h | 1 day |
238
+
239
+ Stale-while-revalidate keeps Googlebot fast even during a Django outage.
240
+
241
+ ### Sitemap submission
242
+
243
+ Submit `https://<host>/sitemap.xml` once via Google Search Console.
244
+ Google polls it on its own schedule afterwards — no `ping` integration
245
+ needed.
246
+
247
+ ## API reference
248
+
249
+ ### `createDjangoSitemap(options)`
250
+
251
+ Returns `{ generateSitemaps, sitemap }`. Export them as Next.js
252
+ expects: named `generateSitemaps` + default-exported `sitemap`.
253
+
254
+ **Options:**
255
+
256
+ | Field | Type | Default | Notes |
257
+ |---|---|---|---|
258
+ | `host` | `string` | (required) | Canonical host (e.g. `https://example.com`). URLs are joined as `${host}${loc}`. |
259
+ | `apiUrl` | `string` | `undefined` | Django backend base URL. Omit to skip the backend feed entirely. |
260
+ | `staticRoutes` | `StaticRoute[]` | `[]` | Frontend-only routes. Auth/account routes should NOT be listed here. |
261
+ | `indexRevalidate` | `number` | `600` | Seconds — `next: { revalidate }` on the index fetch. |
262
+ | `feedRevalidate` | `number` | `3600` | Seconds — `next: { revalidate }` on each chunk fetch. |
263
+
264
+ **`StaticRoute`:**
265
+
266
+ ```ts
267
+ interface StaticRoute {
268
+ path: string;
269
+ changeFrequency?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
270
+ priority?: number; // 0.0 – 1.0
271
+ }
272
+ ```
273
+
274
+ ### `createRobots(options)`
275
+
276
+ Returns a Next.js `() => MetadataRoute.Robots` function. Default-export
277
+ it from `app/robots.ts`.
278
+
279
+ **Options:**
280
+
281
+ | Field | Type | Default | Notes |
282
+ |---|---|---|---|
283
+ | `host` | `string` | (required) | Canonical host. |
284
+ | `disallow` | `string[]` | `['/account/', '/auth', '/api/']` | Path prefixes to block. |
285
+ | `sitemap` | `string` | `${host}/sitemap.xml` | Override the sitemap pointer. |
286
+
287
+ ### Lower-level utilities
288
+
289
+ Exposed for advanced use cases (custom Next route handlers, debugging):
290
+
291
+ ```ts
292
+ import {
293
+ fetchSitemapIndex,
294
+ fetchSitemapFeed,
295
+ encodeChunkId,
296
+ decodeChunkId,
297
+ } from '@djangocfg/nextjs/sitemap';
298
+ ```
299
+
300
+ ## When NOT to use this
301
+
302
+ - Sites under ~1 000 URLs with no backend pagination needs — a flat
303
+ `staticRoutes`-only configuration is fine, but a hand-written
304
+ `sitemap.ts` returning a literal array works just as well.
305
+ - You need per-locale `<xhtml:link rel="alternate">` tags — not currently
306
+ emitted; the wire contract has room for it (`alternates`) but no
307
+ consumer requires it yet.
308
+ - You need image/video/news sitemap extensions — not implemented.
309
+
310
+ ## See also
311
+
312
+ - `django_cfg.modules.django_sitemap` — backend module (registry, HTTP
313
+ views, `sitemap_inspect` management command).
314
+ - [Next.js docs — generateSitemaps](https://nextjs.org/docs/app/api-reference/functions/generate-sitemaps)
315
+ - [Google — large sitemaps](https://developers.google.com/search/docs/crawling-indexing/sitemaps/large-sitemaps)
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Sitemap fetch helpers — never throw.
3
+ *
4
+ * Failures are converted to empty payloads so a transient Django outage
5
+ * doesn't break `next build`. The next ISR cycle (`revalidate`) recovers
6
+ * automatically.
7
+ */
8
+
9
+ import type { SitemapFeedPage, SitemapIndex } from './types';
10
+
11
+ const EMPTY_INDEX: SitemapIndex = {
12
+ sources: [],
13
+ generated_at: new Date(0).toISOString(),
14
+ ttl_seconds: 60,
15
+ };
16
+
17
+ const emptyFeed = (source: string, cursor: string | null): SitemapFeedPage => ({
18
+ source,
19
+ chunk_id: `${source}-empty`,
20
+ count: 0,
21
+ has_more: false,
22
+ next_cursor: cursor,
23
+ entries: [],
24
+ });
25
+
26
+ export async function fetchSitemapIndex(
27
+ apiUrl: string,
28
+ revalidate: number,
29
+ ): Promise<SitemapIndex> {
30
+ try {
31
+ const r = await fetch(`${apiUrl}/cfg/sitemap/index/`, {
32
+ next: { revalidate },
33
+ });
34
+ if (!r.ok) {
35
+ console.warn(`[sitemap] index fetch returned ${r.status}; falling back to empty`);
36
+ return EMPTY_INDEX;
37
+ }
38
+ return (await r.json()) as SitemapIndex;
39
+ } catch (err) {
40
+ console.warn('[sitemap] index fetch failed:', err);
41
+ return EMPTY_INDEX;
42
+ }
43
+ }
44
+
45
+ export async function fetchSitemapFeed(
46
+ apiUrl: string,
47
+ source: string,
48
+ cursor: string | null,
49
+ revalidate: number,
50
+ ): Promise<SitemapFeedPage> {
51
+ const params = new URLSearchParams({ source });
52
+ if (cursor) params.set('cursor', cursor);
53
+ try {
54
+ const r = await fetch(`${apiUrl}/cfg/sitemap/feed/?${params}`, {
55
+ next: { revalidate },
56
+ });
57
+ if (!r.ok) {
58
+ console.warn(`[sitemap] feed ${source} returned ${r.status}; falling back to empty`);
59
+ return emptyFeed(source, cursor);
60
+ }
61
+ return (await r.json()) as SitemapFeedPage;
62
+ } catch (err) {
63
+ console.warn(`[sitemap] feed ${source} fetch failed:`, err);
64
+ return emptyFeed(source, cursor);
65
+ }
66
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Sitemap chunk id encoding.
3
+ *
4
+ * Each Next.js sitemap file is addressed by an opaque string id. We
5
+ * round-trip a `(source, cursor)` pair through that id so the default
6
+ * sitemap handler knows which Django chunk to fetch.
7
+ *
8
+ * Separator `--` is unambiguous: backend cursors use URL-safe base64 (`-_`).
9
+ */
10
+
11
+ export function encodeChunkId(source: string, cursor: string | null): string {
12
+ return `${source}--${cursor ?? ''}`;
13
+ }
14
+
15
+ export function decodeChunkId(id: string): { source: string; cursor: string | null } {
16
+ const sep = id.indexOf('--');
17
+ if (sep === -1) return { source: id, cursor: null };
18
+ const source = id.slice(0, sep);
19
+ const cursorRaw = id.slice(sep + 2);
20
+ return { source, cursor: cursorRaw === '' ? null : cursorRaw };
21
+ }
@@ -1,8 +1,26 @@
1
1
  /**
2
- * Sitemap exports
2
+ * @djangocfg/nextjs/sitemap
3
+ *
4
+ * Sitemap-index + per-chunk Next.js sitemap factory backed by
5
+ * `django_cfg.modules.django_sitemap`. Handles catalogs up to Google's
6
+ * per-file URL caps without materialising data in the frontend.
7
+ *
8
+ * See `createDjangoSitemap` for the main entry point and `createRobots`
9
+ * for the matching robots.txt factory.
3
10
  */
4
11
 
5
- export { createSitemapHandler } from './route';
6
- export { generateSitemapXml, normalizeUrl } from './generator';
7
- export type { SitemapGeneratorOptions, SitemapRoute, SitemapI18nOptions } from './types';
12
+ export { createDjangoSitemap } from './sitemap';
13
+ export { createRobots } from './robots';
14
+ export { encodeChunkId, decodeChunkId } from './ids';
15
+ export { fetchSitemapIndex, fetchSitemapFeed } from './fetch';
8
16
 
17
+ export type {
18
+ SitemapChunkInfo,
19
+ SitemapEntry,
20
+ SitemapFeedPage,
21
+ SitemapIndex,
22
+ SitemapSourceInfo,
23
+ CreateDjangoSitemapOptions,
24
+ CreateRobotsOptions,
25
+ StaticRoute,
26
+ } from './types';
@@ -0,0 +1,34 @@
1
+ /**
2
+ * createRobots — factory for Next.js `app/robots.ts`.
3
+ *
4
+ * Emits a single `User-Agent: *` block plus a Sitemap pointer. Disallow
5
+ * defaults cover the common cases (auth, account, API surface) — override
6
+ * per app if needed.
7
+ *
8
+ * Usage:
9
+ *
10
+ * ```ts
11
+ * // apps/<app>/app/robots.ts
12
+ * import { createRobots } from '@djangocfg/nextjs/sitemap';
13
+ *
14
+ * export default createRobots({
15
+ * host: process.env.NEXT_PUBLIC_SITE_URL!,
16
+ * disallow: ['/account/', '/auth', '/api/', '/apix/'],
17
+ * });
18
+ * ```
19
+ */
20
+
21
+ import type { MetadataRoute } from 'next';
22
+
23
+ import type { CreateRobotsOptions } from './types';
24
+
25
+ const DEFAULT_DISALLOW = ['/account/', '/auth', '/api/'];
26
+
27
+ export function createRobots(opts: CreateRobotsOptions): () => MetadataRoute.Robots {
28
+ const { host, disallow = DEFAULT_DISALLOW, sitemap } = opts;
29
+ return () => ({
30
+ rules: [{ userAgent: '*', allow: '/', disallow }],
31
+ sitemap: sitemap ?? `${host}/sitemap.xml`,
32
+ host,
33
+ });
34
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * createDjangoSitemap — factory for Next.js `app/sitemap.ts`.
3
+ *
4
+ * Backed by `django_cfg.modules.django_sitemap`: the backend exposes a
5
+ * paginated JSON feed, this factory turns each chunk into a Next.js
6
+ * sitemap file via `generateSitemaps()`.
7
+ *
8
+ * Usage:
9
+ *
10
+ * ```ts
11
+ * // apps/<app>/app/sitemap.ts
12
+ * import { createDjangoSitemap } from '@djangocfg/nextjs/sitemap';
13
+ *
14
+ * const { generateSitemaps, sitemap } = createDjangoSitemap({
15
+ * host: process.env.NEXT_PUBLIC_SITE_URL!,
16
+ * apiUrl: process.env.NEXT_PUBLIC_API_URL!,
17
+ * staticRoutes: [
18
+ * { path: '/', changeFrequency: 'daily', priority: 1.0 },
19
+ * { path: '/catalog', changeFrequency: 'daily', priority: 0.9 },
20
+ * ],
21
+ * });
22
+ *
23
+ * export { generateSitemaps, sitemap as default };
24
+ * export const revalidate = 3600;
25
+ * ```
26
+ *
27
+ * Without `apiUrl`, the sitemap emits only the static routes — handy for
28
+ * marketing sites that don't have a backend-driven URL space.
29
+ */
30
+
31
+ import type { MetadataRoute } from 'next';
32
+
33
+ import { fetchSitemapFeed, fetchSitemapIndex } from './fetch';
34
+ import { decodeChunkId, encodeChunkId } from './ids';
35
+
36
+ import type { CreateDjangoSitemapOptions, StaticRoute } from './types';
37
+
38
+ const STATIC_ID = 'static';
39
+ const DEFAULT_INDEX_REVALIDATE = 600;
40
+ const DEFAULT_FEED_REVALIDATE = 3600;
41
+
42
+ interface SitemapApi {
43
+ generateSitemaps: () => Promise<Array<{ id: string }>>;
44
+ sitemap: (props: { id: Promise<string> }) => Promise<MetadataRoute.Sitemap>;
45
+ }
46
+
47
+ export function createDjangoSitemap(opts: CreateDjangoSitemapOptions): SitemapApi {
48
+ const {
49
+ host,
50
+ apiUrl,
51
+ staticRoutes = [],
52
+ indexRevalidate = DEFAULT_INDEX_REVALIDATE,
53
+ feedRevalidate = DEFAULT_FEED_REVALIDATE,
54
+ } = opts;
55
+
56
+ return {
57
+ async generateSitemaps() {
58
+ const ids: Array<{ id: string }> = [{ id: STATIC_ID }];
59
+ if (!apiUrl) return ids;
60
+ const index = await fetchSitemapIndex(apiUrl, indexRevalidate);
61
+ for (const s of index.sources) {
62
+ for (const c of s.chunks) {
63
+ ids.push({ id: encodeChunkId(s.name, c.cursor_to) });
64
+ }
65
+ }
66
+ return ids;
67
+ },
68
+
69
+ async sitemap({ id: idPromise }) {
70
+ const id = await idPromise;
71
+
72
+ if (id === STATIC_ID) {
73
+ return renderStatic(host, staticRoutes);
74
+ }
75
+
76
+ if (!apiUrl) return [];
77
+
78
+ const { source, cursor } = decodeChunkId(id);
79
+ const feed = await fetchSitemapFeed(apiUrl, source, cursor, feedRevalidate);
80
+ return feed.entries.map((e) => ({
81
+ url: `${host}${e.loc}`,
82
+ lastModified: e.lastmod ? new Date(e.lastmod) : undefined,
83
+ }));
84
+ },
85
+ };
86
+ }
87
+
88
+ function renderStatic(host: string, routes: StaticRoute[]): MetadataRoute.Sitemap {
89
+ return routes.map((r) => ({
90
+ url: `${host}${r.path}`,
91
+ changeFrequency: r.changeFrequency,
92
+ priority: r.priority,
93
+ }));
94
+ }
@@ -1,29 +1,79 @@
1
1
  /**
2
- * Sitemap types
2
+ * Sitemap module — types
3
+ *
4
+ * Wire-contract for `django_cfg.modules.django_sitemap` JSON endpoints
5
+ * plus the public option types for `createDjangoSitemap` and
6
+ * `createRobots`.
3
7
  */
4
8
 
5
- import type { ChangeFreq, SitemapUrl } from '../types';
9
+ import type { ChangeFreq } from '../types';
6
10
 
7
- export interface SitemapI18nOptions {
8
- /** Supported locales (e.g., ['en', 'ru', 'ko']) */
9
- locales: string[];
10
- /** Default locale for x-default hreflang */
11
- defaultLocale: string;
11
+ // ── Wire contract with Django backend ───────────────────────────────────
12
+
13
+ export interface SitemapChunkInfo {
14
+ id: string;
15
+ cursor_to: string | null;
16
+ count_estimate: number;
17
+ }
18
+
19
+ export interface SitemapSourceInfo {
20
+ name: string;
21
+ chunks: SitemapChunkInfo[];
22
+ total_estimate: number;
23
+ }
24
+
25
+ export interface SitemapIndex {
26
+ sources: SitemapSourceInfo[];
27
+ generated_at: string;
28
+ ttl_seconds: number;
29
+ }
30
+
31
+ export interface SitemapEntry {
32
+ loc: string;
33
+ lastmod?: string | null;
12
34
  }
13
35
 
14
- export interface SitemapGeneratorOptions {
15
- siteUrl: string;
16
- staticPages?: SitemapUrl[];
17
- dynamicPages?: (() => Promise<SitemapUrl[]>) | SitemapUrl[];
18
- cacheControl?: string;
19
- /** i18n configuration for hreflang support */
20
- i18n?: SitemapI18nOptions;
36
+ export interface SitemapFeedPage {
37
+ source: string;
38
+ chunk_id: string;
39
+ count: number;
40
+ has_more: boolean;
41
+ next_cursor: string | null;
42
+ entries: SitemapEntry[];
21
43
  }
22
44
 
23
- export interface SitemapRoute {
45
+ // ── Public option types ─────────────────────────────────────────────────
46
+
47
+ export interface StaticRoute {
24
48
  path: string;
25
- lastmod?: string | Date;
26
- changefreq?: ChangeFreq;
49
+ changeFrequency?: ChangeFreq;
27
50
  priority?: number;
28
51
  }
29
52
 
53
+ export interface CreateDjangoSitemapOptions {
54
+ /** Canonical site host, e.g. `https://vamcar.com`. URLs in the sitemap
55
+ * are joined as `${host}${loc}`. */
56
+ host: string;
57
+
58
+ /** Django backend base URL. When omitted, backend chunks are skipped
59
+ * and only `staticRoutes` are emitted (single-chunk sitemap). */
60
+ apiUrl?: string;
61
+
62
+ /** Frontend-only routes the backend doesn't know about. Auth/account
63
+ * pages should NOT be listed here. */
64
+ staticRoutes?: StaticRoute[];
65
+
66
+ /** Index revalidate window in seconds. Default: 600. */
67
+ indexRevalidate?: number;
68
+
69
+ /** Feed (chunk) revalidate window in seconds. Default: 3600. */
70
+ feedRevalidate?: number;
71
+ }
72
+
73
+ export interface CreateRobotsOptions {
74
+ host: string;
75
+ /** Disallow patterns. Default: ['/account/', '/auth', '/api/']. */
76
+ disallow?: string[];
77
+ /** Override sitemap URL. Default: `${host}/sitemap.xml`. */
78
+ sitemap?: string;
79
+ }