@nitrogenbuilder/connector-payload 1.1.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/collections/NitrogenTemplates.js +8 -0
- package/dist/endpoints/all.js +3 -0
- package/dist/endpoints/batch.js +9 -0
- package/dist/endpoints/collection-endpoints.js +119 -3
- package/dist/endpoints/ctrlLinkResolver.js +43 -2
- package/dist/endpoints/helpers.js +2 -1
- package/dist/endpoints/menu.js +2 -0
- package/dist/endpoints/sitemap.js +47 -7
- package/dist/endpoints/templates.js +116 -0
- package/dist/endpoints/translation-status.d.ts +11 -0
- package/dist/endpoints/translation-status.js +105 -0
- package/dist/frontend/NitrogenPageClient.d.ts +6 -1
- package/dist/frontend/NitrogenPageClient.js +6 -2
- package/dist/frontend/NitrogenWrapper.d.ts +8 -1
- package/dist/frontend/NitrogenWrapper.js +25 -3
- package/dist/index.d.ts +22 -0
- package/dist/index.js +69 -2
- package/dist/inventory/indexing.d.ts +2 -0
- package/dist/inventory/indexing.js +1 -0
- package/dist/localization.d.ts +121 -0
- package/dist/localization.js +563 -0
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
|
@@ -93,5 +93,13 @@ export const NitrogenTemplates = {
|
|
|
93
93
|
},
|
|
94
94
|
],
|
|
95
95
|
},
|
|
96
|
+
{
|
|
97
|
+
name: 'nitrogenTranslationStatus',
|
|
98
|
+
type: 'json',
|
|
99
|
+
admin: {
|
|
100
|
+
hidden: true,
|
|
101
|
+
description: 'Per-language translation coverage, computed by Nitrogen on save',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
96
104
|
],
|
|
97
105
|
};
|
package/dist/endpoints/all.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getDocumentPermalink, getDocumentRelativePermalink, getNitrogenSettings, } from './helpers.js';
|
|
2
2
|
import { resolveCollection } from '../collection-registry.js';
|
|
3
|
+
import { getRequestLanguage, resolveLocaleOptions } from '../localization.js';
|
|
3
4
|
export const allEndpoints = [
|
|
4
5
|
// GET /api/nitrogen/v1/all — List all items with filtering
|
|
5
6
|
{
|
|
@@ -17,6 +18,7 @@ export const allEndpoints = [
|
|
|
17
18
|
const paged = parseInt(url.searchParams.get('paged') || '1', 10);
|
|
18
19
|
const perPage = parseInt(url.searchParams.get('per_page') || '20', 10);
|
|
19
20
|
const statuses = statusParam.split(',').map((s) => s.trim());
|
|
21
|
+
const localeOptions = resolveLocaleOptions(payload, getRequestLanguage(req));
|
|
20
22
|
const collection = resolveCollection(postType);
|
|
21
23
|
const where = {};
|
|
22
24
|
if (!statuses.includes('any')) {
|
|
@@ -29,6 +31,7 @@ export const allEndpoints = [
|
|
|
29
31
|
page: paged,
|
|
30
32
|
limit: perPage,
|
|
31
33
|
depth: 0,
|
|
34
|
+
...localeOptions,
|
|
32
35
|
});
|
|
33
36
|
const items = result.docs.map((doc) => {
|
|
34
37
|
const document = doc;
|
package/dist/endpoints/batch.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveCollection } from "../collection-registry.js";
|
|
2
2
|
import { getNitrogenSettings, buildDynamicData, buildPageResponse, getDocumentPermalink, getDocumentRelativePermalink, } from "./helpers.js";
|
|
3
|
+
import { getRequestLanguage, resolveLocaleOptions } from "../localization.js";
|
|
3
4
|
function toArray(value) {
|
|
4
5
|
if (Array.isArray(value)) {
|
|
5
6
|
return value.map((item) => String(item).trim()).filter(Boolean);
|
|
@@ -98,6 +99,9 @@ export const batchEndpoints = [
|
|
|
98
99
|
}
|
|
99
100
|
const settings = await getNitrogenSettings(payload);
|
|
100
101
|
const results = {};
|
|
102
|
+
// Request-level language (?lang=/?locale=) applies to every batched
|
|
103
|
+
// query; a per-request `lang`/`locale` param overrides it.
|
|
104
|
+
const requestLang = getRequestLanguage(req);
|
|
101
105
|
// Dedup: group identical (endpoint, params) requests and run each group's
|
|
102
106
|
// query once via its first ("representative") request; fan the result out
|
|
103
107
|
// to the duplicate keys afterward.
|
|
@@ -285,6 +289,10 @@ export const batchEndpoints = [
|
|
|
285
289
|
};
|
|
286
290
|
}
|
|
287
291
|
}
|
|
292
|
+
const lang = (typeof params.lang === "string" && params.lang) ||
|
|
293
|
+
(typeof params.locale === "string" && params.locale) ||
|
|
294
|
+
requestLang;
|
|
295
|
+
const localeOptions = resolveLocaleOptions(payload, lang);
|
|
288
296
|
const result = await payload.find({
|
|
289
297
|
collection: collectionSlug,
|
|
290
298
|
where,
|
|
@@ -292,6 +300,7 @@ export const batchEndpoints = [
|
|
|
292
300
|
limit,
|
|
293
301
|
sort,
|
|
294
302
|
depth,
|
|
303
|
+
...localeOptions,
|
|
295
304
|
});
|
|
296
305
|
let data;
|
|
297
306
|
if (raw) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getNitrogenSettings, buildDynamicData, buildResolvedPageResponse, buildResolvedListItemResponse, getTemplateForType, requireAuth, canServeDocument, } from './helpers.js';
|
|
2
2
|
import { resolveTemplateForDocument } from './templateConditions.js';
|
|
3
|
+
import { buildDynamicDataMeta, computeDocTranslationStatus, fetchLocaleAllDoc, getDefaultLocaleTitle, getLocalizationSettings, getPayloadLocalization, getRequestLanguage, resolveLocaleOptions, } from '../localization.js';
|
|
3
4
|
/**
|
|
4
5
|
* Creates a complete set of CRUD endpoints for a given Payload collection.
|
|
5
6
|
*
|
|
@@ -19,6 +20,7 @@ export function createCollectionEndpoints(collectionSlug, endpointPrefix) {
|
|
|
19
20
|
const url = new URL(req.url || '', 'http://localhost');
|
|
20
21
|
const statusParam = url.searchParams.get('post_status') || 'published';
|
|
21
22
|
const statuses = statusParam.split(',').map((s) => s.trim());
|
|
23
|
+
const localeOptions = resolveLocaleOptions(payload, getRequestLanguage(req));
|
|
22
24
|
const where = {};
|
|
23
25
|
if (!statuses.includes('any')) {
|
|
24
26
|
where._status = { in: statuses };
|
|
@@ -28,6 +30,7 @@ export function createCollectionEndpoints(collectionSlug, endpointPrefix) {
|
|
|
28
30
|
where,
|
|
29
31
|
limit: 0,
|
|
30
32
|
depth: 1,
|
|
33
|
+
...localeOptions,
|
|
31
34
|
});
|
|
32
35
|
const items = await Promise.all(result.docs.map((doc) => buildResolvedListItemResponse(payload, doc, settings, collectionSlug)));
|
|
33
36
|
return Response.json(items);
|
|
@@ -67,9 +70,10 @@ export function createCollectionEndpoints(collectionSlug, endpointPrefix) {
|
|
|
67
70
|
handler: async (req) => {
|
|
68
71
|
const { payload, routeParams } = req;
|
|
69
72
|
const id = routeParams?.id;
|
|
73
|
+
const localeOptions = resolveLocaleOptions(payload, getRequestLanguage(req));
|
|
70
74
|
try {
|
|
71
75
|
const [doc, settings, headerTemplate, footerTemplate] = await Promise.all([
|
|
72
|
-
payload.findByID({ collection, id, depth: 1 }),
|
|
76
|
+
payload.findByID({ collection, id, depth: 1, ...localeOptions }),
|
|
73
77
|
getNitrogenSettings(payload),
|
|
74
78
|
getTemplateForType(payload, 'header'),
|
|
75
79
|
getTemplateForType(payload, 'footer'),
|
|
@@ -79,9 +83,22 @@ export function createCollectionEndpoints(collectionSlug, endpointPrefix) {
|
|
|
79
83
|
}
|
|
80
84
|
const pageTemplate = await resolveTemplateForDocument(payload, doc, collectionSlug, settings);
|
|
81
85
|
const dynamicData = buildDynamicData(doc, settings);
|
|
86
|
+
// When a language was requested, the top-level `title` must stay
|
|
87
|
+
// the default-locale one — the editor edits default-language field
|
|
88
|
+
// data only and PATCHes `title` back on save. Only dynamic_data is
|
|
89
|
+
// locale-resolved. One locale:'all' fetch serves title + meta.
|
|
90
|
+
const localeAllDoc = localeOptions.locale
|
|
91
|
+
? await fetchLocaleAllDoc(payload, collectionSlug, id)
|
|
92
|
+
: undefined;
|
|
93
|
+
const dynamicDataMeta = await buildDynamicDataMeta(payload, collectionSlug, id, localeAllDoc);
|
|
94
|
+
const titleOverride = localeOptions.locale
|
|
95
|
+
? getDefaultLocaleTitle(payload, localeAllDoc ?? null)
|
|
96
|
+
: null;
|
|
82
97
|
const response = await buildResolvedPageResponse(payload, doc, settings, dynamicData, collectionSlug);
|
|
83
98
|
return Response.json({
|
|
84
99
|
...response,
|
|
100
|
+
...(titleOverride !== null ? { title: titleOverride } : {}),
|
|
101
|
+
...(dynamicDataMeta ? { dynamic_data_meta: dynamicDataMeta } : {}),
|
|
85
102
|
template: pageTemplate,
|
|
86
103
|
headerTemplate,
|
|
87
104
|
footerTemplate,
|
|
@@ -92,6 +109,36 @@ export function createCollectionEndpoints(collectionSlug, endpointPrefix) {
|
|
|
92
109
|
}
|
|
93
110
|
},
|
|
94
111
|
},
|
|
112
|
+
// GET /api/nitrogen/v1/{prefix}/:id/dynamic-data — Dynamic data (+ meta)
|
|
113
|
+
// for one item, optionally in a specific language (?lang=es). Used by the
|
|
114
|
+
// editor on language switch, without reloading the whole page.
|
|
115
|
+
{
|
|
116
|
+
path: `/nitrogen/v1/${endpointPrefix}/:id/dynamic-data`,
|
|
117
|
+
method: 'get',
|
|
118
|
+
handler: async (req) => {
|
|
119
|
+
const { payload, routeParams } = req;
|
|
120
|
+
const id = routeParams?.id;
|
|
121
|
+
const localeOptions = resolveLocaleOptions(payload, getRequestLanguage(req));
|
|
122
|
+
try {
|
|
123
|
+
const [doc, settings] = await Promise.all([
|
|
124
|
+
payload.findByID({ collection, id, depth: 1, ...localeOptions }),
|
|
125
|
+
getNitrogenSettings(payload),
|
|
126
|
+
]);
|
|
127
|
+
if (!canServeDocument(req, doc, settings)) {
|
|
128
|
+
return Response.json({ error: 'Not found' }, { status: 404 });
|
|
129
|
+
}
|
|
130
|
+
const dynamicData = buildDynamicData(doc, settings);
|
|
131
|
+
const dynamicDataMeta = await buildDynamicDataMeta(payload, collectionSlug, id);
|
|
132
|
+
return Response.json({
|
|
133
|
+
dynamic_data: dynamicData,
|
|
134
|
+
...(dynamicDataMeta ? { dynamic_data_meta: dynamicDataMeta } : {}),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return Response.json({ error: 'Not found' }, { status: 404 });
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
},
|
|
95
142
|
// PATCH /api/nitrogen/v1/{prefix}/:id — Update an item
|
|
96
143
|
{
|
|
97
144
|
path: `/nitrogen/v1/${endpointPrefix}/:id`,
|
|
@@ -145,6 +192,34 @@ export function createCollectionEndpoints(collectionSlug, endpointPrefix) {
|
|
|
145
192
|
if (data.settings !== undefined) {
|
|
146
193
|
updateData.pageSettings = data.settings;
|
|
147
194
|
}
|
|
195
|
+
// Localization enabled → recompute per-language translation status
|
|
196
|
+
// from the saved module tree. Advisory only — never blocks the save.
|
|
197
|
+
if (updateData.nitrogenData !== undefined) {
|
|
198
|
+
try {
|
|
199
|
+
const settings = await getNitrogenSettings(payload);
|
|
200
|
+
const localization = getLocalizationSettings(settings);
|
|
201
|
+
if (localization) {
|
|
202
|
+
const translationStatus = await computeDocTranslationStatus({
|
|
203
|
+
payload,
|
|
204
|
+
collectionSlug,
|
|
205
|
+
docId: id,
|
|
206
|
+
nitrogenData: updateData.nitrogenData,
|
|
207
|
+
localization,
|
|
208
|
+
});
|
|
209
|
+
if (translationStatus) {
|
|
210
|
+
updateData.nitrogenTranslationStatus = translationStatus;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Status is advisory — never block the save.
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// The editor edits default-language field data only — pin the write
|
|
219
|
+
// to the default locale. Without this, Payload's createLocalReq falls
|
|
220
|
+
// back to req.locale / req.query.locale, so a stray `?locale=` on the
|
|
221
|
+
// request would silently retarget localized fields (title).
|
|
222
|
+
const payloadLocalization = getPayloadLocalization(payload);
|
|
148
223
|
try {
|
|
149
224
|
await payload.update({
|
|
150
225
|
collection,
|
|
@@ -152,6 +227,9 @@ export function createCollectionEndpoints(collectionSlug, endpointPrefix) {
|
|
|
152
227
|
data: updateData,
|
|
153
228
|
depth: 0,
|
|
154
229
|
overrideAccess: true,
|
|
230
|
+
...(payloadLocalization
|
|
231
|
+
? { locale: payloadLocalization.defaultLocale }
|
|
232
|
+
: {}),
|
|
155
233
|
req,
|
|
156
234
|
});
|
|
157
235
|
}
|
|
@@ -183,8 +261,16 @@ export function createCollectionEndpoints(collectionSlug, endpointPrefix) {
|
|
|
183
261
|
if (!slug) {
|
|
184
262
|
return Response.json({ error: 'Slug is required' }, { status: 400 });
|
|
185
263
|
}
|
|
264
|
+
const lang = body?.lang || body?.data?.lang || getRequestLanguage(req);
|
|
265
|
+
const localeOptions = resolveLocaleOptions(payload, lang);
|
|
186
266
|
const [result, settings, headerTemplate, footerTemplate] = await Promise.all([
|
|
187
|
-
payload.find({
|
|
267
|
+
payload.find({
|
|
268
|
+
collection,
|
|
269
|
+
where: { slug: { equals: slug } },
|
|
270
|
+
limit: 1,
|
|
271
|
+
depth: 1,
|
|
272
|
+
...localeOptions,
|
|
273
|
+
}),
|
|
188
274
|
getNitrogenSettings(payload),
|
|
189
275
|
getTemplateForType(payload, 'header'),
|
|
190
276
|
getTemplateForType(payload, 'footer'),
|
|
@@ -198,9 +284,19 @@ export function createCollectionEndpoints(collectionSlug, endpointPrefix) {
|
|
|
198
284
|
}
|
|
199
285
|
const pageTemplate = await resolveTemplateForDocument(payload, doc, collectionSlug, settings);
|
|
200
286
|
const dynamicData = buildDynamicData(doc, settings);
|
|
287
|
+
// See GET /:id — keep the round-tripped `title` at the default locale.
|
|
288
|
+
const localeAllDoc = localeOptions.locale
|
|
289
|
+
? await fetchLocaleAllDoc(payload, collectionSlug, doc.id)
|
|
290
|
+
: undefined;
|
|
291
|
+
const dynamicDataMeta = await buildDynamicDataMeta(payload, collectionSlug, doc.id, localeAllDoc);
|
|
292
|
+
const titleOverride = localeOptions.locale
|
|
293
|
+
? getDefaultLocaleTitle(payload, localeAllDoc ?? null)
|
|
294
|
+
: null;
|
|
201
295
|
const response = await buildResolvedPageResponse(payload, doc, settings, dynamicData, collectionSlug);
|
|
202
296
|
return Response.json({
|
|
203
297
|
...response,
|
|
298
|
+
...(titleOverride !== null ? { title: titleOverride } : {}),
|
|
299
|
+
...(dynamicDataMeta ? { dynamic_data_meta: dynamicDataMeta } : {}),
|
|
204
300
|
template: pageTemplate,
|
|
205
301
|
headerTemplate,
|
|
206
302
|
footerTemplate,
|
|
@@ -214,8 +310,18 @@ export function createCollectionEndpoints(collectionSlug, endpointPrefix) {
|
|
|
214
310
|
handler: async (req) => {
|
|
215
311
|
const { payload, routeParams } = req;
|
|
216
312
|
const slug = routeParams?.slug;
|
|
313
|
+
// When the host collection's slug field is localized, the locale-aware
|
|
314
|
+
// query matches the localized slug (falling back to the default
|
|
315
|
+
// locale's slug via fallbackLocale).
|
|
316
|
+
const localeOptions = resolveLocaleOptions(payload, getRequestLanguage(req));
|
|
217
317
|
const [result, settings, headerTemplate, footerTemplate] = await Promise.all([
|
|
218
|
-
payload.find({
|
|
318
|
+
payload.find({
|
|
319
|
+
collection,
|
|
320
|
+
where: { slug: { equals: slug } },
|
|
321
|
+
limit: 1,
|
|
322
|
+
depth: 1,
|
|
323
|
+
...localeOptions,
|
|
324
|
+
}),
|
|
219
325
|
getNitrogenSettings(payload),
|
|
220
326
|
getTemplateForType(payload, 'header'),
|
|
221
327
|
getTemplateForType(payload, 'footer'),
|
|
@@ -229,9 +335,19 @@ export function createCollectionEndpoints(collectionSlug, endpointPrefix) {
|
|
|
229
335
|
}
|
|
230
336
|
const pageTemplate = await resolveTemplateForDocument(payload, doc, collectionSlug, settings);
|
|
231
337
|
const dynamicData = buildDynamicData(doc, settings);
|
|
338
|
+
// See GET /:id — keep the round-tripped `title` at the default locale.
|
|
339
|
+
const localeAllDoc = localeOptions.locale
|
|
340
|
+
? await fetchLocaleAllDoc(payload, collectionSlug, doc.id)
|
|
341
|
+
: undefined;
|
|
342
|
+
const dynamicDataMeta = await buildDynamicDataMeta(payload, collectionSlug, doc.id, localeAllDoc);
|
|
343
|
+
const titleOverride = localeOptions.locale
|
|
344
|
+
? getDefaultLocaleTitle(payload, localeAllDoc ?? null)
|
|
345
|
+
: null;
|
|
232
346
|
const response = await buildResolvedPageResponse(payload, doc, settings, dynamicData, collectionSlug);
|
|
233
347
|
return Response.json({
|
|
234
348
|
...response,
|
|
349
|
+
...(titleOverride !== null ? { title: titleOverride } : {}),
|
|
350
|
+
...(dynamicDataMeta ? { dynamic_data_meta: dynamicDataMeta } : {}),
|
|
235
351
|
template: pageTemplate,
|
|
236
352
|
headerTemplate,
|
|
237
353
|
footerTemplate,
|
|
@@ -106,6 +106,35 @@ function resolveMediaUrl(url, settings) {
|
|
|
106
106
|
const relativeUrl = url.startsWith('/') ? url : `/${url}`;
|
|
107
107
|
return baseUrl ? `${baseUrl}${relativeUrl}` : relativeUrl;
|
|
108
108
|
}
|
|
109
|
+
const MEDIA_PATH_MARKER = 'api/media/file/';
|
|
110
|
+
// The editor's wysiwyg (TinyMCE) rewrites same-origin media URLs to relative
|
|
111
|
+
// paths on save (e.g. `../../api/media/file/x.pdf`), which 404 on the frontend
|
|
112
|
+
// host. Re-anchor anything that is only `./`/`../` hops away from the media
|
|
113
|
+
// path back to the Payload origin; other relative URLs are left untouched.
|
|
114
|
+
function resolveInlineMediaUrl(url, settings) {
|
|
115
|
+
if (/^https?:\/\//i.test(url))
|
|
116
|
+
return url;
|
|
117
|
+
const index = url.indexOf(MEDIA_PATH_MARKER);
|
|
118
|
+
if (index === -1)
|
|
119
|
+
return url;
|
|
120
|
+
const prefix = url.slice(0, index);
|
|
121
|
+
if (!/^[./]*$/.test(prefix))
|
|
122
|
+
return url;
|
|
123
|
+
return resolveMediaUrl(`/${url.slice(index)}`, settings);
|
|
124
|
+
}
|
|
125
|
+
function resolveInlineMediaUrls(value, settings) {
|
|
126
|
+
if (!value.includes(MEDIA_PATH_MARKER))
|
|
127
|
+
return value;
|
|
128
|
+
if (!value.includes('<')) {
|
|
129
|
+
const trimmed = value.trim();
|
|
130
|
+
const resolved = resolveInlineMediaUrl(trimmed, settings);
|
|
131
|
+
return resolved === trimmed ? value : resolved;
|
|
132
|
+
}
|
|
133
|
+
return value.replace(/((?:href|src)=)(["'])([^"']*)\2/gi, (match, attr, quote, url) => {
|
|
134
|
+
const resolved = resolveInlineMediaUrl(url, settings);
|
|
135
|
+
return resolved === url ? match : `${attr}${quote}${resolved}${quote}`;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
109
138
|
async function findInternalTarget(context, link, internalRef) {
|
|
110
139
|
const entryId = getId(internalRef.entryId);
|
|
111
140
|
if (entryId === null)
|
|
@@ -205,13 +234,25 @@ async function resolveLinkRecord(context, link) {
|
|
|
205
234
|
}
|
|
206
235
|
async function walk(context, value) {
|
|
207
236
|
if (Array.isArray(value)) {
|
|
208
|
-
await Promise.all(value.map((item) =>
|
|
237
|
+
await Promise.all(value.map(async (item, index) => {
|
|
238
|
+
if (typeof item === 'string') {
|
|
239
|
+
value[index] = resolveInlineMediaUrls(item, context.settings);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
await walk(context, item);
|
|
243
|
+
}));
|
|
209
244
|
return;
|
|
210
245
|
}
|
|
211
246
|
if (!isRecord(value))
|
|
212
247
|
return;
|
|
213
248
|
await resolveLinkRecord(context, value);
|
|
214
|
-
await Promise.all(Object.
|
|
249
|
+
await Promise.all(Object.entries(value).map(async ([key, item]) => {
|
|
250
|
+
if (typeof item === 'string') {
|
|
251
|
+
value[key] = resolveInlineMediaUrls(item, context.settings);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
await walk(context, item);
|
|
255
|
+
}));
|
|
215
256
|
}
|
|
216
257
|
export async function resolveCtrlLinksInNitrogenData(payload, value, settings) {
|
|
217
258
|
if (!value)
|
|
@@ -30,7 +30,8 @@ export function buildDynamicData(doc, globalSettings) {
|
|
|
30
30
|
const rawDoc = doc;
|
|
31
31
|
const docData = Object.fromEntries(Object.entries(rawDoc).filter(([key]) => key !== 'nitrogenData' &&
|
|
32
32
|
key !== 'pageSettings' &&
|
|
33
|
-
key !== 'templateSettings'
|
|
33
|
+
key !== 'templateSettings' &&
|
|
34
|
+
key !== 'nitrogenTranslationStatus'));
|
|
34
35
|
const rawContent = typeof rawDoc.content === 'string'
|
|
35
36
|
? rawDoc.content
|
|
36
37
|
: '';
|
package/dist/endpoints/menu.js
CHANGED
|
@@ -2,6 +2,8 @@ export const menuEndpoints = [
|
|
|
2
2
|
// POST /api/nitrogen/v1/menu — Get menu items (stub)
|
|
3
3
|
// WordPress nav menus are WP-specific. Returns empty array.
|
|
4
4
|
// Implement with a NavigationMenus collection if menus are needed.
|
|
5
|
+
// `?lang=` is accepted (and trivially honored) — thread it through
|
|
6
|
+
// resolveLocaleOptions if this stub gains a real menu query.
|
|
5
7
|
{
|
|
6
8
|
path: '/nitrogen/v1/menu',
|
|
7
9
|
method: 'post',
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { buildRelativePermalink, getRegisteredCollections, resolveCollection, } from '../collection-registry.js';
|
|
14
14
|
import { findAllDocs } from '../inventory/indexing.js';
|
|
15
|
+
import { getPayloadLocalization, } from '../localization.js';
|
|
15
16
|
/** Internal Nitrogen collections that must never appear in the sitemap. */
|
|
16
17
|
const EXCLUDED_COLLECTIONS = new Set([
|
|
17
18
|
'nitrogen-templates',
|
|
@@ -24,18 +25,30 @@ const EXCLUDED_COLLECTIONS = new Set([
|
|
|
24
25
|
* Payload collections may or may not have drafts enabled (the `_status`
|
|
25
26
|
* field). We first try filtering by `_status: 'published'`; if that throws
|
|
26
27
|
* (collection has no `_status`), we fall back to fetching all docs.
|
|
28
|
+
*
|
|
29
|
+
* With localization configured, docs are fetched at `locale: 'all'` so a
|
|
30
|
+
* localized slug field yields every locale's slug in one query.
|
|
27
31
|
*/
|
|
28
|
-
async function fetchPublishedSitemapDocs(payload, collection) {
|
|
32
|
+
async function fetchPublishedSitemapDocs(payload, collection, localization) {
|
|
29
33
|
const select = { slug: true, updatedAt: true };
|
|
34
|
+
const locale = localization ? 'all' : undefined;
|
|
30
35
|
try {
|
|
31
36
|
return await findAllDocs(payload, collection, {
|
|
32
37
|
where: { _status: { equals: 'published' } },
|
|
33
38
|
select,
|
|
39
|
+
locale,
|
|
34
40
|
});
|
|
35
41
|
}
|
|
36
42
|
catch {
|
|
37
|
-
return findAllDocs(payload, collection, { select });
|
|
43
|
+
return findAllDocs(payload, collection, { select, locale });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Resolve a (possibly locale-keyed) slug value for one locale. */
|
|
47
|
+
function slugForLocale(slug, locale, defaultLocale) {
|
|
48
|
+
if (slug && typeof slug === 'object' && !Array.isArray(slug)) {
|
|
49
|
+
return String(slug[locale] ?? slug[defaultLocale] ?? '');
|
|
38
50
|
}
|
|
51
|
+
return String(slug ?? '');
|
|
39
52
|
}
|
|
40
53
|
/** Coerce a doc's updatedAt into an ISO-8601 string, or null when absent. */
|
|
41
54
|
function toIsoString(value) {
|
|
@@ -89,17 +102,44 @@ export const sitemapEndpoints = [
|
|
|
89
102
|
if (EXCLUDED_COLLECTIONS.has(collection)) {
|
|
90
103
|
return Response.json({ error: 'Post type not eligible for sitemap' }, { status: 404 });
|
|
91
104
|
}
|
|
105
|
+
const localization = getPayloadLocalization(payload);
|
|
92
106
|
let docs;
|
|
93
107
|
try {
|
|
94
|
-
docs = await fetchPublishedSitemapDocs(payload, collection);
|
|
108
|
+
docs = await fetchPublishedSitemapDocs(payload, collection, localization);
|
|
95
109
|
}
|
|
96
110
|
catch {
|
|
97
111
|
return Response.json({ error: 'No URLs for this post type' }, { status: 404 });
|
|
98
112
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
113
|
+
// With localization configured, each doc emits its canonical
|
|
114
|
+
// default-locale URL plus one `/{lang}/`-prefixed entry per non-default
|
|
115
|
+
// locale, using the localized slug when present (falling back to the
|
|
116
|
+
// default locale's slug otherwise).
|
|
117
|
+
const urls = docs.flatMap((doc) => {
|
|
118
|
+
const lastmod = toIsoString(doc.updatedAt);
|
|
119
|
+
if (!localization) {
|
|
120
|
+
return [
|
|
121
|
+
{
|
|
122
|
+
loc: buildRelativePermalink(postType, String(doc.slug || '')),
|
|
123
|
+
lastmod,
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
const { locales, defaultLocale } = localization;
|
|
128
|
+
const defaultSlug = slugForLocale(doc.slug, defaultLocale, defaultLocale);
|
|
129
|
+
const entries = [
|
|
130
|
+
{ loc: buildRelativePermalink(postType, defaultSlug), lastmod },
|
|
131
|
+
];
|
|
132
|
+
for (const locale of locales) {
|
|
133
|
+
if (locale === defaultLocale)
|
|
134
|
+
continue;
|
|
135
|
+
const localizedSlug = slugForLocale(doc.slug, locale, defaultLocale);
|
|
136
|
+
entries.push({
|
|
137
|
+
loc: `/${locale}${buildRelativePermalink(postType, localizedSlug)}`,
|
|
138
|
+
lastmod,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return entries;
|
|
142
|
+
});
|
|
103
143
|
if (urls.length === 0) {
|
|
104
144
|
return Response.json({ error: 'No URLs for this post type' }, { status: 404 });
|
|
105
145
|
}
|