@nitrogenbuilder/connector-payload 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/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,
|
|
@@ -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
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getNitrogenSettings, buildDynamicData, buildResolvedPageResponse, buildResolvedListItemResponse, getDocumentPermalink, getDocumentRelativePermalink, getTemplateForType, requireAuth, } from './helpers.js';
|
|
2
|
+
import { buildDynamicDataMeta, computeDocTranslationStatus, fetchLocaleAllDoc, getDefaultLocaleTitle, getLocalizationSettings, getPayloadLocalization, getRequestLanguage, resolveLocaleOptions, } from '../localization.js';
|
|
2
3
|
function isContentTemplateCollection(collection) {
|
|
3
4
|
return !!collection && collection !== 'header' && collection !== 'footer';
|
|
4
5
|
}
|
|
@@ -25,6 +26,7 @@ export const templatesEndpoints = [
|
|
|
25
26
|
handler: async (req) => {
|
|
26
27
|
const { payload, routeParams } = req;
|
|
27
28
|
const slug = routeParams?.slug;
|
|
29
|
+
const localeOptions = resolveLocaleOptions(payload, getRequestLanguage(req));
|
|
28
30
|
try {
|
|
29
31
|
const [result, headerTemplate, footerTemplate] = await Promise.all([
|
|
30
32
|
payload.find({
|
|
@@ -32,6 +34,7 @@ export const templatesEndpoints = [
|
|
|
32
34
|
where: { slug: { equals: slug } },
|
|
33
35
|
limit: 1,
|
|
34
36
|
depth: 1,
|
|
37
|
+
...localeOptions,
|
|
35
38
|
}),
|
|
36
39
|
getTemplateForType(payload, 'header'),
|
|
37
40
|
getTemplateForType(payload, 'footer'),
|
|
@@ -50,6 +53,7 @@ export const templatesEndpoints = [
|
|
|
50
53
|
collection: associatedCollection,
|
|
51
54
|
limit: 1,
|
|
52
55
|
depth: 1,
|
|
56
|
+
...localeOptions,
|
|
53
57
|
});
|
|
54
58
|
if (associated.docs.length) {
|
|
55
59
|
associatedDoc = associated.docs[0];
|
|
@@ -61,9 +65,24 @@ export const templatesEndpoints = [
|
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
67
|
const response = await buildResolvedPageResponse(payload, doc, settings, dynamicData);
|
|
68
|
+
// When a language was requested, the top-level `title` must stay the
|
|
69
|
+
// default-locale one — the editor edits default-language field data
|
|
70
|
+
// only and PATCHes `title` back on save. The meta may describe the
|
|
71
|
+
// associated doc, but the round-tripped title is the template's own.
|
|
72
|
+
const templateAllDoc = localeOptions.locale
|
|
73
|
+
? await fetchLocaleAllDoc(payload, 'nitrogen-templates', doc.id)
|
|
74
|
+
: undefined;
|
|
75
|
+
const dynamicDataMeta = associatedDoc
|
|
76
|
+
? await buildDynamicDataMeta(payload, associatedCollection, associatedDoc.id)
|
|
77
|
+
: await buildDynamicDataMeta(payload, 'nitrogen-templates', doc.id, templateAllDoc);
|
|
78
|
+
const titleOverride = localeOptions.locale
|
|
79
|
+
? getDefaultLocaleTitle(payload, templateAllDoc ?? null)
|
|
80
|
+
: null;
|
|
64
81
|
const previewTarget = getTemplatePreviewTarget(doc, settings, associatedCollection, associatedDoc);
|
|
65
82
|
return Response.json({
|
|
66
83
|
...response,
|
|
84
|
+
...(titleOverride !== null ? { title: titleOverride } : {}),
|
|
85
|
+
...(dynamicDataMeta ? { dynamic_data_meta: dynamicDataMeta } : {}),
|
|
67
86
|
slug: previewTarget.slug,
|
|
68
87
|
permalink: previewTarget.permalink,
|
|
69
88
|
relative_permalink: previewTarget.relative_permalink,
|
|
@@ -87,6 +106,7 @@ export const templatesEndpoints = [
|
|
|
87
106
|
const settings = await getNitrogenSettings(payload);
|
|
88
107
|
const url = new URL(req.url || '', 'http://localhost');
|
|
89
108
|
const templateType = url.searchParams.get('templateType');
|
|
109
|
+
const localeOptions = resolveLocaleOptions(payload, getRequestLanguage(req));
|
|
90
110
|
const where = {
|
|
91
111
|
status: { equals: 'published' },
|
|
92
112
|
};
|
|
@@ -98,6 +118,7 @@ export const templatesEndpoints = [
|
|
|
98
118
|
where,
|
|
99
119
|
limit: 0,
|
|
100
120
|
depth: 1,
|
|
121
|
+
...localeOptions,
|
|
101
122
|
});
|
|
102
123
|
const items = await Promise.all(result.docs.map(async (doc) => {
|
|
103
124
|
const response = await buildResolvedListItemResponse(payload, doc, settings);
|
|
@@ -118,12 +139,14 @@ export const templatesEndpoints = [
|
|
|
118
139
|
handler: async (req) => {
|
|
119
140
|
const { payload, routeParams } = req;
|
|
120
141
|
const id = routeParams?.id;
|
|
142
|
+
const localeOptions = resolveLocaleOptions(payload, getRequestLanguage(req));
|
|
121
143
|
try {
|
|
122
144
|
const [doc, headerTemplate, footerTemplate] = await Promise.all([
|
|
123
145
|
payload.findByID({
|
|
124
146
|
collection: 'nitrogen-templates',
|
|
125
147
|
id,
|
|
126
148
|
depth: 1,
|
|
149
|
+
...localeOptions,
|
|
127
150
|
}),
|
|
128
151
|
getTemplateForType(payload, 'header'),
|
|
129
152
|
getTemplateForType(payload, 'footer'),
|
|
@@ -140,6 +163,7 @@ export const templatesEndpoints = [
|
|
|
140
163
|
collection: associatedCollection,
|
|
141
164
|
limit: 1,
|
|
142
165
|
depth: 1,
|
|
166
|
+
...localeOptions,
|
|
143
167
|
});
|
|
144
168
|
if (associated.docs.length) {
|
|
145
169
|
associatedDoc = associated.docs[0];
|
|
@@ -151,9 +175,22 @@ export const templatesEndpoints = [
|
|
|
151
175
|
}
|
|
152
176
|
}
|
|
153
177
|
const response = await buildResolvedPageResponse(payload, doc, settings, dynamicData);
|
|
178
|
+
// See GET slug/:slug — keep the round-tripped `title` at the default
|
|
179
|
+
// locale.
|
|
180
|
+
const templateAllDoc = localeOptions.locale
|
|
181
|
+
? await fetchLocaleAllDoc(payload, 'nitrogen-templates', doc.id)
|
|
182
|
+
: undefined;
|
|
183
|
+
const dynamicDataMeta = associatedDoc
|
|
184
|
+
? await buildDynamicDataMeta(payload, associatedCollection, associatedDoc.id)
|
|
185
|
+
: await buildDynamicDataMeta(payload, 'nitrogen-templates', doc.id, templateAllDoc);
|
|
186
|
+
const titleOverride = localeOptions.locale
|
|
187
|
+
? getDefaultLocaleTitle(payload, templateAllDoc ?? null)
|
|
188
|
+
: null;
|
|
154
189
|
const previewTarget = getTemplatePreviewTarget(doc, settings, associatedCollection, associatedDoc);
|
|
155
190
|
return Response.json({
|
|
156
191
|
...response,
|
|
192
|
+
...(titleOverride !== null ? { title: titleOverride } : {}),
|
|
193
|
+
...(dynamicDataMeta ? { dynamic_data_meta: dynamicDataMeta } : {}),
|
|
157
194
|
slug: previewTarget.slug,
|
|
158
195
|
permalink: previewTarget.permalink,
|
|
159
196
|
relative_permalink: previewTarget.relative_permalink,
|
|
@@ -168,6 +205,56 @@ export const templatesEndpoints = [
|
|
|
168
205
|
}
|
|
169
206
|
},
|
|
170
207
|
},
|
|
208
|
+
// GET /api/nitrogen/v1/nitrogen-templates/:id/dynamic-data — Dynamic data
|
|
209
|
+
// (+ meta) for a template, optionally in a specific language (?lang=es).
|
|
210
|
+
{
|
|
211
|
+
path: '/nitrogen/v1/nitrogen-templates/:id/dynamic-data',
|
|
212
|
+
method: 'get',
|
|
213
|
+
handler: async (req) => {
|
|
214
|
+
const { payload, routeParams } = req;
|
|
215
|
+
const id = routeParams?.id;
|
|
216
|
+
const localeOptions = resolveLocaleOptions(payload, getRequestLanguage(req));
|
|
217
|
+
try {
|
|
218
|
+
const doc = (await payload.findByID({
|
|
219
|
+
collection: 'nitrogen-templates',
|
|
220
|
+
id,
|
|
221
|
+
depth: 1,
|
|
222
|
+
...localeOptions,
|
|
223
|
+
}));
|
|
224
|
+
const settings = await getNitrogenSettings(payload);
|
|
225
|
+
let dynamicData = buildDynamicData(doc, settings);
|
|
226
|
+
let associatedDoc;
|
|
227
|
+
const associatedCollection = doc.associatedCollection;
|
|
228
|
+
if (isContentTemplateCollection(associatedCollection)) {
|
|
229
|
+
try {
|
|
230
|
+
const associated = await payload.find({
|
|
231
|
+
collection: associatedCollection,
|
|
232
|
+
limit: 1,
|
|
233
|
+
depth: 1,
|
|
234
|
+
...localeOptions,
|
|
235
|
+
});
|
|
236
|
+
if (associated.docs.length) {
|
|
237
|
+
associatedDoc = associated.docs[0];
|
|
238
|
+
dynamicData = buildDynamicData(associatedDoc, settings);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// Collection may not exist — use template's own data
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const dynamicDataMeta = associatedDoc
|
|
246
|
+
? await buildDynamicDataMeta(payload, associatedCollection, associatedDoc.id)
|
|
247
|
+
: await buildDynamicDataMeta(payload, 'nitrogen-templates', doc.id);
|
|
248
|
+
return Response.json({
|
|
249
|
+
dynamic_data: dynamicData,
|
|
250
|
+
...(dynamicDataMeta ? { dynamic_data_meta: dynamicDataMeta } : {}),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return Response.json({ error: 'Not found' }, { status: 404 });
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
},
|
|
171
258
|
// PATCH /api/nitrogen/v1/templates/:id — Update a template
|
|
172
259
|
{
|
|
173
260
|
path: '/nitrogen/v1/nitrogen-templates/:id',
|
|
@@ -206,10 +293,39 @@ export const templatesEndpoints = [
|
|
|
206
293
|
if (Object.keys(updateData).length === 0) {
|
|
207
294
|
return Response.json({ error: 'No update data provided' }, { status: 400 });
|
|
208
295
|
}
|
|
296
|
+
// Localization enabled → recompute per-language translation status from
|
|
297
|
+
// the saved module tree. Advisory only — never blocks the save.
|
|
298
|
+
if (updateData.nitrogenData !== undefined) {
|
|
299
|
+
try {
|
|
300
|
+
const settings = await getNitrogenSettings(payload);
|
|
301
|
+
const localization = getLocalizationSettings(settings);
|
|
302
|
+
if (localization) {
|
|
303
|
+
const translationStatus = await computeDocTranslationStatus({
|
|
304
|
+
payload,
|
|
305
|
+
collectionSlug: 'nitrogen-templates',
|
|
306
|
+
docId: id,
|
|
307
|
+
nitrogenData: updateData.nitrogenData,
|
|
308
|
+
localization,
|
|
309
|
+
});
|
|
310
|
+
if (translationStatus) {
|
|
311
|
+
updateData.nitrogenTranslationStatus = translationStatus;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
// Status is advisory — never block the save.
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// The editor edits default-language field data only — pin the write to
|
|
320
|
+
// the default locale so it can never target a request-derived locale.
|
|
321
|
+
const payloadLocalization = getPayloadLocalization(payload);
|
|
209
322
|
await payload.update({
|
|
210
323
|
collection: 'nitrogen-templates',
|
|
211
324
|
id,
|
|
212
325
|
data: updateData,
|
|
326
|
+
...(payloadLocalization
|
|
327
|
+
? { locale: payloadLocalization.defaultLocale }
|
|
328
|
+
: {}),
|
|
213
329
|
});
|
|
214
330
|
return Response.json({ success: true });
|
|
215
331
|
},
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation status route — lists per-language translation coverage for
|
|
3
|
+
* every Nitrogen-enabled document and template, for the editor's translation
|
|
4
|
+
* dashboard.
|
|
5
|
+
*
|
|
6
|
+
* The status is computed and stored (in `nitrogenTranslationStatus`) by the
|
|
7
|
+
* save handlers; this route reads the stored value and only computes on the
|
|
8
|
+
* fly for docs that have never been saved since localization was enabled.
|
|
9
|
+
*/
|
|
10
|
+
import type { Endpoint } from 'payload';
|
|
11
|
+
export declare function createTranslationStatusEndpoints(collections: string[]): Endpoint[];
|