meno-core 1.0.51 → 1.0.53

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 (137) hide show
  1. package/build-astro.ts +183 -13
  2. package/build-next.ts +1361 -0
  3. package/build-static.ts +7 -5
  4. package/dist/bin/cli.js +2 -2
  5. package/dist/build-static.js +6 -6
  6. package/dist/chunks/{chunk-HNLUO36W.js → chunk-GZHGVVW3.js} +2 -2
  7. package/dist/chunks/chunk-GZHGVVW3.js.map +7 -0
  8. package/dist/chunks/{chunk-LPVETICS.js → chunk-H3GJ4H2U.js} +185 -1
  9. package/dist/chunks/chunk-H3GJ4H2U.js.map +7 -0
  10. package/dist/chunks/{chunk-DM54NPEC.js → chunk-IGYR22T6.js} +89 -271
  11. package/dist/chunks/chunk-IGYR22T6.js.map +7 -0
  12. package/dist/chunks/{chunk-3KJ6SJZC.js → chunk-JGP5A3Y5.js} +12 -11
  13. package/dist/chunks/chunk-JGP5A3Y5.js.map +7 -0
  14. package/dist/chunks/{chunk-7NIC4I3V.js → chunk-JGWFTO6P.js} +167 -21
  15. package/dist/chunks/chunk-JGWFTO6P.js.map +7 -0
  16. package/dist/chunks/{chunk-EDQSMAMP.js → chunk-O3NAGJP4.js} +85 -4
  17. package/dist/chunks/chunk-O3NAGJP4.js.map +7 -0
  18. package/dist/chunks/{chunk-H4JSCDNW.js → chunk-QB2LNO4W.js} +24 -1
  19. package/dist/chunks/chunk-QB2LNO4W.js.map +7 -0
  20. package/dist/chunks/{chunk-V7CD7V7W.js → chunk-R6XHAFBF.js} +561 -112
  21. package/dist/chunks/chunk-R6XHAFBF.js.map +7 -0
  22. package/dist/chunks/{chunk-J23ZX5AP.js → chunk-X754AHS5.js} +277 -1
  23. package/dist/chunks/chunk-X754AHS5.js.map +7 -0
  24. package/dist/chunks/{chunk-2QK6U5UK.js → chunk-YBLHKYFF.js} +12 -2
  25. package/dist/chunks/chunk-YBLHKYFF.js.map +7 -0
  26. package/dist/chunks/{constants-GWBAD66U.js → constants-STK2YBIW.js} +2 -2
  27. package/dist/entries/server-router.js +7 -7
  28. package/dist/lib/client/index.js +354 -59
  29. package/dist/lib/client/index.js.map +4 -4
  30. package/dist/lib/server/index.js +1458 -190
  31. package/dist/lib/server/index.js.map +4 -4
  32. package/dist/lib/shared/index.js +202 -34
  33. package/dist/lib/shared/index.js.map +4 -4
  34. package/dist/lib/test-utils/index.js +1 -1
  35. package/entries/client-router.tsx +5 -165
  36. package/lib/client/ErrorBoundary.test.tsx +27 -25
  37. package/lib/client/ErrorBoundary.tsx +34 -19
  38. package/lib/client/core/ComponentBuilder.ts +19 -2
  39. package/lib/client/core/builders/embedBuilder.ts +8 -4
  40. package/lib/client/core/builders/listBuilder.ts +23 -4
  41. package/lib/client/fontFamiliesService.test.ts +76 -0
  42. package/lib/client/fontFamiliesService.ts +69 -0
  43. package/lib/client/hmrCssReload.ts +160 -0
  44. package/lib/client/hooks/useColorVariables.ts +2 -0
  45. package/lib/client/index.ts +4 -0
  46. package/lib/client/meno-filter/ui.ts +2 -0
  47. package/lib/client/routing/RouteLoader.test.ts +2 -2
  48. package/lib/client/routing/RouteLoader.ts +8 -2
  49. package/lib/client/routing/Router.tsx +81 -15
  50. package/lib/client/scripts/ScriptExecutor.test.ts +143 -0
  51. package/lib/client/scripts/ScriptExecutor.ts +56 -2
  52. package/lib/client/styles/StyleInjector.ts +20 -5
  53. package/lib/client/styles/UtilityClassCollector.ts +7 -1
  54. package/lib/client/styles/cspNonce.test.ts +67 -0
  55. package/lib/client/styles/cspNonce.ts +63 -0
  56. package/lib/client/templateEngine.test.ts +80 -0
  57. package/lib/client/templateEngine.ts +5 -0
  58. package/lib/server/astro/cmsPageEmitter.ts +35 -5
  59. package/lib/server/astro/componentEmitter.ts +61 -5
  60. package/lib/server/astro/nodeToAstro.ts +149 -11
  61. package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +264 -0
  62. package/lib/server/astro/normalizeOrphanTemplateProps.ts +184 -0
  63. package/lib/server/createServer.ts +11 -0
  64. package/lib/server/draftPageStore.ts +49 -0
  65. package/lib/server/fileWatcher.ts +62 -2
  66. package/lib/server/index.ts +13 -1
  67. package/lib/server/providers/fileSystemPageProvider.ts +8 -0
  68. package/lib/server/routes/api/components.ts +9 -4
  69. package/lib/server/routes/api/core-routes.ts +2 -2
  70. package/lib/server/routes/api/pages.ts +14 -22
  71. package/lib/server/routes/api/shared.ts +56 -0
  72. package/lib/server/routes/index.ts +90 -0
  73. package/lib/server/routes/pages.ts +13 -6
  74. package/lib/server/services/componentService.test.ts +199 -2
  75. package/lib/server/services/componentService.ts +354 -49
  76. package/lib/server/services/fileWatcherService.ts +4 -24
  77. package/lib/server/services/pageService.test.ts +23 -0
  78. package/lib/server/services/pageService.ts +124 -6
  79. package/lib/server/ssr/attributeBuilder.ts +8 -2
  80. package/lib/server/ssr/buildErrorOverlay.ts +1 -1
  81. package/lib/server/ssr/errorOverlay.test.ts +21 -2
  82. package/lib/server/ssr/errorOverlay.ts +38 -11
  83. package/lib/server/ssr/htmlGenerator.test.ts +53 -13
  84. package/lib/server/ssr/htmlGenerator.ts +71 -27
  85. package/lib/server/ssr/liveReloadIntegration.test.ts +123 -2
  86. package/lib/server/ssr/metaTagGenerator.ts +19 -1
  87. package/lib/server/ssr/ssrRenderer.test.ts +67 -0
  88. package/lib/server/ssr/ssrRenderer.ts +94 -9
  89. package/lib/server/ssrRenderer.test.ts +70 -0
  90. package/lib/server/websocketManager.ts +0 -1
  91. package/lib/shared/componentRefs.ts +45 -0
  92. package/lib/shared/constants.ts +8 -0
  93. package/lib/shared/cssGeneration.ts +2 -0
  94. package/lib/shared/cssProperties.ts +184 -0
  95. package/lib/shared/expressionEvaluator.ts +54 -0
  96. package/lib/shared/fontCss.ts +101 -0
  97. package/lib/shared/fontLoader.ts +8 -86
  98. package/lib/shared/friendlyError.test.ts +87 -0
  99. package/lib/shared/friendlyError.ts +121 -0
  100. package/lib/shared/hrefRefs.test.ts +130 -0
  101. package/lib/shared/hrefRefs.ts +100 -0
  102. package/lib/shared/index.ts +52 -0
  103. package/lib/shared/inlineSvgStyleRules.test.ts +108 -0
  104. package/lib/shared/inlineSvgStyleRules.ts +134 -0
  105. package/lib/shared/interfaces/contentProvider.ts +13 -0
  106. package/lib/shared/itemTemplateUtils.test.ts +14 -0
  107. package/lib/shared/itemTemplateUtils.ts +4 -1
  108. package/lib/shared/registry/NodeTypeDefinition.ts +1 -1
  109. package/lib/shared/registry/nodeTypes/LinkNodeType.ts +1 -1
  110. package/lib/shared/slugTranslator.test.ts +24 -0
  111. package/lib/shared/slugTranslator.ts +24 -0
  112. package/lib/shared/styleNodeUtils.ts +4 -1
  113. package/lib/shared/tree/PathBuilder.test.ts +128 -1
  114. package/lib/shared/tree/PathBuilder.ts +83 -31
  115. package/lib/shared/types/comment.ts +99 -0
  116. package/lib/shared/types/index.ts +12 -0
  117. package/lib/shared/types/rendering.ts +8 -0
  118. package/lib/shared/utilityClassConfig.ts +4 -2
  119. package/lib/shared/utilityClassMapper.test.ts +24 -0
  120. package/lib/shared/validation/commentValidators.ts +69 -0
  121. package/lib/shared/validation/index.ts +1 -0
  122. package/lib/shared/viewportUnits.integration.test.ts +42 -0
  123. package/lib/shared/viewportUnits.test.ts +103 -0
  124. package/lib/shared/viewportUnits.ts +63 -0
  125. package/lib/test-utils/dom-setup.ts +6 -0
  126. package/package.json +1 -1
  127. package/dist/chunks/chunk-2QK6U5UK.js.map +0 -7
  128. package/dist/chunks/chunk-3KJ6SJZC.js.map +0 -7
  129. package/dist/chunks/chunk-7NIC4I3V.js.map +0 -7
  130. package/dist/chunks/chunk-DM54NPEC.js.map +0 -7
  131. package/dist/chunks/chunk-EDQSMAMP.js.map +0 -7
  132. package/dist/chunks/chunk-H4JSCDNW.js.map +0 -7
  133. package/dist/chunks/chunk-HNLUO36W.js.map +0 -7
  134. package/dist/chunks/chunk-J23ZX5AP.js.map +0 -7
  135. package/dist/chunks/chunk-LPVETICS.js.map +0 -7
  136. package/dist/chunks/chunk-V7CD7V7W.js.map +0 -7
  137. /package/dist/chunks/{constants-GWBAD66U.js.map → constants-STK2YBIW.js.map} +0 -0
package/build-astro.ts CHANGED
@@ -23,9 +23,12 @@ import { loadProjectConfig, generateFontCSS, generateFontPreloadTags } from "./l
23
23
  import { FileSystemCMSProvider } from "./lib/server/providers/fileSystemCMSProvider";
24
24
  import { CMSService } from "./lib/server/services/cmsService";
25
25
  import { isI18nValue, resolveI18nValue } from "./lib/shared/i18n";
26
+ import { tiptapToHtml } from "./lib/shared/richtext/tiptapToHtml";
27
+ import { isTiptapDocument } from "./lib/shared/richtext/types";
26
28
  import type { ComponentDefinition, JSONPage, CMSSchema, CMSItem, I18nConfig } from "./lib/shared/types";
27
29
  import type { CMSFieldDefinition } from "./lib/shared/types/cms";
28
30
  import { isItemDraftForLocale } from "./lib/shared/types";
31
+ import { CMS_DRAFT_SUFFIX } from "./lib/shared/pathSecurity";
29
32
  import type { SlugMap } from "./lib/shared/slugTranslator";
30
33
  import { renderPageSSR } from "./lib/server/ssr/ssrRenderer";
31
34
  import { generateThemeColorVariablesCSS, generateVariablesCSS } from "./lib/server/cssGenerator";
@@ -38,7 +41,8 @@ import { collectComponentLibraries, filterLibrariesByContext, mergeLibraries, ge
38
41
  import { migrateTemplatesDirectory } from "./lib/server/migrateTemplates";
39
42
  import { emitAstroComponent } from "./lib/server/astro/componentEmitter";
40
43
  import { emitAstroPage } from "./lib/server/astro/pageEmitter";
41
- import { emitCMSPage } from './lib/server/astro/cmsPageEmitter';
44
+ import { normalizeOrphanTemplateProps } from "./lib/server/astro/normalizeOrphanTemplateProps";
45
+ import { emitCMSPage, CMS_SLUG_PLACEHOLDER } from './lib/server/astro/cmsPageEmitter';
42
46
  import { collectAllMappingClasses } from "./lib/server/astro/cssCollector";
43
47
  import { buildImageMetadataMap, RESPONSIVE_WIDTHS } from "./lib/server/ssr/imageMetadata";
44
48
  import { needsFormHandler, formHandlerScript } from "./lib/client/scripts/formHandler";
@@ -182,7 +186,9 @@ function cmsFieldToZod(field: CMSFieldDefinition): string {
182
186
  case 'string':
183
187
  case 'text':
184
188
  case 'rich-text':
185
- // Support both plain strings and i18n objects { _i18n: true, en: "...", pl: "..." }
189
+ // Rich-text is serialized to an HTML string at item-copy time (see the
190
+ // CMS collection emission below), so by the time Astro validates it the
191
+ // value is a string (or an i18n object of strings). Support both.
186
192
  return 'z.union([z.string(), z.object({ _i18n: z.literal(true) }).passthrough()])';
187
193
  case 'number':
188
194
  return 'z.number()';
@@ -206,6 +212,99 @@ function cmsFieldToZod(field: CMSFieldDefinition): string {
206
212
  }
207
213
  }
208
214
 
215
+ /**
216
+ * Serialize a single rich-text field value to an HTML string. Handles plain
217
+ * Tiptap doc objects, the `{ html: "..." }` marker shape, i18n-wrapped values
218
+ * ({ _i18n: true, en: <doc|string>, ... }), and already-serialized strings.
219
+ */
220
+ function serializeRichTextValue(value: unknown): unknown {
221
+ const one = (v: unknown): unknown => {
222
+ if (v == null || typeof v === 'string') return v ?? '';
223
+ if (isTiptapDocument(v)) return tiptapToHtml(v);
224
+ if (typeof v === 'object' && v !== null && typeof (v as { html?: unknown }).html === 'string') {
225
+ return (v as { html: string }).html;
226
+ }
227
+ return v;
228
+ };
229
+
230
+ if (isI18nValue(value)) {
231
+ const out: Record<string, unknown> = { _i18n: true };
232
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
233
+ if (k === '_i18n') continue;
234
+ out[k] = one(v);
235
+ }
236
+ return out;
237
+ }
238
+ return one(value);
239
+ }
240
+
241
+ /**
242
+ * Walk a component structure and collect the names of every component instance
243
+ * it references (`{ type: 'component', component: 'X' }`).
244
+ */
245
+ function collectComponentRefs(node: unknown, acc: Set<string>): void {
246
+ if (Array.isArray(node)) {
247
+ for (const child of node) collectComponentRefs(child, acc);
248
+ return;
249
+ }
250
+ if (!node || typeof node !== 'object') return;
251
+ const n = node as Record<string, unknown>;
252
+ if (n.type === 'component' && typeof n.component === 'string') {
253
+ acc.add(n.component);
254
+ }
255
+ for (const value of Object.values(n)) {
256
+ if (value && typeof value === 'object') collectComponentRefs(value, acc);
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Compute the set of components that consume the CMS entry (`{{cms.*}}`),
262
+ * transitively including any component that renders a consumer in its own
263
+ * structure (it must forward the `cms` prop down). Astro components have
264
+ * isolated scopes, so each consumer needs the entry threaded in explicitly.
265
+ */
266
+ function computeCmsConsumerComponents(
267
+ components: Record<string, ComponentDefinition>
268
+ ): Set<string> {
269
+ const consumers = new Set<string>();
270
+ const refsByComponent = new Map<string, Set<string>>();
271
+
272
+ for (const [name, def] of Object.entries(components)) {
273
+ const structure = def.component?.structure;
274
+ if (structure && JSON.stringify(structure).includes('{{cms.')) {
275
+ consumers.add(name);
276
+ }
277
+ const refs = new Set<string>();
278
+ collectComponentRefs(structure, refs);
279
+ refsByComponent.set(name, refs);
280
+ }
281
+
282
+ // Fixpoint: a component that renders a consumer is itself a consumer/forwarder.
283
+ let changed = true;
284
+ while (changed) {
285
+ changed = false;
286
+ for (const [name, refs] of refsByComponent) {
287
+ if (consumers.has(name)) continue;
288
+ for (const ref of refs) {
289
+ if (consumers.has(ref)) { consumers.add(name); changed = true; break; }
290
+ }
291
+ }
292
+ }
293
+ return consumers;
294
+ }
295
+
296
+ /**
297
+ * Build the `_url` expression (a JS template literal over the `e` collection
298
+ * entry) for a CMS collection, so flattened list items expose a usable link.
299
+ * E.g. urlPattern "/blog/{{slug}}" + slugField "slug" → `/blog/${e.data.slug ?? e.id}`.
300
+ */
301
+ function buildCollectionUrlExpr(schema: CMSSchema): string {
302
+ const slugField = schema.slugField || 'slug';
303
+ const pattern = schema.urlPattern || `/${schema.id}/{{slug}}`;
304
+ const body = pattern.replace(/\{\{[^}]+\}\}/, '${e.data.' + slugField + ' ?? e.id}');
305
+ return '`' + body + '`';
306
+ }
307
+
209
308
  // ---------------------------------------------------------------------------
210
309
  // Types
211
310
  // ---------------------------------------------------------------------------
@@ -484,7 +583,12 @@ export async function buildAstroProject(
484
583
  // Compute URL path
485
584
  let slug: string;
486
585
  if (slugs && slugs[locale]) {
487
- slug = slugs[locale];
586
+ // Slugs may be authored with a leading slash (e.g. "/blog" from the
587
+ // page-rename flow). Normalize to the bare form the rest of the
588
+ // pipeline expects — matches buildPageUrlForLocale(). A leading slash
589
+ // here would otherwise inflate fileDepth (".../blog.astro".split('/'))
590
+ // and produce a wrong "../../layouts" import.
591
+ slug = slugs[locale].replace(/^\/+/, '');
488
592
  } else if (basePath === '/') {
489
593
  slug = '';
490
594
  } else {
@@ -584,6 +688,30 @@ export async function buildAstroProject(
584
688
  const templateSchemas: CMSSchema[] = [];
585
689
  let cmsPageCount = 0;
586
690
 
691
+ // Pre-pass: components that consume the CMS entry (so the page/component
692
+ // emitters know which instances to thread `cms={...}` into), plus per-
693
+ // collection `_url` expressions (for flattened collection lists). Built
694
+ // before the template loop so the very first emitted CMS page sees the
695
+ // full picture even when it lists items from another collection.
696
+ const cmsConsumerComponents = computeCmsConsumerComponents(globalComponents);
697
+ const collectionUrlExpr = new Map<string, string>();
698
+ const mergedRichTextFields = new Set<string>();
699
+ if (existsSync(templatesDir)) {
700
+ for (const file of readdirSync(templatesDir).filter((f) => f.endsWith('.json'))) {
701
+ const tc = await loadJSONFile(join(templatesDir, file));
702
+ if (!tc) continue;
703
+ try {
704
+ const pd = parseJSON<JSONPage>(tc);
705
+ const schema = pd.meta?.cms as CMSSchema | undefined;
706
+ if (!schema?.id) continue;
707
+ collectionUrlExpr.set(schema.id, buildCollectionUrlExpr(schema));
708
+ for (const [fn, fd] of Object.entries(schema.fields || {})) {
709
+ if (fd.type === 'rich-text') mergedRichTextFields.add(fn);
710
+ }
711
+ } catch { /* ignore parse errors; handled in main loop */ }
712
+ }
713
+ }
714
+
587
715
  if (existsSync(templatesDir)) {
588
716
  const templateFiles = readdirSync(templatesDir).filter(f => f.endsWith('.json'));
589
717
 
@@ -613,7 +741,7 @@ export async function buildAstroProject(
613
741
 
614
742
  // Render SSR once for metadata collection (interactive styles, component CSS, JS)
615
743
  const defaultLocale = i18nConfig.defaultLocale;
616
- const dummyPath = cmsSchema.urlPattern.replace('{{slug}}', '__placeholder__');
744
+ const dummyPath = cmsSchema.urlPattern.replace('{{slug}}', CMS_SLUG_PLACEHOLDER);
617
745
 
618
746
  const metaResult = await renderPageSSR(
619
747
  pageData,
@@ -699,6 +827,8 @@ export async function buildAstroProject(
699
827
  imageFormat: configService.getImageFormat(),
700
828
  processedRawHtml: metaResult.processedRawHtmlCollector,
701
829
  remConfig: remConversionConfig,
830
+ cmsConsumers: cmsConsumerComponents,
831
+ collectionUrlExpr,
702
832
  });
703
833
 
704
834
  const astroFileFull = join(pagesOutDir, astroFilePath);
@@ -815,10 +945,20 @@ const { title, meta = '', scripts = [], locale = 'en', theme = '${themeConfig.de
815
945
  // ----------------------------------------------------------
816
946
  // 7.5. Generate component .astro files
817
947
  // ----------------------------------------------------------
948
+ // SSR/Next.js resolve orphan `{{x}}` refs in a component's structure via
949
+ // the parentProps cascade in templateEngine. Astro can't fall back at
950
+ // runtime (each .astro file is its own scope), so we lift orphan refs
951
+ // onto each component's interface and forward them from hosts that
952
+ // declare the same prop. See `normalizeOrphanTemplateProps` for details.
953
+ const emittableComponents = normalizeOrphanTemplateProps(globalComponents);
818
954
  let componentFileCount = 0;
819
- for (const [compName, compDef] of Object.entries(globalComponents)) {
955
+ for (const [compName, compDef] of Object.entries(emittableComponents)) {
820
956
  try {
821
- const astroContent = emitAstroComponent(compName, compDef, globalComponents, breakpoints, i18nConfig.defaultLocale, responsiveScales, remConversionConfig);
957
+ const astroContent = emitAstroComponent(compName, compDef, emittableComponents, breakpoints, i18nConfig.defaultLocale, responsiveScales, remConversionConfig, {
958
+ cmsConsumers: cmsConsumerComponents,
959
+ cmsRichTextFields: mergedRichTextFields,
960
+ collectionUrlExpr,
961
+ });
822
962
  await writeFile(join(componentsOutDir, `${compName}.astro`), astroContent, 'utf-8');
823
963
  componentFileCount++;
824
964
  } catch (error: any) {
@@ -852,7 +992,7 @@ const { title, meta = '', scripts = [], locale = 'en', theme = '${themeConfig.de
852
992
  // because each .astro component already has its own inline <script>
853
993
  astroContent = emitAstroPage({
854
994
  pageData: result.pageData,
855
- globalComponents,
995
+ globalComponents: emittableComponents,
856
996
  title: result.title,
857
997
  meta: result.meta,
858
998
  locale: result.locale,
@@ -931,10 +1071,22 @@ export const GET: APIRoute = () => {
931
1071
  const collectionDir = join(contentDir, schema.id);
932
1072
  mkdirSync(collectionDir, { recursive: true });
933
1073
 
1074
+ // Rich-text fields are stored as Tiptap doc objects; Astro's content
1075
+ // schema (and `<Fragment set:html>`) expects HTML strings. Mirror the
1076
+ // live SSR (cmsSSRProcessor) by serializing them to HTML at copy time.
1077
+ const richTextFieldNames = Object.entries(schema.fields || {})
1078
+ .filter(([, fd]) => fd.type === 'rich-text')
1079
+ .map(([fn]) => fn);
1080
+
934
1081
  // Copy CMS item JSON files, resolving i18n values to default locale
935
1082
  const cmsItemsDir = join(projectPaths.cms(), schema.id);
936
1083
  if (existsSync(cmsItemsDir)) {
937
- const itemFiles = readdirSync(cmsItemsDir).filter(f => f.endsWith('.json'));
1084
+ // Skip `*.draft.json` siblings in production builds — they mirror the
1085
+ // published item-list filter and may hold partial/invalid WIP data.
1086
+ const isDevBuild = process.env.MENO_DEV_BUILD === 'true';
1087
+ const itemFiles = readdirSync(cmsItemsDir).filter(f =>
1088
+ f.endsWith('.json') && (isDevBuild || !f.endsWith(`${CMS_DRAFT_SUFFIX}.json`))
1089
+ );
938
1090
 
939
1091
  for (const itemFile of itemFiles) {
940
1092
  try {
@@ -944,6 +1096,12 @@ export const GET: APIRoute = () => {
944
1096
  // Keep i18n values as-is so getStaticPaths() can resolve per-locale
945
1097
  const resolved: Record<string, unknown> = { ...item };
946
1098
 
1099
+ // Serialize rich-text fields (Tiptap doc → HTML string), handling
1100
+ // i18n-wrapped values ({ _i18n: true, en: <doc>, ... }) per-locale.
1101
+ for (const fieldName of richTextFieldNames) {
1102
+ resolved[fieldName] = serializeRichTextValue(resolved[fieldName]);
1103
+ }
1104
+
947
1105
  await writeFile(
948
1106
  join(collectionDir, itemFile),
949
1107
  JSON.stringify(resolved, null, 2),
@@ -1052,15 +1210,13 @@ export { collections };
1052
1210
  preview: 'astro preview',
1053
1211
  },
1054
1212
  dependencies: {
1055
- 'astro': '^6.0.0',
1213
+ // Astro 5 (stable Vite), NOT 6 — Astro 6's rolldown-vite breaks
1214
+ // @tailwindcss/vite at build time ("Missing field `tsconfigPaths`").
1215
+ 'astro': '^5.0.0',
1056
1216
  '@astrojs/sitemap': '^3.0.0',
1057
1217
  '@tailwindcss/vite': '^4.0.0',
1058
1218
  'tailwindcss': '^4.0.0',
1059
1219
  },
1060
- // Astro 6 expects Vite 7; pin it so npm doesn't pull Vite 8+ and warn.
1061
- overrides: {
1062
- 'vite': '^7.0.0',
1063
- },
1064
1220
  };
1065
1221
 
1066
1222
  await writeFile(join(outDir, 'package.json'), JSON.stringify(packageJson, null, 2), 'utf-8');
@@ -1092,6 +1248,20 @@ export default defineConfig({${siteUrl ? `\n site: '${siteUrl}',` : ''}${i18nBl
1092
1248
 
1093
1249
  await writeFile(join(outDir, 'tsconfig.json'), JSON.stringify(tsConfig, null, 2), 'utf-8');
1094
1250
 
1251
+ // netlify.toml — makes the exported project deployable on Netlify out of the
1252
+ // box. `astro` is a real dependency here (see package.json above), so once
1253
+ // Netlify runs `npm install`, `astro build` is on PATH and writes to dist/.
1254
+ // Harmless on other hosts (Cloudflare Pages / Vercel ignore this file).
1255
+ const netlifyToml = `# Generated by Meno's Astro build.
1256
+ [build]
1257
+ command = "npm run build"
1258
+ publish = "dist"
1259
+
1260
+ [build.environment]
1261
+ NODE_VERSION = "22"
1262
+ `;
1263
+ await writeFile(join(outDir, 'netlify.toml'), netlifyToml, 'utf-8');
1264
+
1095
1265
  // src/env.d.ts — resolves astro:assets and other virtual module types in IDE
1096
1266
  await writeFile(join(outDir, 'src', 'env.d.ts'), '/// <reference path="../.astro/types.d.ts" />\n', 'utf-8');
1097
1267