meno-core 1.0.48 → 1.0.50

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.
Files changed (89) hide show
  1. package/build-astro.ts +6 -2
  2. package/dist/build-static.js +7 -7
  3. package/dist/chunks/{chunk-D5E3OKSL.js → chunk-56EUSC6D.js} +5 -5
  4. package/dist/chunks/{chunk-3FHJUHAS.js → chunk-7NIC4I3V.js} +300 -43
  5. package/dist/chunks/chunk-7NIC4I3V.js.map +7 -0
  6. package/dist/chunks/{chunk-B2RTLDXY.js → chunk-AZQYF6KE.js} +132 -1
  7. package/dist/chunks/chunk-AZQYF6KE.js.map +7 -0
  8. package/dist/chunks/{chunk-TPQ7APVQ.js → chunk-CVLFID6V.js} +473 -73
  9. package/dist/chunks/chunk-CVLFID6V.js.map +7 -0
  10. package/dist/chunks/{chunk-NP76N4HQ.js → chunk-EDQSMAMP.js} +13 -2
  11. package/dist/chunks/{chunk-NP76N4HQ.js.map → chunk-EDQSMAMP.js.map} +2 -2
  12. package/dist/chunks/{chunk-RQSTH2BS.js → chunk-H4JSCDNW.js} +2 -2
  13. package/dist/chunks/{chunk-EK4KESLU.js → chunk-J23ZX5AP.js} +8 -2
  14. package/dist/chunks/{chunk-EK4KESLU.js.map → chunk-J23ZX5AP.js.map} +2 -2
  15. package/dist/chunks/{chunk-UUA5LEWF.js → chunk-LPVETICS.js} +156 -8
  16. package/dist/chunks/chunk-LPVETICS.js.map +7 -0
  17. package/dist/chunks/{chunk-BJRKEPMP.js → chunk-PQ2HRXDR.js} +5 -2
  18. package/dist/chunks/chunk-PQ2HRXDR.js.map +7 -0
  19. package/dist/chunks/{chunk-NKUV77SR.js → chunk-YWJJD5D6.js} +133 -37
  20. package/dist/chunks/chunk-YWJJD5D6.js.map +7 -0
  21. package/dist/chunks/{configService-IGJEC3MC.js → configService-VOY2MY2K.js} +3 -3
  22. package/dist/entries/server-router.js +9 -9
  23. package/dist/entries/server-router.js.map +2 -2
  24. package/dist/lib/client/index.js +92 -32
  25. package/dist/lib/client/index.js.map +3 -3
  26. package/dist/lib/server/index.js +14 -12
  27. package/dist/lib/server/index.js.map +2 -2
  28. package/dist/lib/shared/index.js +46 -10
  29. package/dist/lib/shared/index.js.map +3 -3
  30. package/entries/server-router.tsx +6 -2
  31. package/lib/client/core/ComponentBuilder.test.ts +34 -0
  32. package/lib/client/core/ComponentBuilder.ts +33 -4
  33. package/lib/client/core/builders/embedBuilder.ts +28 -7
  34. package/lib/client/core/builders/linkNodeBuilder.ts +28 -7
  35. package/lib/client/core/builders/localeListBuilder.ts +30 -11
  36. package/lib/client/styles/StyleInjector.ts +3 -2
  37. package/lib/client/templateEngine.ts +24 -0
  38. package/lib/client/theme.ts +4 -4
  39. package/lib/server/cssGenerator.test.ts +64 -1
  40. package/lib/server/cssGenerator.ts +48 -9
  41. package/lib/server/fileWatcher.test.ts +134 -0
  42. package/lib/server/fileWatcher.ts +100 -32
  43. package/lib/server/jsonLoader.ts +1 -0
  44. package/lib/server/providers/fileSystemCMSProvider.test.ts +163 -0
  45. package/lib/server/providers/fileSystemCMSProvider.ts +240 -19
  46. package/lib/server/routes/index.ts +1 -1
  47. package/lib/server/routes/pages.ts +23 -1
  48. package/lib/server/services/cmsService.test.ts +246 -0
  49. package/lib/server/services/cmsService.ts +122 -5
  50. package/lib/server/services/configService.ts +6 -0
  51. package/lib/server/services/fileWatcherService.ts +17 -0
  52. package/lib/server/ssr/attributeBuilder.ts +41 -0
  53. package/lib/server/ssr/htmlGenerator.test.ts +113 -0
  54. package/lib/server/ssr/htmlGenerator.ts +62 -7
  55. package/lib/server/ssr/liveReloadIntegration.test.ts +209 -0
  56. package/lib/server/ssr/ssrRenderer.test.ts +564 -0
  57. package/lib/server/ssr/ssrRenderer.ts +228 -49
  58. package/lib/server/webflow/buildWebflow.ts +1 -1
  59. package/lib/server/websocketManager.test.ts +61 -6
  60. package/lib/server/websocketManager.ts +25 -1
  61. package/lib/shared/cssGeneration.test.ts +267 -1
  62. package/lib/shared/cssGeneration.ts +240 -18
  63. package/lib/shared/cssProperties.test.ts +275 -1
  64. package/lib/shared/cssProperties.ts +223 -7
  65. package/lib/shared/interfaces/contentProvider.ts +39 -6
  66. package/lib/shared/pathSecurity.ts +16 -0
  67. package/lib/shared/responsiveScaling.test.ts +143 -0
  68. package/lib/shared/responsiveScaling.ts +253 -2
  69. package/lib/shared/themeDefaults.test.ts +3 -3
  70. package/lib/shared/themeDefaults.ts +3 -3
  71. package/lib/shared/types/api.ts +10 -1
  72. package/lib/shared/types/cms.ts +46 -12
  73. package/lib/shared/types/index.ts +1 -0
  74. package/lib/shared/utilityClassConfig.ts +3 -0
  75. package/lib/shared/utilityClassMapper.test.ts +123 -0
  76. package/lib/shared/utilityClassMapper.ts +179 -8
  77. package/lib/shared/validation/schemas.test.ts +93 -0
  78. package/lib/shared/validation/schemas.ts +71 -16
  79. package/lib/shared/validation/validators.ts +26 -1
  80. package/package.json +1 -1
  81. package/dist/chunks/chunk-3FHJUHAS.js.map +0 -7
  82. package/dist/chunks/chunk-B2RTLDXY.js.map +0 -7
  83. package/dist/chunks/chunk-BJRKEPMP.js.map +0 -7
  84. package/dist/chunks/chunk-NKUV77SR.js.map +0 -7
  85. package/dist/chunks/chunk-TPQ7APVQ.js.map +0 -7
  86. package/dist/chunks/chunk-UUA5LEWF.js.map +0 -7
  87. /package/dist/chunks/{chunk-D5E3OKSL.js.map → chunk-56EUSC6D.js.map} +0 -0
  88. /package/dist/chunks/{chunk-RQSTH2BS.js.map → chunk-H4JSCDNW.js.map} +0 -0
  89. /package/dist/chunks/{configService-IGJEC3MC.js.map → configService-VOY2MY2K.js.map} +0 -0
@@ -7,10 +7,12 @@ import { existsSync, readdirSync, mkdirSync } from 'fs';
7
7
  import { join } from 'path';
8
8
  import type { CMSProvider, CMSSchemaInfo } from '../../shared/interfaces/contentProvider';
9
9
  import type { CMSSchema, CMSItem } from '../../shared/types';
10
- import { validateCMSItem } from '../../shared/validation/validators';
11
- import { isSafePathSegment, isValidIdentifier } from '../../shared/pathSecurity';
10
+ import { validateCMSItem, validateCMSDraftItem } from '../../shared/validation/validators';
11
+ import { isSafePathSegment, isValidIdentifier, isReservedDraftFilename, CMS_DRAFT_SUFFIX } from '../../shared/pathSecurity';
12
12
  import { readJsonFile, fileExists } from '../runtime';
13
13
 
14
+ const DRAFT_FILE_SUFFIX = `${CMS_DRAFT_SUFFIX}.json`;
15
+
14
16
  /**
15
17
  * Load JSON file content from disk
16
18
  * Logs a warning if JSON parsing fails (helps debug malformed files)
@@ -28,14 +30,37 @@ async function loadJSONFile(filePath: string): Promise<unknown | null> {
28
30
  }
29
31
 
30
32
  /**
31
- * Normalize a raw CMS item by adding _slug and _filename fields
33
+ * Normalize a raw CMS item by adding _slug, _filename, and _id (backfilled
34
+ * from filename for items written without one — the editor stopped writing
35
+ * a separate random _id once _filename became the canonical identifier).
36
+ * If `isDraft` is true, also sets the transient `_isDraft` flag.
32
37
  */
33
- function normalizeItem(content: unknown, filename: string): CMSItem {
34
- return {
35
- ...content as CMSItem,
38
+ function normalizeItem(content: unknown, filename: string, isDraft = false): CMSItem {
39
+ const raw = content as CMSItem;
40
+ const base: CMSItem = {
41
+ ...raw,
42
+ _id: raw._id || filename,
36
43
  _slug: filename,
37
44
  _filename: filename,
38
45
  };
46
+ if (isDraft) base._isDraft = true;
47
+ return base;
48
+ }
49
+
50
+ /**
51
+ * Strip transient fields that must never be persisted to disk.
52
+ * Also drops `_filename` when it equals `_id` (the canonical case for new
53
+ * items) so the field stops accumulating in storage. Legacy items where
54
+ * `_filename` differs from `_id` keep the field on disk so the on-disk
55
+ * file-routing path can still find them.
56
+ */
57
+ function stripTransient<T extends Record<string, unknown>>(item: T): Omit<T, '_slug' | '_isDraft' | '_hasDraft' | '_url'> {
58
+ const { _slug, _isDraft, _hasDraft, _url, _filename, ...rest } = item;
59
+ const out = rest as Omit<T, '_slug' | '_isDraft' | '_hasDraft' | '_url'>;
60
+ if (typeof _filename === 'string' && _filename !== (out as Record<string, unknown>)._id) {
61
+ (out as Record<string, unknown>)._filename = _filename;
62
+ }
63
+ return out;
39
64
  }
40
65
 
41
66
  /**
@@ -61,13 +86,16 @@ export class FileSystemCMSProvider implements CMSProvider {
61
86
  }
62
87
 
63
88
  /**
64
- * Validate filename to prevent path traversal attacks
89
+ * Validate filename to prevent path traversal attacks and reserved-suffix collisions.
65
90
  * @throws Error if filename is invalid
66
91
  */
67
92
  private validateFilename(filename: string): void {
68
93
  if (!isSafePathSegment(filename)) {
69
94
  throw new Error(`Invalid filename: "${filename}". Filenames cannot contain path separators or traversal sequences.`);
70
95
  }
96
+ if (isReservedDraftFilename(filename)) {
97
+ throw new Error(`Invalid filename: "${filename}". The "${CMS_DRAFT_SUFFIX}" suffix is reserved for draft files.`);
98
+ }
71
99
  }
72
100
 
73
101
  /**
@@ -131,7 +159,8 @@ export class FileSystemCMSProvider implements CMSProvider {
131
159
  }
132
160
 
133
161
  const files = readdirSync(collectionDir);
134
- const jsonFiles = files.filter(f => f.endsWith('.json'));
162
+ // Published items only — exclude `*.draft.json` files (draft-version siblings)
163
+ const jsonFiles = files.filter(f => f.endsWith('.json') && !f.endsWith(DRAFT_FILE_SUFFIX));
135
164
 
136
165
  const results = await Promise.all(
137
166
  jsonFiles.map(async file => {
@@ -200,8 +229,10 @@ export class FileSystemCMSProvider implements CMSProvider {
200
229
  }
201
230
 
202
231
  /**
203
- * Save item to file system
204
- * Uses _filename for file path (stable, doesn't change when slug changes)
232
+ * Save item to file system.
233
+ * The on-disk filename is derived from `_filename` (legacy alias) when set,
234
+ * otherwise from `_id` (canonical identifier; equals the filename for new
235
+ * items). Falls back to the slugField value as a last resort.
205
236
  */
206
237
  async saveItem(collection: string, item: CMSItem): Promise<void> {
207
238
  this.validateCollection(collection);
@@ -215,21 +246,31 @@ export class FileSystemCMSProvider implements CMSProvider {
215
246
  throw new Error(`Unknown collection: ${collection}`);
216
247
  }
217
248
 
218
- // Use _filename for file path (new behavior)
219
- // Falls back to slugField for backward compatibility with items without _filename
249
+ // Filename derivation precedence:
250
+ // 1. `_filename` explicit on-disk filename. Used by the editor (preserves
251
+ // legacy items where `_filename` differs from `_id`).
252
+ // 2. slug-field value — back-compat for external API callers who build
253
+ // items from raw input with a slug field.
254
+ // 3. `_id` — last resort. For new items written by the editor `_id`
255
+ // equals the filename, so this path also produces the correct name.
220
256
  let filename: string;
221
257
 
222
258
  if (item._filename) {
223
259
  filename = item._filename;
224
260
  } else {
225
- // Legacy fallback: use slug field value (may break for i18n slugs)
226
261
  const slugField = schemaInfo.schema.slugField;
227
262
  const slugValue = item[slugField];
228
- filename = typeof slugValue === 'string' ? slugValue : String(slugValue);
263
+ if (typeof slugValue === 'string' && slugValue) {
264
+ filename = slugValue;
265
+ } else if (typeof item._id === 'string' && item._id) {
266
+ filename = item._id;
267
+ } else {
268
+ filename = String(slugValue);
269
+ }
229
270
  }
230
271
 
231
272
  if (!filename || filename === '[object Object]') {
232
- throw new Error('Missing _filename field. Items must have _filename set on creation.');
273
+ throw new Error('Cannot derive filename: item is missing _id, _filename, and a usable slug-field value.');
233
274
  }
234
275
 
235
276
  // Validate filename to prevent path traversal
@@ -241,27 +282,207 @@ export class FileSystemCMSProvider implements CMSProvider {
241
282
  mkdirSync(collectionDir, { recursive: true });
242
283
  }
243
284
 
244
- // Remove internal _slug field before saving (keep _filename in file for persistence)
245
- const { _slug, ...itemData } = item;
285
+ // Strip transient fields (_slug, _isDraft, _hasDraft, _url) before persisting
286
+ const itemData = stripTransient(item as Record<string, unknown>);
246
287
 
247
288
  const filePath = join(collectionDir, `${filename}.json`);
248
289
  await writeFile(filePath, JSON.stringify(itemData, null, 2), 'utf-8');
249
290
  }
250
291
 
251
292
  /**
252
- * Delete item by filename (or slug for backward compat)
293
+ * Delete item by filename. Removes the published file AND any draft sibling.
253
294
  */
254
295
  async deleteItem(collection: string, filename: string): Promise<void> {
255
296
  this.validateCollection(collection);
256
297
  this.validateFilename(filename);
257
298
  const { unlink } = await import('fs/promises');
258
- const filePath = join(this.cmsDir, collection, `${filename}.json`);
259
299
 
300
+ const publishedPath = join(this.cmsDir, collection, `${filename}.json`);
301
+ if (existsSync(publishedPath)) {
302
+ await unlink(publishedPath);
303
+ }
304
+
305
+ const draftPath = this.draftPath(collection, filename);
306
+ if (existsSync(draftPath)) {
307
+ await unlink(draftPath);
308
+ }
309
+ }
310
+
311
+ // ---- Draft helpers ----------------------------------------------------
312
+
313
+ private draftPath(collection: string, filename: string): string {
314
+ return join(this.cmsDir, collection, `${filename}${DRAFT_FILE_SUFFIX}`);
315
+ }
316
+
317
+ /**
318
+ * Get the draft version of an item, or null if no draft file exists.
319
+ * Drafts skip strict validation — they may be partial / WIP.
320
+ */
321
+ async getDraft(collection: string, filename: string): Promise<CMSItem | null> {
322
+ this.validateCollection(collection);
323
+ this.validateFilename(filename);
324
+ const filePath = this.draftPath(collection, filename);
325
+ const content = await loadJSONFile(filePath);
326
+
327
+ if (!content || typeof content !== 'object') {
328
+ return null;
329
+ }
330
+
331
+ return normalizeItem(content, filename, /*isDraft*/ true);
332
+ }
333
+
334
+ /**
335
+ * List all drafts in a collection. Used by the Studio item list to mark
336
+ * items that have an outstanding draft sibling (or are draft-only).
337
+ */
338
+ async getAllDrafts(collection: string): Promise<CMSItem[]> {
339
+ this.validateCollection(collection);
340
+ const collectionDir = join(this.cmsDir, collection);
341
+ if (!existsSync(collectionDir)) return [];
342
+
343
+ const files = readdirSync(collectionDir).filter(f => f.endsWith(DRAFT_FILE_SUFFIX));
344
+
345
+ const results = await Promise.all(
346
+ files.map(async file => {
347
+ const filePath = join(collectionDir, file);
348
+ const content = await loadJSONFile(filePath);
349
+ return { file, content };
350
+ })
351
+ );
352
+
353
+ const drafts: CMSItem[] = [];
354
+ for (const { file, content } of results) {
355
+ if (content && typeof content === 'object') {
356
+ const filename = file.slice(0, -DRAFT_FILE_SUFFIX.length);
357
+ drafts.push(normalizeItem(content, filename, /*isDraft*/ true));
358
+ }
359
+ }
360
+ return drafts;
361
+ }
362
+
363
+ async hasDraft(collection: string, filename: string): Promise<boolean> {
364
+ this.validateCollection(collection);
365
+ this.validateFilename(filename);
366
+ return existsSync(this.draftPath(collection, filename));
367
+ }
368
+
369
+ /**
370
+ * Save the draft version of an item. Loose validation — drafts may have
371
+ * missing required fields or partial data. Strict validation only runs at
372
+ * publish time. The on-disk filename is derived the same way as for
373
+ * `saveItem` (prefer `_filename`, fall back to `_id`).
374
+ */
375
+ async saveDraft(collection: string, item: CMSItem): Promise<void> {
376
+ this.validateCollection(collection);
377
+ const { writeFile } = await import('fs/promises');
378
+
379
+ const schemas = await this.getAllSchemas();
380
+ const schemaInfo = schemas.get(collection);
381
+ if (!schemaInfo) {
382
+ throw new Error(`Unknown collection: ${collection}`);
383
+ }
384
+
385
+ // Filename derivation: same precedence as saveItem.
386
+ let filename: string;
387
+ if (item._filename) {
388
+ filename = item._filename;
389
+ } else {
390
+ const slugField = schemaInfo.schema.slugField;
391
+ const slugValue = item[slugField];
392
+ if (typeof slugValue === 'string' && slugValue) {
393
+ filename = slugValue;
394
+ } else if (typeof item._id === 'string' && item._id) {
395
+ filename = item._id;
396
+ } else {
397
+ filename = String(slugValue);
398
+ }
399
+ }
400
+
401
+ if (!filename || filename === '[object Object]') {
402
+ throw new Error('Cannot derive draft filename: item is missing _id, _filename, and a usable slug-field value.');
403
+ }
404
+
405
+ this.validateFilename(filename);
406
+
407
+ const collectionDir = join(this.cmsDir, collection);
408
+ if (!existsSync(collectionDir)) {
409
+ mkdirSync(collectionDir, { recursive: true });
410
+ }
411
+
412
+ const itemData = stripTransient(item as Record<string, unknown>);
413
+
414
+ // Loose validation — drafts may be partial WIP. Strict validation
415
+ // (validateCMSItem) runs on Publish.
416
+ const validation = validateCMSDraftItem(itemData);
417
+ if (!validation.valid) {
418
+ const messages = validation.errors.map(e => `${e.path}: ${e.message}`).join(', ');
419
+ throw new Error(`Invalid draft: ${messages}`);
420
+ }
421
+
422
+ const filePath = this.draftPath(collection, filename);
423
+ await writeFile(filePath, JSON.stringify(itemData, null, 2), 'utf-8');
424
+ }
425
+
426
+ /**
427
+ * Discard the draft version of an item. No-op if no draft exists.
428
+ */
429
+ async discardDraft(collection: string, filename: string): Promise<void> {
430
+ this.validateCollection(collection);
431
+ this.validateFilename(filename);
432
+ const { unlink } = await import('fs/promises');
433
+ const filePath = this.draftPath(collection, filename);
260
434
  if (existsSync(filePath)) {
261
435
  await unlink(filePath);
262
436
  }
263
437
  }
264
438
 
439
+ /**
440
+ * Promote a draft to published. Reads `{filename}.draft.json`, writes the
441
+ * content to `{filename}.json`, then unlinks the draft. The published write
442
+ * happens first so a crash mid-operation leaves a valid published file plus
443
+ * an orphan draft (recoverable via the editor's Discard button) — never a
444
+ * gap with no published content.
445
+ *
446
+ * Throws if no draft exists.
447
+ */
448
+ async publishDraft(collection: string, filename: string): Promise<CMSItem> {
449
+ this.validateCollection(collection);
450
+ this.validateFilename(filename);
451
+ const { writeFile, unlink } = await import('fs/promises');
452
+
453
+ const draftFilePath = this.draftPath(collection, filename);
454
+ const content = await loadJSONFile(draftFilePath);
455
+ if (!content || typeof content !== 'object') {
456
+ throw new Error(`No draft to publish for ${collection}/${filename}`);
457
+ }
458
+
459
+ // Strict validation when going live
460
+ const item = normalizeItem(content, filename);
461
+ const validation = validateCMSItem(item);
462
+ if (!validation.valid) {
463
+ const messages = validation.errors.map(e => `${e.path}: ${e.message}`).join(', ');
464
+ throw new Error(`Cannot publish invalid draft: ${messages}`);
465
+ }
466
+
467
+ const collectionDir = join(this.cmsDir, collection);
468
+ if (!existsSync(collectionDir)) {
469
+ mkdirSync(collectionDir, { recursive: true });
470
+ }
471
+
472
+ const itemData = stripTransient(validation.data as unknown as Record<string, unknown>);
473
+ const publishedPath = join(collectionDir, `${filename}.json`);
474
+ await writeFile(publishedPath, JSON.stringify(itemData, null, 2), 'utf-8');
475
+
476
+ // Published written successfully — now retire the draft. If unlink fails,
477
+ // the published copy is already in place; an orphan draft can be cleared
478
+ // via the editor's Discard action.
479
+ if (existsSync(draftFilePath)) {
480
+ await unlink(draftFilePath);
481
+ }
482
+
483
+ return normalizeItem(itemData, filename);
484
+ }
485
+
265
486
  /**
266
487
  * Clear schema cache (useful when pages are modified)
267
488
  */
@@ -141,7 +141,7 @@ export async function handleRoutes(
141
141
 
142
142
  // Page routes (SSR)
143
143
  if (url.pathname === '/' || (url.pathname.startsWith('/') && !url.pathname.includes('.'))) {
144
- const response = await handlePageRoute(url, context);
144
+ const response = await handlePageRoute(url, context, req);
145
145
  logResponseTime(startTime, req);
146
146
  return response;
147
147
  }
@@ -15,15 +15,23 @@ import { generateErrorPage } from '../ssr/errorOverlay';
15
15
  import { cacheScript, hashContent } from '../scriptCache';
16
16
  import { readTextFile, fileExists, serveFile } from '../runtime';
17
17
 
18
+ /** HTTP header sent by the studio's /__static__/ proxy to flag editor-preview requests. */
19
+ const EDITOR_HEADER = 'x-meno-editor';
20
+
18
21
  /**
19
22
  * Handle page route requests
20
23
  */
21
24
  export async function handlePageRoute(
22
25
  url: URL,
23
- context: RouteContext
26
+ context: RouteContext,
27
+ req?: Request
24
28
  ): Promise<Response | undefined> {
25
29
  const { pageService, componentService, cmsService, injectLiveReload, isEditor, serverPort } = context;
26
30
  const pagePath = url.pathname;
31
+ // Editor selection attributes (data-element-path, data-cms-context, ...) are emitted
32
+ // only when the request is proxied from the studio editor (it sets `x-meno-editor: 1`).
33
+ // Direct access to the SSR preview server (e.g., http://localhost:8082/) gets clean output.
34
+ const injectEditorAttrs = req?.headers.get(EDITOR_HEADER) === '1';
27
35
 
28
36
  // Load i18n config for locale extraction
29
37
  const i18nConfig = await loadI18nConfig();
@@ -40,6 +48,16 @@ export async function handlePageRoute(
40
48
  const cmsMatch = await cmsService.matchRoute(pathWithoutLocale, locale);
41
49
 
42
50
  if (cmsMatch) {
51
+ // Editor-only draft preview: when the studio editor requests a CMS page
52
+ // with `?previewDraft=1` and a draft exists for that item, swap the
53
+ // matched item for the draft. SSR continues to render normally; the
54
+ // live site (no editor header, no query param) is unaffected.
55
+ if (injectEditorAttrs && url.searchParams.get('previewDraft') === '1' && cmsMatch.item._filename) {
56
+ const draft = await cmsService.getDraft(cmsMatch.collection, cmsMatch.item._filename);
57
+ if (draft) {
58
+ cmsMatch.item = draft;
59
+ }
60
+ }
43
61
  // Load template page content by file path
44
62
  const templatePageContent = await loadPageByFilePath(cmsMatch.pagePath);
45
63
 
@@ -79,6 +97,7 @@ export async function handlePageRoute(
79
97
  pageLibraries: typedPageData.meta?.libraries,
80
98
  pageCustomCode: typedPageData.meta?.customCode,
81
99
  injectLiveReload,
100
+ injectEditorAttrs,
82
101
  isEditor,
83
102
  serverPort,
84
103
  returnSeparateJS: true,
@@ -115,6 +134,7 @@ export async function handlePageRoute(
115
134
  cmsTemplatePath,
116
135
  pageLibraries: typedPageData.meta?.libraries,
117
136
  pageCustomCode: typedPageData.meta?.customCode,
137
+ injectEditorAttrs,
118
138
  isEditor,
119
139
  });
120
140
 
@@ -200,6 +220,7 @@ export async function handlePageRoute(
200
220
  pageLibraries: pageData.meta?.libraries,
201
221
  pageCustomCode: pageData.meta?.customCode,
202
222
  injectLiveReload,
223
+ injectEditorAttrs,
203
224
  isEditor,
204
225
  serverPort,
205
226
  returnSeparateJS: true,
@@ -234,6 +255,7 @@ export async function handlePageRoute(
234
255
  cmsService,
235
256
  pageLibraries: pageData.meta?.libraries,
236
257
  pageCustomCode: pageData.meta?.customCode,
258
+ injectEditorAttrs,
237
259
  isEditor,
238
260
  });
239
261