meno-core 1.0.52 → 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 (135) 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-CXCBV2M7.js → chunk-IGYR22T6.js} +76 -270
  11. package/dist/chunks/chunk-IGYR22T6.js.map +7 -0
  12. package/dist/chunks/{chunk-LHLHPYSP.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-A725KYFK.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/ssrRenderer.test.ts +67 -0
  87. package/lib/server/ssr/ssrRenderer.ts +94 -9
  88. package/lib/server/websocketManager.ts +0 -1
  89. package/lib/shared/componentRefs.ts +45 -0
  90. package/lib/shared/constants.ts +8 -0
  91. package/lib/shared/cssGeneration.ts +2 -0
  92. package/lib/shared/cssProperties.ts +184 -0
  93. package/lib/shared/expressionEvaluator.ts +54 -0
  94. package/lib/shared/fontCss.ts +101 -0
  95. package/lib/shared/fontLoader.ts +8 -86
  96. package/lib/shared/friendlyError.test.ts +87 -0
  97. package/lib/shared/friendlyError.ts +121 -0
  98. package/lib/shared/hrefRefs.test.ts +130 -0
  99. package/lib/shared/hrefRefs.ts +100 -0
  100. package/lib/shared/index.ts +52 -0
  101. package/lib/shared/inlineSvgStyleRules.test.ts +108 -0
  102. package/lib/shared/inlineSvgStyleRules.ts +134 -0
  103. package/lib/shared/interfaces/contentProvider.ts +13 -0
  104. package/lib/shared/itemTemplateUtils.test.ts +14 -0
  105. package/lib/shared/itemTemplateUtils.ts +4 -1
  106. package/lib/shared/registry/NodeTypeDefinition.ts +1 -1
  107. package/lib/shared/registry/nodeTypes/LinkNodeType.ts +1 -1
  108. package/lib/shared/slugTranslator.test.ts +24 -0
  109. package/lib/shared/slugTranslator.ts +24 -0
  110. package/lib/shared/styleNodeUtils.ts +4 -1
  111. package/lib/shared/tree/PathBuilder.test.ts +128 -1
  112. package/lib/shared/tree/PathBuilder.ts +83 -31
  113. package/lib/shared/types/comment.ts +99 -0
  114. package/lib/shared/types/index.ts +12 -0
  115. package/lib/shared/types/rendering.ts +8 -0
  116. package/lib/shared/utilityClassConfig.ts +4 -2
  117. package/lib/shared/utilityClassMapper.test.ts +24 -0
  118. package/lib/shared/validation/commentValidators.ts +69 -0
  119. package/lib/shared/validation/index.ts +1 -0
  120. package/lib/shared/viewportUnits.integration.test.ts +42 -0
  121. package/lib/shared/viewportUnits.test.ts +103 -0
  122. package/lib/shared/viewportUnits.ts +63 -0
  123. package/lib/test-utils/dom-setup.ts +6 -0
  124. package/package.json +1 -1
  125. package/dist/chunks/chunk-2QK6U5UK.js.map +0 -7
  126. package/dist/chunks/chunk-7NIC4I3V.js.map +0 -7
  127. package/dist/chunks/chunk-A725KYFK.js.map +0 -7
  128. package/dist/chunks/chunk-CXCBV2M7.js.map +0 -7
  129. package/dist/chunks/chunk-EDQSMAMP.js.map +0 -7
  130. package/dist/chunks/chunk-H4JSCDNW.js.map +0 -7
  131. package/dist/chunks/chunk-HNLUO36W.js.map +0 -7
  132. package/dist/chunks/chunk-J23ZX5AP.js.map +0 -7
  133. package/dist/chunks/chunk-LHLHPYSP.js.map +0 -7
  134. package/dist/chunks/chunk-LPVETICS.js.map +0 -7
  135. /package/dist/chunks/{constants-GWBAD66U.js.map → constants-STK2YBIW.js.map} +0 -0
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  generateBuildErrorPage
3
- } from "../../chunks/chunk-HNLUO36W.js";
3
+ } from "../../chunks/chunk-GZHGVVW3.js";
4
4
  import {
5
5
  buildStaticPages
6
- } from "../../chunks/chunk-LHLHPYSP.js";
6
+ } from "../../chunks/chunk-JGP5A3Y5.js";
7
7
  import {
8
8
  ComponentService,
9
9
  EnumService,
@@ -32,7 +32,7 @@ import {
32
32
  logResponseTime,
33
33
  withErrorHandling,
34
34
  withLogging
35
- } from "../../chunks/chunk-A725KYFK.js";
35
+ } from "../../chunks/chunk-R6XHAFBF.js";
36
36
  import {
37
37
  CMSService,
38
38
  ColorService,
@@ -43,26 +43,21 @@ import {
43
43
  buildAttributes,
44
44
  buildComponentHTML,
45
45
  buildImageMetadataMap,
46
- buildSlugIndex,
47
46
  clearJSValidationCache,
48
47
  collectComponentCSS,
49
48
  collectComponentJavaScript,
50
- collectComponentLibraries,
51
49
  colorService,
52
50
  createI18nResolver,
53
51
  escapeHtml,
54
52
  extractPageMeta,
55
- filterLibrariesByContext,
56
53
  formHandlerScript,
57
54
  generateFontCSS,
58
55
  generateFontPreloadTags,
59
- generateLibraryTags,
60
56
  generateMetaTags,
61
57
  generateSSRHTML,
62
58
  generateThemeColorVariablesCSS,
63
59
  generateVariablesCSS,
64
60
  getJSValidationErrors,
65
- getLocaleLinks,
66
61
  loadBreakpointConfig,
67
62
  loadComponentDirectory,
68
63
  loadI18nConfig,
@@ -71,7 +66,6 @@ import {
71
66
  loadProjectConfig,
72
67
  mapPageNameToPath,
73
68
  menoFilterScript,
74
- mergeLibraries,
75
69
  migrateTemplatesDirectory,
76
70
  needsFormHandler,
77
71
  needsMenoFilter,
@@ -81,9 +75,8 @@ import {
81
75
  renderPageSSR,
82
76
  resetFontConfig,
83
77
  styleToString,
84
- translatePath,
85
78
  variableService
86
- } from "../../chunks/chunk-CXCBV2M7.js";
79
+ } from "../../chunks/chunk-IGYR22T6.js";
87
80
  import {
88
81
  ConfigService,
89
82
  configService
@@ -116,23 +109,34 @@ import {
116
109
  spawnProcess,
117
110
  writeFile
118
111
  } from "../../chunks/chunk-WQFG7PAH.js";
119
- import "../../chunks/chunk-H4JSCDNW.js";
112
+ import "../../chunks/chunk-QB2LNO4W.js";
120
113
  import {
121
- resolvePaletteColor
122
- } from "../../chunks/chunk-J23ZX5AP.js";
114
+ CMS_DRAFT_SUFFIX,
115
+ buildSlugIndex,
116
+ collectComponentLibraries,
117
+ filterLibrariesByContext,
118
+ generateLibraryTags,
119
+ getLocaleLinks,
120
+ mergeLibraries,
121
+ resolvePaletteColor,
122
+ translatePath
123
+ } from "../../chunks/chunk-X754AHS5.js";
123
124
  import {
125
+ deepMergeStyles,
124
126
  hasTemplates,
125
127
  isHtmlMapping,
126
128
  processCodeTemplates,
127
129
  resolveHtmlMapping
128
- } from "../../chunks/chunk-EDQSMAMP.js";
130
+ } from "../../chunks/chunk-O3NAGJP4.js";
129
131
  import {
130
132
  addItemUrl,
131
133
  buildTemplateContext,
132
134
  extractInteractiveStyleMappings,
135
+ extractUtilityClassesFromHTML,
133
136
  generateAllInteractiveCSS,
134
137
  generateElementClassName,
135
138
  generateInteractiveCSS,
139
+ generateUtilityCSS,
136
140
  getNestedValue,
137
141
  hasIf,
138
142
  hasInteractiveStyleMappings,
@@ -144,7 +148,7 @@ import {
144
148
  resolvePropsFromDefinition,
145
149
  shortHash,
146
150
  singularize
147
- } from "../../chunks/chunk-7NIC4I3V.js";
151
+ } from "../../chunks/chunk-JGWFTO6P.js";
148
152
  import {
149
153
  DEFAULT_BREAKPOINTS,
150
154
  DEFAULT_I18N_CONFIG,
@@ -155,7 +159,10 @@ import {
155
159
  resolveVariableValueAtBreakpoint,
156
160
  scalePropertyValue
157
161
  } from "../../chunks/chunk-AZQYF6KE.js";
158
- import "../../chunks/chunk-UB44F4Z2.js";
162
+ import {
163
+ isTiptapDocument,
164
+ tiptapToHtml
165
+ } from "../../chunks/chunk-UB44F4Z2.js";
159
166
  import {
160
167
  HMR_ROUTE,
161
168
  MAX_PORT_ATTEMPTS,
@@ -164,9 +171,43 @@ import {
164
171
  SERVER_PORT,
165
172
  SERVE_PORT,
166
173
  init_constants
167
- } from "../../chunks/chunk-2QK6U5UK.js";
174
+ } from "../../chunks/chunk-YBLHKYFF.js";
168
175
  import "../../chunks/chunk-KSBZ2L7C.js";
169
176
 
177
+ // lib/server/draftPageStore.ts
178
+ var DraftPageStore = class {
179
+ drafts = /* @__PURE__ */ new Map();
180
+ /**
181
+ * Store a draft for a page path. The value is the raw JSON string the
182
+ * SSR pipeline expects, matching PageService.getPage()'s contract.
183
+ */
184
+ set(path, content) {
185
+ this.drafts.set(path, content);
186
+ }
187
+ /**
188
+ * Get the current draft string for a page path, if any.
189
+ */
190
+ get(path) {
191
+ return this.drafts.get(path);
192
+ }
193
+ /**
194
+ * Drop the draft for a specific path. Called when the page is saved to
195
+ * disk so subsequent renders read the persisted version.
196
+ */
197
+ clear(path) {
198
+ this.drafts.delete(path);
199
+ }
200
+ /**
201
+ * Drop every draft. Used on shutdown / project switch.
202
+ */
203
+ clearAll() {
204
+ this.drafts.clear();
205
+ }
206
+ has(path) {
207
+ return this.drafts.has(path);
208
+ }
209
+ };
210
+
170
211
  // build-astro.ts
171
212
  import { existsSync, readdirSync, mkdirSync, rmSync, statSync, copyFileSync, writeFileSync } from "fs";
172
213
  import { writeFile as writeFile2, readFile } from "fs/promises";
@@ -765,6 +806,9 @@ function astroComponentName(name) {
765
806
  function ind(ctx) {
766
807
  return " ".repeat(ctx.indent);
767
808
  }
809
+ function linkHrefExpr(expr) {
810
+ return `(typeof ${expr} === 'string' ? ${expr} : ${expr}?.href) ?? "#"`;
811
+ }
768
812
  function localizeHref(href, ctx) {
769
813
  if (!href.startsWith("/") || href.startsWith("//")) return href;
770
814
  const { locale, i18nDefaultLocale, slugMappings } = ctx;
@@ -996,6 +1040,41 @@ function buildElementClass(ctx, label) {
996
1040
  path: ctx.elementPath
997
1041
  });
998
1042
  }
1043
+ function isResponsiveStyleObject(style) {
1044
+ return "base" in style || "tablet" in style || "mobile" in style;
1045
+ }
1046
+ function getComponentRootStyle(def) {
1047
+ const root = def?.component?.structure;
1048
+ if (!root || typeof root !== "object") return void 0;
1049
+ const s = root.style ?? root.props?.style;
1050
+ return s && typeof s === "object" ? s : void 0;
1051
+ }
1052
+ function hasNonEmptyStyle(style) {
1053
+ if (isResponsiveStyleObject(style)) {
1054
+ return Object.values(style).some(
1055
+ (branch) => !!branch && typeof branch === "object" && Object.keys(branch).length > 0
1056
+ );
1057
+ }
1058
+ return Object.keys(style).length > 0;
1059
+ }
1060
+ function stripTemplateExpressionStyle(style) {
1061
+ const cleanFlat = (s) => {
1062
+ const out = {};
1063
+ for (const [k, v] of Object.entries(s)) {
1064
+ if (typeof v === "string" && hasTemplates2(v)) continue;
1065
+ out[k] = v;
1066
+ }
1067
+ return out;
1068
+ };
1069
+ if (isResponsiveStyleObject(style)) {
1070
+ const out = {};
1071
+ for (const [bp, branch] of Object.entries(style)) {
1072
+ out[bp] = branch && typeof branch === "object" ? cleanFlat(branch) : branch;
1073
+ }
1074
+ return out;
1075
+ }
1076
+ return cleanFlat(style);
1077
+ }
999
1078
  function buildAttributesString(attributes, ctx) {
1000
1079
  if (!attributes) return "";
1001
1080
  const parts = [];
@@ -1012,7 +1091,7 @@ function buildAttributesString(attributes, ctx) {
1012
1091
  if (ctx.listIndexVar) expr = replaceItemMetaVars(expr, ctx.listIndexVar, ctx.listSourceVar);
1013
1092
  const propDef = ctx.componentProps[expr];
1014
1093
  if (propDef && propDef.type === "link") {
1015
- parts.push(`${key}={${expr}?.href ?? "#"}`);
1094
+ parts.push(`${key}={${linkHrefExpr(expr)}}`);
1016
1095
  } else {
1017
1096
  parts.push(`${key}={${expr} || undefined}`);
1018
1097
  }
@@ -1022,7 +1101,7 @@ function buildAttributesString(attributes, ctx) {
1022
1101
  if (ctx.listItemBinding) trimmed = rewriteItemVar(trimmed, ctx.listItemBinding);
1023
1102
  if (ctx.listIndexVar) trimmed = replaceItemMetaVars(trimmed, ctx.listIndexVar, ctx.listSourceVar);
1024
1103
  const pd = ctx.componentProps[trimmed];
1025
- return pd?.type === "link" ? `\${${trimmed}?.href ?? "#"}` : `\${${trimmed}}`;
1104
+ return pd?.type === "link" ? `\${${linkHrefExpr(trimmed)}}` : `\${${trimmed}}`;
1026
1105
  });
1027
1106
  parts.push(`${key}={\`${resolved}\`}`);
1028
1107
  }
@@ -1520,10 +1599,29 @@ function emitComponentInstance(node, ctx) {
1520
1599
  }
1521
1600
  }
1522
1601
  }
1523
- if (node.style) {
1524
- const { classes: instanceClasses } = responsiveStylesToTailwind(node.style, ctx.breakpoints, ctx.responsiveScales);
1525
- if (instanceClasses.length > 0) {
1526
- propParts.push(`class="${instanceClasses.join(" ")}"`);
1602
+ if (ctx.cmsMode && ctx.cmsConsumers?.has(node.component) && !("cms" in (node.props ?? {}))) {
1603
+ propParts.push(`cms={${ctx.cmsEntryBinding || "entry"}}`);
1604
+ }
1605
+ {
1606
+ const instanceStyle = node.style;
1607
+ const instanceInteractive = node.interactiveStyles;
1608
+ const hasStyle = !!instanceStyle && hasNonEmptyStyle(instanceStyle);
1609
+ const hasInteractive = Array.isArray(instanceInteractive) && instanceInteractive.length > 0;
1610
+ if ((hasStyle || hasInteractive) && ctx.collectedInteractiveStyles) {
1611
+ const elementClass = buildElementClass(ctx, node.label);
1612
+ const rules = [];
1613
+ if (hasStyle) {
1614
+ const rootStyle = getComponentRootStyle(ctx.globalComponents[node.component]);
1615
+ const merged = rootStyle ? deepMergeStyles(rootStyle, instanceStyle) : instanceStyle;
1616
+ const cssSafe = stripTemplateExpressionStyle(merged);
1617
+ if (hasNonEmptyStyle(cssSafe)) rules.push({ style: cssSafe });
1618
+ }
1619
+ if (hasInteractive) rules.push(...instanceInteractive);
1620
+ if (rules.length > 0) {
1621
+ const existing = ctx.collectedInteractiveStyles.get(elementClass);
1622
+ ctx.collectedInteractiveStyles.set(elementClass, existing ? [...existing, ...rules] : rules);
1623
+ propParts.push(`class="${elementClass}"`);
1624
+ }
1527
1625
  }
1528
1626
  }
1529
1627
  const propsStr = propParts.length > 0 ? " " + propParts.join(" ") : "";
@@ -1630,7 +1728,7 @@ function emitLinkNode(node, ctx) {
1630
1728
  if (isLinkMapping(nodeHref)) {
1631
1729
  if (ctx.isComponentDef) {
1632
1730
  const propRef = nodeHref.prop;
1633
- hrefAttr = ` href={${propRef}?.href ?? "#"}`;
1731
+ hrefAttr = ` href={${linkHrefExpr(propRef)}}`;
1634
1732
  } else {
1635
1733
  hrefAttr = ' href="#"';
1636
1734
  }
@@ -1644,7 +1742,7 @@ function emitLinkNode(node, ctx) {
1644
1742
  if (ctx.listIndexVar) expr = replaceItemMetaVars(expr, ctx.listIndexVar, ctx.listSourceVar);
1645
1743
  const propDef = ctx.componentProps[expr];
1646
1744
  if (propDef && propDef.type === "link") {
1647
- hrefAttr = ` href={${expr}?.href ?? "#"}`;
1745
+ hrefAttr = ` href={${linkHrefExpr(expr)}}`;
1648
1746
  } else {
1649
1747
  hrefAttr = ` href={${expr}}`;
1650
1748
  }
@@ -1654,7 +1752,7 @@ function emitLinkNode(node, ctx) {
1654
1752
  if (ctx.listItemBinding) trimmed = rewriteItemVar(trimmed, ctx.listItemBinding);
1655
1753
  if (ctx.listIndexVar) trimmed = replaceItemMetaVars(trimmed, ctx.listIndexVar, ctx.listSourceVar);
1656
1754
  const pd = ctx.componentProps[trimmed];
1657
- return pd?.type === "link" ? `\${${trimmed}?.href ?? "#"}` : `\${${trimmed}}`;
1755
+ return pd?.type === "link" ? `\${${linkHrefExpr(trimmed)}}` : `\${${trimmed}}`;
1658
1756
  });
1659
1757
  hrefAttr = ` href={\`${resolved}\`}`;
1660
1758
  }
@@ -1837,6 +1935,8 @@ function emitCollectionListNode(node, ctx) {
1837
1935
  const end = node.limit ? start + node.limit : void 0;
1838
1936
  queryChain += `.then(items => items.slice(${start}${end !== void 0 ? `, ${end}` : ""}))`;
1839
1937
  }
1938
+ const urlExpr = ctx.collectionUrlExpr?.get(source) ?? `\`/${source}/\${e.data.slug ?? e.id}\``;
1939
+ queryChain += `.then(items => items.map((e) => ({ ...e.data, _id: e.id, _url: ${urlExpr} })))`;
1840
1940
  ctx.frontmatterLines.push(`const ${collectionVar} = ${queryChain};`);
1841
1941
  const indexVar = `${itemAs}Index`;
1842
1942
  const innerCtx = {
@@ -2127,13 +2227,14 @@ function mergeClassNameOntoRoot(template) {
2127
2227
  }
2128
2228
  return prefix + tagName + ` class={className}` + attrs + close + template.slice(fullMatch.length);
2129
2229
  }
2130
- function emitAstroComponent(name, def, allComponents, breakpoints = DEFAULT_BREAKPOINTS, defaultLocale = "en", responsiveScales, remConfig) {
2230
+ function emitAstroComponent(name, def, allComponents, breakpoints = DEFAULT_BREAKPOINTS, defaultLocale = "en", responsiveScales, remConfig, cmsOptions) {
2131
2231
  const comp = def.component;
2132
2232
  const propDefs = comp.interface || {};
2133
2233
  const structure = comp.structure;
2134
2234
  if (!structure) {
2135
2235
  return buildNoStructureComponent(name, comp);
2136
2236
  }
2237
+ const isCmsConsumer = cmsOptions?.cmsConsumers?.has(name) ?? false;
2137
2238
  const ctx = {
2138
2239
  imports: /* @__PURE__ */ new Set(),
2139
2240
  isComponentDef: true,
@@ -2150,7 +2251,18 @@ function emitAstroComponent(name, def, allComponents, breakpoints = DEFAULT_BREA
2150
2251
  imageImports: /* @__PURE__ */ new Map(),
2151
2252
  fileDepth: 0,
2152
2253
  // components live at src/components/
2153
- collectedInteractiveStyles: /* @__PURE__ */ new Map()
2254
+ collectedInteractiveStyles: /* @__PURE__ */ new Map(),
2255
+ // Frontmatter sink for collection queries (getCollection) emitted by lists.
2256
+ frontmatterLines: [],
2257
+ astroImports: /* @__PURE__ */ new Set(),
2258
+ cmsConsumers: cmsOptions?.cmsConsumers,
2259
+ collectionUrlExpr: cmsOptions?.collectionUrlExpr,
2260
+ ...isCmsConsumer ? {
2261
+ cmsMode: true,
2262
+ cmsEntryBinding: "cms",
2263
+ cmsWrapFn: "r",
2264
+ cmsRichTextFields: cmsOptions?.cmsRichTextFields
2265
+ } : {}
2154
2266
  };
2155
2267
  let templateBody = nodeToAstro(structure, ctx);
2156
2268
  templateBody = mergeClassNameOntoRoot(templateBody);
@@ -2159,8 +2271,11 @@ function emitAstroComponent(name, def, allComponents, breakpoints = DEFAULT_BREA
2159
2271
  propDefs,
2160
2272
  ctx.imports,
2161
2273
  ctx.dynamicTags,
2162
- ctx.needsI18nResolver ? defaultLocale : void 0,
2163
- ctx.imageImports
2274
+ ctx.needsI18nResolver || isCmsConsumer ? defaultLocale : void 0,
2275
+ ctx.imageImports,
2276
+ ctx.astroImports,
2277
+ ctx.frontmatterLines,
2278
+ isCmsConsumer
2164
2279
  );
2165
2280
  const styleSection = comp.css ? `
2166
2281
  <style>
@@ -2177,8 +2292,11 @@ ${generateAllInteractiveCSS(ctx.collectedInteractiveStyles, breakpoints, remConf
2177
2292
  ${frontmatter}---
2178
2293
  ${templateBody}${styleSection}${interactiveStyleSection}${scriptSection}`;
2179
2294
  }
2180
- function buildFrontmatter(componentName, propDefs, imports, dynamicTags, i18nDefaultLocale, imageImports) {
2295
+ function buildFrontmatter(componentName, propDefs, imports, dynamicTags, i18nDefaultLocale, imageImports, astroImports, frontmatterLines, isCmsConsumer) {
2181
2296
  const lines = [];
2297
+ if (astroImports && astroImports.size > 0) {
2298
+ lines.push(`import { ${Array.from(astroImports).sort().join(", ")} } from 'astro:content';`);
2299
+ }
2182
2300
  for (const imp of Array.from(imports).sort()) {
2183
2301
  lines.push(`import ${astroComponentName(imp)} from './${imp}.astro';`);
2184
2302
  }
@@ -2199,6 +2317,9 @@ function buildFrontmatter(componentName, propDefs, imports, dynamicTags, i18nDef
2199
2317
  const optional = "default" in propDef && propDef.default !== void 0;
2200
2318
  lines.push(` ${propName}${optional ? "?" : ""}: ${tsType};`);
2201
2319
  }
2320
+ if (isCmsConsumer) {
2321
+ lines.push(" cms?: any;");
2322
+ }
2202
2323
  lines.push(" class?: string;");
2203
2324
  lines.push("}");
2204
2325
  lines.push("");
@@ -2214,6 +2335,9 @@ function buildFrontmatter(componentName, propDefs, imports, dynamicTags, i18nDef
2214
2335
  destructParts.push(propName);
2215
2336
  }
2216
2337
  }
2338
+ if (isCmsConsumer) {
2339
+ destructParts.push("cms");
2340
+ }
2217
2341
  destructParts.push('class: className = ""');
2218
2342
  if (destructParts.length <= 3 && destructParts.join(", ").length < 80) {
2219
2343
  lines.push(`const { ${destructParts.join(", ")} } = Astro.props;`);
@@ -2241,6 +2365,10 @@ function buildFrontmatter(componentName, propDefs, imports, dynamicTags, i18nDef
2241
2365
  lines.push(` return v ?? '';`);
2242
2366
  lines.push(`};`);
2243
2367
  }
2368
+ if (frontmatterLines && frontmatterLines.length > 0) {
2369
+ lines.push("");
2370
+ for (const line of frontmatterLines) lines.push(line);
2371
+ }
2244
2372
  if (lines.length > 0) lines.push("");
2245
2373
  return lines.join("\n");
2246
2374
  }
@@ -2446,7 +2574,111 @@ import BaseLayout from '${layoutImport}';
2446
2574
  `;
2447
2575
  }
2448
2576
 
2577
+ // lib/server/astro/normalizeOrphanTemplateProps.ts
2578
+ var BARE_IDENT_BODY = /^\s*([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\s*\.\s*[a-zA-Z_$][a-zA-Z0-9_$]*)*\s*$/;
2579
+ var RESERVED_BASES = /* @__PURE__ */ new Set([
2580
+ "cms",
2581
+ "item",
2582
+ "page",
2583
+ "itemIndex",
2584
+ "itemFirst",
2585
+ "itemLast"
2586
+ ]);
2587
+ function recordRef(base, declared, listVars, out) {
2588
+ if (RESERVED_BASES.has(base)) return;
2589
+ if (listVars.has(base)) return;
2590
+ if (declared.has(base)) return;
2591
+ out.add(base);
2592
+ }
2593
+ function findOrphanRefs(value, declared, listVars, out) {
2594
+ if (value == null) return;
2595
+ if (typeof value === "string") {
2596
+ const re = /\{\{([^}]+)\}\}/g;
2597
+ let m;
2598
+ while ((m = re.exec(value)) !== null) {
2599
+ const body = m[1];
2600
+ const idMatch = body.match(BARE_IDENT_BODY);
2601
+ if (!idMatch) continue;
2602
+ recordRef(idMatch[1], declared, listVars, out);
2603
+ }
2604
+ return;
2605
+ }
2606
+ if (Array.isArray(value)) {
2607
+ for (const v of value) findOrphanRefs(v, declared, listVars, out);
2608
+ return;
2609
+ }
2610
+ if (typeof value !== "object") return;
2611
+ const obj = value;
2612
+ if (obj._mapping === true && typeof obj.prop === "string") {
2613
+ recordRef(obj.prop, declared, listVars, out);
2614
+ }
2615
+ let nextListVars = listVars;
2616
+ if (obj.type === "list") {
2617
+ const itemAs = typeof obj.itemAs === "string" && obj.itemAs ? obj.itemAs : "item";
2618
+ nextListVars = new Set(listVars);
2619
+ nextListVars.add(itemAs);
2620
+ }
2621
+ for (const key of Object.keys(obj)) {
2622
+ if (obj.type === "component" && key === "props") continue;
2623
+ findOrphanRefs(obj[key], declared, nextListVars, out);
2624
+ }
2625
+ }
2626
+ function forwardLiftedPropsOnInstances(structure, hostProps, liftedByComp) {
2627
+ if (structure == null || typeof structure !== "object") return;
2628
+ if (Array.isArray(structure)) {
2629
+ for (const s of structure) forwardLiftedPropsOnInstances(s, hostProps, liftedByComp);
2630
+ return;
2631
+ }
2632
+ const obj = structure;
2633
+ if (obj.type === "component" && typeof obj.component === "string") {
2634
+ const lifted = liftedByComp.get(obj.component);
2635
+ if (lifted && lifted.size > 0) {
2636
+ const props = obj.props ?? {};
2637
+ let changed = false;
2638
+ for (const propName of lifted) {
2639
+ if (propName in props) continue;
2640
+ if (!hostProps.has(propName)) continue;
2641
+ props[propName] = `{{${propName}}}`;
2642
+ changed = true;
2643
+ }
2644
+ if (changed) obj.props = props;
2645
+ }
2646
+ }
2647
+ for (const key of Object.keys(obj)) {
2648
+ forwardLiftedPropsOnInstances(obj[key], hostProps, liftedByComp);
2649
+ }
2650
+ }
2651
+ function normalizeOrphanTemplateProps(components) {
2652
+ const next = {};
2653
+ const liftedByComp = /* @__PURE__ */ new Map();
2654
+ for (const [name, def] of Object.entries(components)) {
2655
+ const cloned = structuredClone(def);
2656
+ next[name] = cloned;
2657
+ const comp = cloned.component;
2658
+ if (!comp || !comp.structure) continue;
2659
+ const iface = comp.interface ?? {};
2660
+ const declared = new Set(Object.keys(iface));
2661
+ const orphans = /* @__PURE__ */ new Set();
2662
+ findOrphanRefs(comp.structure, declared, /* @__PURE__ */ new Set(), orphans);
2663
+ if (orphans.size === 0) continue;
2664
+ const augmented = { ...iface };
2665
+ for (const propName of orphans) {
2666
+ augmented[propName] = { type: "string", default: "" };
2667
+ }
2668
+ comp.interface = augmented;
2669
+ liftedByComp.set(name, orphans);
2670
+ }
2671
+ for (const [, hostDef] of Object.entries(next)) {
2672
+ const comp = hostDef.component;
2673
+ if (!comp || !comp.structure) continue;
2674
+ const hostProps = new Set(Object.keys(comp.interface ?? {}));
2675
+ forwardLiftedPropsOnInstances(comp.structure, hostProps, liftedByComp);
2676
+ }
2677
+ return next;
2678
+ }
2679
+
2449
2680
  // lib/server/astro/cmsPageEmitter.ts
2681
+ var CMS_SLUG_PLACEHOLDER = "__placeholder__";
2450
2682
  function escapeTemplateLiteral3(s) {
2451
2683
  return s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
2452
2684
  }
@@ -2622,7 +2854,12 @@ function emitCMSPage(options) {
2622
2854
  processedRawHtml,
2623
2855
  imageImports: /* @__PURE__ */ new Map(),
2624
2856
  fileDepth,
2625
- collectedInteractiveStyles: /* @__PURE__ */ new Map()
2857
+ collectedInteractiveStyles: /* @__PURE__ */ new Map(),
2858
+ // Frontmatter sink for any page-level collection-sourced lists.
2859
+ frontmatterLines: [],
2860
+ astroImports: /* @__PURE__ */ new Set(),
2861
+ cmsConsumers: options.cmsConsumers,
2862
+ collectionUrlExpr: options.collectionUrlExpr
2626
2863
  };
2627
2864
  const templateBody = nodeToAstro(root, ctx);
2628
2865
  const importLines = [];
@@ -2643,9 +2880,13 @@ function emitCMSPage(options) {
2643
2880
  const staticPaths = buildGetStaticPaths(cmsSchema, isMultiLocale, i18nConfig, locale);
2644
2881
  const scriptsArrayLiteral = scriptPaths.length > 0 ? `[${scriptPaths.map((s) => `"${s}"`).join(", ")}]` : "[]";
2645
2882
  const libraryTagsLiteral = `{ headCSS: \`${escapeTemplateLiteral3(libraryTags.headCSS || "")}\`, headJS: \`${escapeTemplateLiteral3(libraryTags.headJS || "")}\`, bodyEndJS: \`${escapeTemplateLiteral3(libraryTags.bodyEndJS || "")}\` }`;
2883
+ const slugField = cmsSchema.slugField || "slug";
2646
2884
  const escapedMeta = escapeTemplateLiteral3(meta).replace(
2647
2885
  /\{\{cms\.([^}]+)\}\}/g,
2648
2886
  (_, fieldPath) => `\${${wrapFn}(${binding}.data.${fieldPath.trim()})}`
2887
+ ).replace(
2888
+ new RegExp(CMS_SLUG_PLACEHOLDER, "g"),
2889
+ `\${${wrapFn}(${binding}.data.${slugField}) || ${binding}.id}`
2649
2890
  );
2650
2891
  const escapedFontPreloads = escapeTemplateLiteral3(fontPreloads);
2651
2892
  const titleExpr = transformTitleExpression(title, binding, richTextFields, wrapFn);
@@ -2653,6 +2894,7 @@ function emitCMSPage(options) {
2653
2894
  if (v && typeof v === 'object' && v._i18n) return v['${locale}'] ?? v['${i18nConfig.defaultLocale}'] ?? Object.values(v).find(x => x !== true && x !== undefined) ?? '';
2654
2895
  return v ?? '';
2655
2896
  }`;
2897
+ const extraFrontmatter = ctx.frontmatterLines && ctx.frontmatterLines.length > 0 ? "\n" + ctx.frontmatterLines.join("\n") : "";
2656
2898
  const interactiveStyleSection = ctx.collectedInteractiveStyles.size > 0 ? `
2657
2899
  <style is:global>
2658
2900
  ${generateAllInteractiveCSS(ctx.collectedInteractiveStyles, breakpoints, remConfig, responsiveScales)}
@@ -2663,7 +2905,7 @@ ${importLines.join("\n")}
2663
2905
 
2664
2906
  ${staticPaths}
2665
2907
 
2666
- ${resolverHelper}
2908
+ ${resolverHelper}${extraFrontmatter}
2667
2909
  ---
2668
2910
  <BaseLayout
2669
2911
  title=${titleExpr}
@@ -2911,6 +3153,73 @@ function cmsFieldToZod(field) {
2911
3153
  return "z.string()";
2912
3154
  }
2913
3155
  }
3156
+ function serializeRichTextValue(value) {
3157
+ const one = (v) => {
3158
+ if (v == null || typeof v === "string") return v ?? "";
3159
+ if (isTiptapDocument(v)) return tiptapToHtml(v);
3160
+ if (typeof v === "object" && v !== null && typeof v.html === "string") {
3161
+ return v.html;
3162
+ }
3163
+ return v;
3164
+ };
3165
+ if (isI18nValue(value)) {
3166
+ const out = { _i18n: true };
3167
+ for (const [k, v] of Object.entries(value)) {
3168
+ if (k === "_i18n") continue;
3169
+ out[k] = one(v);
3170
+ }
3171
+ return out;
3172
+ }
3173
+ return one(value);
3174
+ }
3175
+ function collectComponentRefs(node, acc) {
3176
+ if (Array.isArray(node)) {
3177
+ for (const child of node) collectComponentRefs(child, acc);
3178
+ return;
3179
+ }
3180
+ if (!node || typeof node !== "object") return;
3181
+ const n = node;
3182
+ if (n.type === "component" && typeof n.component === "string") {
3183
+ acc.add(n.component);
3184
+ }
3185
+ for (const value of Object.values(n)) {
3186
+ if (value && typeof value === "object") collectComponentRefs(value, acc);
3187
+ }
3188
+ }
3189
+ function computeCmsConsumerComponents(components) {
3190
+ const consumers = /* @__PURE__ */ new Set();
3191
+ const refsByComponent = /* @__PURE__ */ new Map();
3192
+ for (const [name, def] of Object.entries(components)) {
3193
+ const structure = def.component?.structure;
3194
+ if (structure && JSON.stringify(structure).includes("{{cms.")) {
3195
+ consumers.add(name);
3196
+ }
3197
+ const refs = /* @__PURE__ */ new Set();
3198
+ collectComponentRefs(structure, refs);
3199
+ refsByComponent.set(name, refs);
3200
+ }
3201
+ let changed = true;
3202
+ while (changed) {
3203
+ changed = false;
3204
+ for (const [name, refs] of refsByComponent) {
3205
+ if (consumers.has(name)) continue;
3206
+ for (const ref of refs) {
3207
+ if (consumers.has(ref)) {
3208
+ consumers.add(name);
3209
+ changed = true;
3210
+ break;
3211
+ }
3212
+ }
3213
+ }
3214
+ }
3215
+ return consumers;
3216
+ }
3217
+ function buildCollectionUrlExpr(schema) {
3218
+ const slugField = schema.slugField || "slug";
3219
+ const pattern = schema.urlPattern || `/${schema.id}/{{slug}}`;
3220
+ const body = pattern.replace(/\{\{[^}]+\}\}/, "${e.data." + slugField + " ?? e.id}");
3221
+ return "`" + body + "`";
3222
+ }
2914
3223
  function buildSSRFallbackPage(result, importPath, fontPreloads, libraryTags, defaultTheme, scriptPaths) {
2915
3224
  const escapedMeta = escapeTemplateLiteral4(result.meta);
2916
3225
  const escapedHTML = escapeTemplateLiteral4(result.html);
@@ -3010,7 +3319,7 @@ async function buildAstroProject(projectRoot, outputDir) {
3010
3319
  }
3011
3320
  }
3012
3321
  }
3013
- function processRenderResult(result, urlPath, astroFilePath, fileDepth, pageData, pageName, isCMSPage3) {
3322
+ function processRenderResult(result, urlPath, astroFilePath, fileDepth, pageData, pageName, isCMSPage4) {
3014
3323
  mergeInteractiveStyles(result.interactiveStylesMap);
3015
3324
  if (result.componentCSS) {
3016
3325
  allComponentCSS.add(result.componentCSS);
@@ -3037,7 +3346,7 @@ async function buildAstroProject(projectRoot, outputDir) {
3037
3346
  astroFilePath,
3038
3347
  pageData,
3039
3348
  pageName,
3040
- isCMSPage: isCMSPage3,
3349
+ isCMSPage: isCMSPage4,
3041
3350
  ssrFallbackCollector: result.ssrFallbackCollector,
3042
3351
  processedRawHtmlCollector: result.processedRawHtmlCollector
3043
3352
  });
@@ -3063,7 +3372,7 @@ async function buildAstroProject(projectRoot, outputDir) {
3063
3372
  const isDefault = locale === i18nConfig.defaultLocale;
3064
3373
  let slug;
3065
3374
  if (slugs && slugs[locale]) {
3066
- slug = slugs[locale];
3375
+ slug = slugs[locale].replace(/^\/+/, "");
3067
3376
  } else if (basePath === "/") {
3068
3377
  slug = "";
3069
3378
  } else {
@@ -3136,6 +3445,25 @@ async function buildAstroProject(projectRoot, outputDir) {
3136
3445
  const templatesDir = projectPaths.templates();
3137
3446
  const templateSchemas = [];
3138
3447
  let cmsPageCount = 0;
3448
+ const cmsConsumerComponents = computeCmsConsumerComponents(globalComponents);
3449
+ const collectionUrlExpr = /* @__PURE__ */ new Map();
3450
+ const mergedRichTextFields = /* @__PURE__ */ new Set();
3451
+ if (existsSync(templatesDir)) {
3452
+ for (const file of readdirSync(templatesDir).filter((f) => f.endsWith(".json"))) {
3453
+ const tc = await loadJSONFile(join(templatesDir, file));
3454
+ if (!tc) continue;
3455
+ try {
3456
+ const pd = parseJSON(tc);
3457
+ const schema = pd.meta?.cms;
3458
+ if (!schema?.id) continue;
3459
+ collectionUrlExpr.set(schema.id, buildCollectionUrlExpr(schema));
3460
+ for (const [fn, fd] of Object.entries(schema.fields || {})) {
3461
+ if (fd.type === "rich-text") mergedRichTextFields.add(fn);
3462
+ }
3463
+ } catch {
3464
+ }
3465
+ }
3466
+ }
3139
3467
  if (existsSync(templatesDir)) {
3140
3468
  const templateFiles = readdirSync(templatesDir).filter((f) => f.endsWith(".json"));
3141
3469
  for (const file of templateFiles) {
@@ -3156,7 +3484,7 @@ async function buildAstroProject(projectRoot, outputDir) {
3156
3484
  const items = await cmsService.queryItems({ collection: cmsSchema.id });
3157
3485
  const itemCount = items.length;
3158
3486
  const defaultLocale = i18nConfig.defaultLocale;
3159
- const dummyPath = cmsSchema.urlPattern.replace("{{slug}}", "__placeholder__");
3487
+ const dummyPath = cmsSchema.urlPattern.replace("{{slug}}", CMS_SLUG_PLACEHOLDER);
3160
3488
  const metaResult = await renderPageSSR(
3161
3489
  pageData,
3162
3490
  globalComponents,
@@ -3223,7 +3551,9 @@ async function buildAstroProject(projectRoot, outputDir) {
3223
3551
  slugMappings,
3224
3552
  imageFormat: configService.getImageFormat(),
3225
3553
  processedRawHtml: metaResult.processedRawHtmlCollector,
3226
- remConfig: remConversionConfig
3554
+ remConfig: remConversionConfig,
3555
+ cmsConsumers: cmsConsumerComponents,
3556
+ collectionUrlExpr
3227
3557
  });
3228
3558
  const astroFileFull = join(pagesOutDir, astroFilePath);
3229
3559
  const astroFileDir = astroFileFull.substring(0, astroFileFull.lastIndexOf("/"));
@@ -3307,10 +3637,15 @@ const { title, meta = '', scripts = [], locale = 'en', theme = '${themeConfig.de
3307
3637
  </html>
3308
3638
  `;
3309
3639
  await writeFile2(join(layoutsDir, "BaseLayout.astro"), baseLayoutContent, "utf-8");
3640
+ const emittableComponents = normalizeOrphanTemplateProps(globalComponents);
3310
3641
  let componentFileCount = 0;
3311
- for (const [compName, compDef] of Object.entries(globalComponents)) {
3642
+ for (const [compName, compDef] of Object.entries(emittableComponents)) {
3312
3643
  try {
3313
- const astroContent = emitAstroComponent(compName, compDef, globalComponents, breakpoints, i18nConfig.defaultLocale, responsiveScales, remConversionConfig);
3644
+ const astroContent = emitAstroComponent(compName, compDef, emittableComponents, breakpoints, i18nConfig.defaultLocale, responsiveScales, remConversionConfig, {
3645
+ cmsConsumers: cmsConsumerComponents,
3646
+ cmsRichTextFields: mergedRichTextFields,
3647
+ collectionUrlExpr
3648
+ });
3314
3649
  await writeFile2(join(componentsOutDir, `${compName}.astro`), astroContent, "utf-8");
3315
3650
  componentFileCount++;
3316
3651
  } catch (error) {
@@ -3327,7 +3662,7 @@ const { title, meta = '', scripts = [], locale = 'en', theme = '${themeConfig.de
3327
3662
  const pageSlugMap = result.pageData.meta?.slugs ? computePageSlugMap(result.pageData.meta.slugs, i18nConfig) : void 0;
3328
3663
  astroContent = emitAstroPage({
3329
3664
  pageData: result.pageData,
3330
- globalComponents,
3665
+ globalComponents: emittableComponents,
3331
3666
  title: result.title,
3332
3667
  meta: result.meta,
3333
3668
  locale: result.locale,
@@ -3390,14 +3725,21 @@ export const GET: APIRoute = () => {
3390
3725
  for (const schema of templateSchemas) {
3391
3726
  const collectionDir = join(contentDir, schema.id);
3392
3727
  mkdirSync(collectionDir, { recursive: true });
3728
+ const richTextFieldNames = Object.entries(schema.fields || {}).filter(([, fd]) => fd.type === "rich-text").map(([fn]) => fn);
3393
3729
  const cmsItemsDir = join(projectPaths.cms(), schema.id);
3394
3730
  if (existsSync(cmsItemsDir)) {
3395
- const itemFiles = readdirSync(cmsItemsDir).filter((f) => f.endsWith(".json"));
3731
+ const isDevBuild = process.env.MENO_DEV_BUILD === "true";
3732
+ const itemFiles = readdirSync(cmsItemsDir).filter(
3733
+ (f) => f.endsWith(".json") && (isDevBuild || !f.endsWith(`${CMS_DRAFT_SUFFIX}.json`))
3734
+ );
3396
3735
  for (const itemFile of itemFiles) {
3397
3736
  try {
3398
3737
  const rawContent = await readFile(join(cmsItemsDir, itemFile), "utf-8");
3399
3738
  const item = JSON.parse(rawContent);
3400
3739
  const resolved = { ...item };
3740
+ for (const fieldName of richTextFieldNames) {
3741
+ resolved[fieldName] = serializeRichTextValue(resolved[fieldName]);
3742
+ }
3401
3743
  await writeFile2(
3402
3744
  join(collectionDir, itemFile),
3403
3745
  JSON.stringify(resolved, null, 2),
@@ -3470,14 +3812,12 @@ export { collections };
3470
3812
  preview: "astro preview"
3471
3813
  },
3472
3814
  dependencies: {
3473
- "astro": "^6.0.0",
3815
+ // Astro 5 (stable Vite), NOT 6 — Astro 6's rolldown-vite breaks
3816
+ // @tailwindcss/vite at build time ("Missing field `tsconfigPaths`").
3817
+ "astro": "^5.0.0",
3474
3818
  "@astrojs/sitemap": "^3.0.0",
3475
3819
  "@tailwindcss/vite": "^4.0.0",
3476
3820
  "tailwindcss": "^4.0.0"
3477
- },
3478
- // Astro 6 expects Vite 7; pin it so npm doesn't pull Vite 8+ and warn.
3479
- overrides: {
3480
- "vite": "^7.0.0"
3481
3821
  }
3482
3822
  };
3483
3823
  await writeFile2(join(outDir, "package.json"), JSON.stringify(packageJson, null, 2), "utf-8");
@@ -3505,6 +3845,15 @@ export default defineConfig({${siteUrl ? `
3505
3845
  extends: "astro/tsconfigs/strict"
3506
3846
  };
3507
3847
  await writeFile2(join(outDir, "tsconfig.json"), JSON.stringify(tsConfig, null, 2), "utf-8");
3848
+ const netlifyToml = `# Generated by Meno's Astro build.
3849
+ [build]
3850
+ command = "npm run build"
3851
+ publish = "dist"
3852
+
3853
+ [build.environment]
3854
+ NODE_VERSION = "22"
3855
+ `;
3856
+ await writeFile2(join(outDir, "netlify.toml"), netlifyToml, "utf-8");
3508
3857
  await writeFile2(join(outDir, "src", "env.d.ts"), '/// <reference path="../.astro/types.d.ts" />\n', "utf-8");
3509
3858
  const totalPages = allResults.length;
3510
3859
  return {
@@ -3515,131 +3864,1048 @@ export default defineConfig({${siteUrl ? `
3515
3864
  };
3516
3865
  }
3517
3866
 
3518
- // lib/server/webflow/buildWebflow.ts
3519
- import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
3520
- import { join as join3 } from "path";
3521
- init_constants();
3522
-
3523
- // lib/server/webflow/nodeToWebflow.ts
3524
- init_constants();
3525
-
3526
- // lib/server/webflow/types.ts
3527
- var MENO_BIND_SENTINEL_PREFIX = "__MENO_BIND__:";
3528
- var MENO_BIND_SENTINEL_SUFFIX = ":__";
3529
- var MENO_BIND_SENTINEL_RE = /__MENO_BIND__:([^:]+):__/g;
3530
- var MENO_BIND_SENTINEL_EXACT_RE = /^__MENO_BIND__:([^:]+):__$/;
3531
- var COLLECTION_LIST_TAG = "__collection_list__";
3532
-
3533
- // lib/server/webflow/styleMapper.ts
3534
- var UNITLESS_PROPERTIES = /* @__PURE__ */ new Set([
3535
- "opacity",
3536
- "z-index",
3537
- "flex-grow",
3538
- "flex-shrink",
3539
- "flex",
3540
- "order",
3541
- "orphans",
3542
- "widows",
3543
- "column-count",
3544
- "font-weight",
3545
- "tab-size"
3546
- ]);
3547
- var TIME_PROPERTIES = /* @__PURE__ */ new Set([
3548
- "transition-duration",
3549
- "transition-delay",
3550
- "animation-duration",
3551
- "animation-delay"
3552
- ]);
3553
- function normalizeZero(cssProp, cssValue) {
3554
- if (cssValue !== "0") return cssValue;
3555
- if (UNITLESS_PROPERTIES.has(cssProp)) return cssValue;
3556
- if (TIME_PROPERTIES.has(cssProp)) return cssValue;
3557
- return "0px";
3558
- }
3559
- var COLOR_PROPS_CAMEL = /* @__PURE__ */ new Set(["color", "backgroundColor", "borderColor"]);
3560
- function maybeWrapColorVar(camelProp, value) {
3561
- if (!COLOR_PROPS_CAMEL.has(camelProp)) return value;
3562
- if (!value) return value;
3563
- if (value.startsWith("#")) return value;
3564
- if (value.startsWith("var(")) return value;
3565
- if (value.includes("(")) return value;
3566
- if (isCssNamedColor(value)) return value;
3567
- return `var(--${value})`;
3867
+ // build-next.ts
3868
+ import { existsSync as existsSync2, readdirSync as readdirSync2, mkdirSync as mkdirSync2, rmSync as rmSync2, statSync as statSync2, copyFileSync as copyFileSync2, writeFileSync as writeFileSync2 } from "fs";
3869
+ import { writeFile as writeFile3, readFile as readFile2 } from "fs/promises";
3870
+ import { join as join2 } from "path";
3871
+ import { createHash as createHash2 } from "crypto";
3872
+ function hashContent3(content) {
3873
+ return createHash2("sha256").update(content).digest("hex").slice(0, 8);
3874
+ }
3875
+ function writePageScript2(javascript, scriptsDir) {
3876
+ if (!javascript) return [];
3877
+ const hash = hashContent3(javascript);
3878
+ const scriptFile = `${hash}.js`;
3879
+ if (!existsSync2(scriptsDir)) {
3880
+ mkdirSync2(scriptsDir, { recursive: true });
3881
+ }
3882
+ const fullScriptPath = join2(scriptsDir, scriptFile);
3883
+ if (!existsSync2(fullScriptPath)) {
3884
+ writeFileSync2(fullScriptPath, javascript, "utf-8");
3885
+ }
3886
+ return [`/_scripts/${scriptFile}`];
3568
3887
  }
3569
- function isStyleMapping4(value) {
3570
- return typeof value === "object" && value !== null && "_mapping" in value && value._mapping === true;
3888
+ function copyDirectory2(src, dest, filter) {
3889
+ if (!existsSync2(src)) return;
3890
+ if (!existsSync2(dest)) mkdirSync2(dest, { recursive: true });
3891
+ const files = readdirSync2(src);
3892
+ for (const file of files) {
3893
+ if (filter && !filter(file)) continue;
3894
+ const srcPath = join2(src, file);
3895
+ const destPath = join2(dest, file);
3896
+ const stat = statSync2(srcPath);
3897
+ if (stat.isDirectory()) copyDirectory2(srcPath, destPath, filter);
3898
+ else copyFileSync2(srcPath, destPath);
3899
+ }
3571
3900
  }
3572
- function isResponsiveStyle4(style) {
3573
- return "base" in style || "tablet" in style || "mobile" in style;
3901
+ function isCMSPage2(pageData) {
3902
+ return pageData.meta?.source === "cms" && !!pageData.meta?.cms;
3574
3903
  }
3575
- function toKebabCase(prop) {
3576
- return prop.replace(/([A-Z])/g, "-$1").toLowerCase();
3904
+ function buildCMSItemPath(urlPattern, item, slugField, locale, i18nConfig) {
3905
+ let slug = item[slugField] ?? item._slug ?? item._id;
3906
+ if (isI18nValue(slug)) {
3907
+ slug = resolveI18nValue(slug, locale, i18nConfig);
3908
+ }
3909
+ return urlPattern.replace("{{slug}}", String(slug));
3577
3910
  }
3578
- function splitTopLevel(value) {
3579
- const out = [];
3580
- let depth = 0;
3581
- let buf = "";
3582
- for (const ch of value.trim()) {
3583
- if (ch === "(") depth++;
3584
- else if (ch === ")") depth--;
3585
- if (depth === 0 && /\s/.test(ch)) {
3586
- if (buf) {
3587
- out.push(buf);
3588
- buf = "";
3589
- }
3590
- continue;
3911
+ function scanJSONFiles2(dir, prefix = "") {
3912
+ const results = [];
3913
+ if (!existsSync2(dir)) return results;
3914
+ const entries = readdirSync2(dir, { withFileTypes: true });
3915
+ for (const entry of entries) {
3916
+ if (entry.isFile() && entry.name.endsWith(".json")) {
3917
+ results.push(prefix ? `${prefix}/${entry.name}` : entry.name);
3918
+ } else if (entry.isDirectory()) {
3919
+ results.push(...scanJSONFiles2(join2(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name));
3591
3920
  }
3592
- buf += ch;
3593
3921
  }
3594
- if (buf) out.push(buf);
3595
- return out;
3922
+ return results;
3596
3923
  }
3597
- function expandShorthand(cssProp, cssValue) {
3598
- if (cssProp !== "margin" && cssProp !== "padding" && cssProp !== "gap") {
3599
- return null;
3924
+ function escapeTemplateLiteral5(s) {
3925
+ return s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
3926
+ }
3927
+ function escapeSingleQuoted(s) {
3928
+ return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
3929
+ }
3930
+ function urlPathToAppRoute(urlPath) {
3931
+ if (urlPath === "/" || urlPath === "") {
3932
+ return { dir: "", isRoot: true };
3600
3933
  }
3601
- const parts = splitTopLevel(cssValue);
3602
- if (cssProp === "gap") {
3603
- if (parts.length === 1) {
3604
- const v = normalizeZero("row-gap", parts[0]);
3605
- return { "row-gap": v, "column-gap": v };
3934
+ const trimmed = urlPath.replace(/^\/+/, "").replace(/\/+$/, "");
3935
+ return { dir: trimmed, isRoot: false };
3936
+ }
3937
+ function metaHtmlToJSX(metaHtml) {
3938
+ if (!metaHtml || !metaHtml.trim()) return "";
3939
+ const lines = metaHtml.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
3940
+ const elements = [];
3941
+ for (const line of lines) {
3942
+ const titleMatch = line.match(/^<title>([\s\S]*?)<\/title>$/);
3943
+ if (titleMatch) {
3944
+ elements.push(`<title>{${JSON.stringify(decodeBasicEntities(titleMatch[1]))}}</title>`);
3945
+ continue;
3606
3946
  }
3607
- if (parts.length === 2) {
3608
- return {
3609
- "row-gap": normalizeZero("row-gap", parts[0]),
3610
- "column-gap": normalizeZero("column-gap", parts[1])
3611
- };
3947
+ const selfMatch = line.match(/^<(meta|link)\s+([\s\S]*?)\s*\/?>$/);
3948
+ if (selfMatch) {
3949
+ const tagName = selfMatch[1];
3950
+ const attrsSrc = selfMatch[2];
3951
+ const attrs = parseAttrs(attrsSrc);
3952
+ const attrParts = Object.entries(attrs).map(([k, v]) => `${jsxAttrName(k)}=${JSON.stringify(v)}`).join(" ");
3953
+ elements.push(`<${tagName} ${attrParts} />`);
3954
+ continue;
3612
3955
  }
3613
- return null;
3614
3956
  }
3615
- let top, right, bottom, left;
3616
- if (parts.length === 1) {
3617
- top = right = bottom = left = parts[0];
3618
- } else if (parts.length === 2) {
3619
- top = bottom = parts[0];
3620
- right = left = parts[1];
3621
- } else if (parts.length === 3) {
3622
- top = parts[0];
3623
- right = left = parts[1];
3624
- bottom = parts[2];
3625
- } else if (parts.length === 4) {
3626
- [top, right, bottom, left] = parts;
3627
- } else {
3628
- return null;
3957
+ return elements.join("\n ");
3958
+ }
3959
+ var JSX_ATTR_NAMES = {
3960
+ "http-equiv": "httpEquiv",
3961
+ "hreflang": "hrefLang",
3962
+ "crossorigin": "crossOrigin",
3963
+ "referrerpolicy": "referrerPolicy",
3964
+ "imagesrcset": "imageSrcSet",
3965
+ "imagesizes": "imageSizes",
3966
+ "fetchpriority": "fetchPriority",
3967
+ "class": "className",
3968
+ "for": "htmlFor",
3969
+ "charset": "charSet"
3970
+ };
3971
+ function jsxAttrName(name) {
3972
+ return JSX_ATTR_NAMES[name] ?? name;
3973
+ }
3974
+ function parseAttrs(src) {
3975
+ const out = {};
3976
+ const re = /([a-zA-Z_:][\w:.-]*)\s*=\s*"([^"]*)"/g;
3977
+ let match;
3978
+ while ((match = re.exec(src)) !== null) {
3979
+ out[match[1]] = decodeBasicEntities(match[2]);
3629
3980
  }
3630
- return {
3631
- [`${cssProp}-top`]: normalizeZero(`${cssProp}-top`, top),
3632
- [`${cssProp}-right`]: normalizeZero(`${cssProp}-right`, right),
3633
- [`${cssProp}-bottom`]: normalizeZero(`${cssProp}-bottom`, bottom),
3634
- [`${cssProp}-left`]: normalizeZero(`${cssProp}-left`, left)
3635
- };
3981
+ return out;
3636
3982
  }
3637
- function styleObjectToCSS(style) {
3638
- const css = {};
3639
- for (const [prop, value] of Object.entries(style)) {
3640
- if (isStyleMapping4(value)) continue;
3641
- if (value === "" || value === void 0 || value === null) continue;
3642
- if (typeof value === "boolean" || typeof value === "object") continue;
3983
+ function decodeBasicEntities(s) {
3984
+ return s.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
3985
+ }
3986
+ function emitNextPage(options) {
3987
+ const {
3988
+ html,
3989
+ meta,
3990
+ title,
3991
+ locale,
3992
+ theme,
3993
+ fontPreloads,
3994
+ libraryTags,
3995
+ scriptPaths,
3996
+ customCode,
3997
+ iconTagsHtml,
3998
+ formHandlerNeeded
3999
+ } = options;
4000
+ const metaJSX = metaHtmlToJSX(meta);
4001
+ const headHtmlBlocks = [];
4002
+ if (iconTagsHtml) headHtmlBlocks.push(iconTagsHtml);
4003
+ if (fontPreloads) headHtmlBlocks.push(fontPreloads);
4004
+ if (libraryTags.headCSS) headHtmlBlocks.push(libraryTags.headCSS);
4005
+ if (libraryTags.headJS) headHtmlBlocks.push(libraryTags.headJS);
4006
+ if (customCode.head) headHtmlBlocks.push(customCode.head);
4007
+ const headHtml = headHtmlBlocks.join("\n");
4008
+ const scriptTags = scriptPaths.map((s) => `<script src="${s}" defer></script>`).join("\n");
4009
+ const bodyEndBlocks = [];
4010
+ if (scriptTags) bodyEndBlocks.push(scriptTags);
4011
+ if (libraryTags.bodyEndJS) bodyEndBlocks.push(libraryTags.bodyEndJS);
4012
+ if (customCode.bodyEnd) bodyEndBlocks.push(customCode.bodyEnd);
4013
+ if (formHandlerNeeded) bodyEndBlocks.push(`<script>${formHandlerScript}</script>`);
4014
+ const bodyEndHtml = bodyEndBlocks.join("\n");
4015
+ const bodyStartHtml = customCode.bodyStart || "";
4016
+ return `// Auto-generated by meno-core/build-next. Do not edit.
4017
+ import RawHead from '../components/RawHead';
4018
+
4019
+ const TITLE = ${JSON.stringify(title)};
4020
+ const LOCALE = ${JSON.stringify(locale)};
4021
+ const THEME = ${JSON.stringify(theme)};
4022
+ const HEAD_HTML = \`${escapeTemplateLiteral5(headHtml)}\`;
4023
+ const BODY_START_HTML = \`${escapeTemplateLiteral5(bodyStartHtml)}\`;
4024
+ const PAGE_HTML = \`${escapeTemplateLiteral5(html)}\`;
4025
+ const BODY_END_HTML = \`${escapeTemplateLiteral5(bodyEndHtml)}\`;
4026
+
4027
+ export const metadata = {
4028
+ title: TITLE,
4029
+ };
4030
+
4031
+ export default function Page() {
4032
+ return (
4033
+ <>
4034
+ ${metaJSX || ""}
4035
+ <RawHead html={HEAD_HTML} locale={LOCALE} theme={THEME} />
4036
+ {BODY_START_HTML ? <div data-meno-body-start dangerouslySetInnerHTML={{ __html: BODY_START_HTML }} /> : null}
4037
+ <div id="root" dangerouslySetInnerHTML={{ __html: PAGE_HTML }} />
4038
+ {BODY_END_HTML ? <div data-meno-body-end dangerouslySetInnerHTML={{ __html: BODY_END_HTML }} /> : null}
4039
+ </>
4040
+ );
4041
+ }
4042
+ `;
4043
+ }
4044
+ function emitNextCMSPage(options) {
4045
+ const {
4046
+ slugs,
4047
+ perSlugData,
4048
+ locale,
4049
+ theme,
4050
+ fontPreloads,
4051
+ libraryTags,
4052
+ customCode,
4053
+ iconTagsHtml,
4054
+ formHandlerNeeded
4055
+ } = options;
4056
+ const headHtmlBlocks = [];
4057
+ if (iconTagsHtml) headHtmlBlocks.push(iconTagsHtml);
4058
+ if (fontPreloads) headHtmlBlocks.push(fontPreloads);
4059
+ if (libraryTags.headCSS) headHtmlBlocks.push(libraryTags.headCSS);
4060
+ if (libraryTags.headJS) headHtmlBlocks.push(libraryTags.headJS);
4061
+ if (customCode.head) headHtmlBlocks.push(customCode.head);
4062
+ const headHtml = headHtmlBlocks.join("\n");
4063
+ const bodyStartHtml = customCode.bodyStart || "";
4064
+ const trailingFormHandler = formHandlerNeeded ? `<script>${formHandlerScript}</script>` : "";
4065
+ const entries = [];
4066
+ for (const slug of slugs) {
4067
+ const data = perSlugData[slug];
4068
+ if (!data) continue;
4069
+ const scriptTags = data.scriptPaths.map((s) => `<script src="${s}" defer></script>`).join("\n");
4070
+ const bodyEndBlocks = [];
4071
+ if (scriptTags) bodyEndBlocks.push(scriptTags);
4072
+ if (libraryTags.bodyEndJS) bodyEndBlocks.push(libraryTags.bodyEndJS);
4073
+ if (customCode.bodyEnd) bodyEndBlocks.push(customCode.bodyEnd);
4074
+ if (trailingFormHandler) bodyEndBlocks.push(trailingFormHandler);
4075
+ const bodyEndHtml = bodyEndBlocks.join("\n");
4076
+ entries.push(
4077
+ ` ${JSON.stringify(slug)}: {
4078
+ title: ${JSON.stringify(data.title)},
4079
+ metaHtml: \`${escapeTemplateLiteral5(data.meta)}\`,
4080
+ html: \`${escapeTemplateLiteral5(data.html)}\`,
4081
+ bodyEndHtml: \`${escapeTemplateLiteral5(bodyEndHtml)}\`,
4082
+ }`
4083
+ );
4084
+ }
4085
+ return `// Auto-generated by meno-core/build-next. Do not edit.
4086
+ import RawHead from '${cmsRawHeadImport(options)}';
4087
+ import MetaTags from '${cmsMetaTagsImport(options)}';
4088
+
4089
+ const LOCALE = ${JSON.stringify(locale)};
4090
+ const THEME = ${JSON.stringify(theme)};
4091
+ const HEAD_HTML = \`${escapeTemplateLiteral5(headHtml)}\`;
4092
+ const BODY_START_HTML = \`${escapeTemplateLiteral5(bodyStartHtml)}\`;
4093
+
4094
+ type Entry = {
4095
+ title: string;
4096
+ metaHtml: string;
4097
+ html: string;
4098
+ bodyEndHtml: string;
4099
+ };
4100
+
4101
+ const ENTRIES: Record<string, Entry> = {
4102
+ ${entries.join(",\n")}
4103
+ };
4104
+
4105
+ export function generateStaticParams() {
4106
+ return Object.keys(ENTRIES).map((slug) => ({ slug }));
4107
+ }
4108
+
4109
+ export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
4110
+ const { slug } = await params;
4111
+ const entry = ENTRIES[slug];
4112
+ return entry ? { title: entry.title } : {};
4113
+ }
4114
+
4115
+ export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
4116
+ const { slug } = await params;
4117
+ const entry = ENTRIES[slug];
4118
+ if (!entry) return null;
4119
+ return (
4120
+ <>
4121
+ <MetaTags html={entry.metaHtml} />
4122
+ <RawHead html={HEAD_HTML} locale={LOCALE} theme={THEME} />
4123
+ {BODY_START_HTML ? <div data-meno-body-start dangerouslySetInnerHTML={{ __html: BODY_START_HTML }} /> : null}
4124
+ <div id="root" dangerouslySetInnerHTML={{ __html: entry.html }} />
4125
+ {entry.bodyEndHtml ? <div data-meno-body-end dangerouslySetInnerHTML={{ __html: entry.bodyEndHtml }} /> : null}
4126
+ </>
4127
+ );
4128
+ }
4129
+ `;
4130
+ }
4131
+ function cmsRawHeadImport(_options) {
4132
+ return "../../components/RawHead";
4133
+ }
4134
+ function cmsMetaTagsImport(_options) {
4135
+ return "../../components/MetaTags";
4136
+ }
4137
+ async function buildNextProject(projectRoot, outputDir) {
4138
+ configService.reset();
4139
+ const projectConfig = await loadProjectConfig();
4140
+ const siteUrl = projectConfig.siteUrl?.replace(/\/$/, "") || "";
4141
+ const i18nConfig = await loadI18nConfig();
4142
+ await migrateTemplatesDirectory();
4143
+ const { components, warnings, errors: compErrors } = await loadComponentDirectory(projectPaths.components());
4144
+ const globalComponents = {};
4145
+ components.forEach((value, key) => {
4146
+ globalComponents[key] = value;
4147
+ });
4148
+ for (const w of warnings) console.warn(` Warning: ${w}`);
4149
+ for (const e of compErrors) console.error(` Error: ${e}`);
4150
+ const cmsProvider = new FileSystemCMSProvider(projectPaths.templates(), projectPaths.cms());
4151
+ const cmsService = new CMSService(cmsProvider);
4152
+ await cmsService.initialize();
4153
+ const themeConfig = await colorService.loadThemeConfig();
4154
+ const variablesConfig = await variableService.loadConfig();
4155
+ const breakpoints = await loadBreakpointConfig();
4156
+ await configService.load();
4157
+ const responsiveScales = configService.getResponsiveScales();
4158
+ const globalLibraries = configService.getLibraries();
4159
+ const componentLibraries = collectComponentLibraries(globalComponents);
4160
+ const outDir = outputDir || join2(projectPaths.project, "next-export");
4161
+ if (existsSync2(outDir)) {
4162
+ rmSync2(outDir, { recursive: true, force: true });
4163
+ }
4164
+ mkdirSync2(outDir, { recursive: true });
4165
+ const appDir = join2(outDir, "app");
4166
+ const componentsDir = join2(appDir, "components");
4167
+ const publicDir = join2(outDir, "public");
4168
+ const scriptsDir = join2(publicDir, "_scripts");
4169
+ for (const d of [appDir, componentsDir, publicDir]) {
4170
+ mkdirSync2(d, { recursive: true });
4171
+ }
4172
+ const pagesDir = projectPaths.pages();
4173
+ if (!existsSync2(pagesDir)) {
4174
+ console.error("Pages directory not found!");
4175
+ return { pages: 0, cmsPages: 0, collections: 0, errors: 1 };
4176
+ }
4177
+ const pageFiles = scanJSONFiles2(pagesDir);
4178
+ if (pageFiles.length === 0) {
4179
+ console.warn("No pages found in ./pages directory");
4180
+ return { pages: 0, cmsPages: 0, collections: 0, errors: 0 };
4181
+ }
4182
+ const slugMappings = [];
4183
+ for (const file of pageFiles) {
4184
+ const pageName = file.replace(".json", "");
4185
+ const basePath = mapPageNameToPath(pageName);
4186
+ const pageContent = await loadJSONFile(join2(pagesDir, file));
4187
+ if (!pageContent) continue;
4188
+ try {
4189
+ const pageData = parseJSON(pageContent);
4190
+ if (pageData.meta?.slugs) {
4191
+ const pageId = basePath === "/" ? "index" : basePath.substring(1);
4192
+ slugMappings.push({ pageId, slugs: pageData.meta.slugs });
4193
+ }
4194
+ } catch {
4195
+ }
4196
+ }
4197
+ const allResults = [];
4198
+ const allInteractiveStyles = /* @__PURE__ */ new Map();
4199
+ const allComponentCSS = /* @__PURE__ */ new Set();
4200
+ const allUtilityClasses = /* @__PURE__ */ new Set();
4201
+ const jsContents = /* @__PURE__ */ new Map();
4202
+ let errorCount = 0;
4203
+ let projectNeedsFormHandler = false;
4204
+ function mergeInteractiveStyles(source) {
4205
+ for (const [key, value] of source) {
4206
+ if (!allInteractiveStyles.has(key)) {
4207
+ allInteractiveStyles.set(key, value);
4208
+ }
4209
+ }
4210
+ }
4211
+ function recordRender(result, urlPath, pageData, pageName) {
4212
+ mergeInteractiveStyles(result.interactiveStylesMap);
4213
+ if (result.componentCSS) allComponentCSS.add(result.componentCSS);
4214
+ for (const c of extractUtilityClassesFromHTML(result.html)) {
4215
+ allUtilityClasses.add(c);
4216
+ }
4217
+ if (result.javascript) {
4218
+ const hash = hashContent3(result.javascript);
4219
+ if (!jsContents.has(hash)) {
4220
+ jsContents.set(hash, result.javascript);
4221
+ }
4222
+ }
4223
+ if (!projectNeedsFormHandler && needsFormHandler(result.html)) {
4224
+ projectNeedsFormHandler = true;
4225
+ }
4226
+ allResults.push({
4227
+ html: result.html,
4228
+ meta: result.meta,
4229
+ title: result.title,
4230
+ javascript: result.javascript,
4231
+ componentCSS: result.componentCSS,
4232
+ locale: result.locale,
4233
+ interactiveStylesMap: result.interactiveStylesMap,
4234
+ urlPath,
4235
+ pageData,
4236
+ pageName
4237
+ });
4238
+ }
4239
+ for (const file of pageFiles) {
4240
+ const pageName = file.replace(".json", "");
4241
+ const basePath = mapPageNameToPath(pageName);
4242
+ const pageContent = await loadJSONFile(join2(pagesDir, file));
4243
+ if (!pageContent) {
4244
+ console.warn(` Skipping ${basePath} (empty file)`);
4245
+ errorCount++;
4246
+ continue;
4247
+ }
4248
+ try {
4249
+ const pageData = parseJSON(pageContent);
4250
+ const isDevBuild = process.env.MENO_DEV_BUILD === "true";
4251
+ if (pageData.meta?.draft === true && !isDevBuild) {
4252
+ continue;
4253
+ }
4254
+ const slugs = pageData.meta?.slugs;
4255
+ for (const localeConfig of i18nConfig.locales) {
4256
+ const locale = localeConfig.code;
4257
+ const isDefault = locale === i18nConfig.defaultLocale;
4258
+ let slug;
4259
+ if (slugs && slugs[locale]) {
4260
+ slug = slugs[locale];
4261
+ } else if (basePath === "/") {
4262
+ slug = "";
4263
+ } else {
4264
+ slug = basePath.substring(1);
4265
+ }
4266
+ const urlPath = isDefault ? slug === "" ? "/" : `/${slug}` : slug === "" ? `/${locale}` : `/${locale}/${slug}`;
4267
+ const result = await renderPageSSR(
4268
+ pageData,
4269
+ globalComponents,
4270
+ urlPath,
4271
+ siteUrl,
4272
+ locale,
4273
+ i18nConfig,
4274
+ slugMappings,
4275
+ void 0,
4276
+ cmsService,
4277
+ true
4278
+ );
4279
+ recordRender(result, urlPath, pageData, pageName);
4280
+ }
4281
+ } catch (error) {
4282
+ const err = error;
4283
+ console.error(` Error rendering ${basePath}:`, err?.message || error);
4284
+ errorCount++;
4285
+ }
4286
+ }
4287
+ const fontPreloads = generateFontPreloadTags();
4288
+ const mergedLibraries = mergeLibraries(globalLibraries, componentLibraries);
4289
+ const buildLibraries = filterLibrariesByContext(mergedLibraries, "build");
4290
+ const inlineContents = /* @__PURE__ */ new Map();
4291
+ const localLibsToCopy = [];
4292
+ for (const css of buildLibraries.css || []) {
4293
+ if (!css.url.startsWith("/")) continue;
4294
+ const shouldInline = css.inline !== false;
4295
+ const relPath = css.url.slice(1);
4296
+ const srcPath = join2(projectPaths.project, relPath);
4297
+ if (!existsSync2(srcPath)) continue;
4298
+ if (shouldInline) {
4299
+ try {
4300
+ inlineContents.set(css.url, await readFile2(srcPath, "utf-8"));
4301
+ } catch {
4302
+ localLibsToCopy.push(relPath);
4303
+ }
4304
+ } else {
4305
+ localLibsToCopy.push(relPath);
4306
+ }
4307
+ }
4308
+ for (const js of buildLibraries.js || []) {
4309
+ if (js.url.startsWith("/")) {
4310
+ const relPath = js.url.slice(1);
4311
+ if (existsSync2(join2(projectPaths.project, relPath))) {
4312
+ localLibsToCopy.push(relPath);
4313
+ }
4314
+ }
4315
+ }
4316
+ const libraryTags = generateLibraryTags(buildLibraries, inlineContents);
4317
+ const defaultTheme = themeConfig.default || "light";
4318
+ const customCode = configService.getCustomCode();
4319
+ const iconsConfig = await loadIconsConfig();
4320
+ const hasDarkFavicon = !!(iconsConfig.favicon && iconsConfig.faviconDark);
4321
+ const faviconTag = iconsConfig.favicon ? `<link rel="icon" href="${iconsConfig.favicon.replace(/"/g, "&quot;")}"${hasDarkFavicon ? ' media="(prefers-color-scheme: light)"' : ""} />` : "";
4322
+ const faviconDarkTag = iconsConfig.faviconDark ? `<link rel="icon" href="${iconsConfig.faviconDark.replace(/"/g, "&quot;")}" media="(prefers-color-scheme: dark)" />` : "";
4323
+ const appleTouchIconTag = iconsConfig.appleTouchIcon ? `<link rel="apple-touch-icon" href="${iconsConfig.appleTouchIcon.replace(/"/g, "&quot;")}" />` : "";
4324
+ const iconTagsHtml = [faviconTag, faviconDarkTag, appleTouchIconTag].filter(Boolean).join("\n ");
4325
+ const remConversionConfig = configService.getRemConversion();
4326
+ const templatesDir = projectPaths.templates();
4327
+ const templateSchemas = [];
4328
+ let cmsPageCount = 0;
4329
+ const cmsEmissions = [];
4330
+ if (existsSync2(templatesDir)) {
4331
+ const templateFiles = readdirSync2(templatesDir).filter((f) => f.endsWith(".json"));
4332
+ for (const file of templateFiles) {
4333
+ const templateContent = await loadJSONFile(join2(templatesDir, file));
4334
+ if (!templateContent) continue;
4335
+ try {
4336
+ const pageData = parseJSON(templateContent);
4337
+ const isDevBuild = process.env.MENO_DEV_BUILD === "true";
4338
+ if (pageData.meta?.draft === true && !isDevBuild) {
4339
+ continue;
4340
+ }
4341
+ if (!isCMSPage2(pageData)) {
4342
+ console.warn(` ${file} is in templates/ but missing meta.source: "cms"`);
4343
+ continue;
4344
+ }
4345
+ const cmsSchema = pageData.meta.cms;
4346
+ templateSchemas.push(cmsSchema);
4347
+ const slugField = cmsSchema.slugField || "slug";
4348
+ const items = await cmsService.queryItems({ collection: cmsSchema.id });
4349
+ const urlPatternWithoutSlash = cmsSchema.urlPattern.replace(/^\//, "");
4350
+ const slugPlaceholderIdx = urlPatternWithoutSlash.indexOf("{{");
4351
+ const pathPrefix = slugPlaceholderIdx > 0 ? urlPatternWithoutSlash.substring(0, slugPlaceholderIdx).replace(/\/$/, "") : "";
4352
+ for (const localeEntry of i18nConfig.locales) {
4353
+ const localeCode = localeEntry.code;
4354
+ const isDefault = localeCode === i18nConfig.defaultLocale;
4355
+ const perSlugData = {};
4356
+ for (const item of items) {
4357
+ if (!isDevBuild && isItemDraftForLocale(item, localeCode)) continue;
4358
+ const itemPath = buildCMSItemPath(cmsSchema.urlPattern, item, slugField, localeCode, i18nConfig);
4359
+ const itemWithUrl = { ...item, _url: itemPath };
4360
+ const fullPath = isDefault ? itemPath : `/${localeCode}${itemPath}`;
4361
+ const result = await renderPageSSR(
4362
+ pageData,
4363
+ globalComponents,
4364
+ fullPath,
4365
+ siteUrl,
4366
+ localeCode,
4367
+ i18nConfig,
4368
+ slugMappings,
4369
+ { cms: itemWithUrl },
4370
+ cmsService,
4371
+ true
4372
+ );
4373
+ mergeInteractiveStyles(result.interactiveStylesMap);
4374
+ if (result.componentCSS) allComponentCSS.add(result.componentCSS);
4375
+ for (const c of extractUtilityClassesFromHTML(result.html)) {
4376
+ allUtilityClasses.add(c);
4377
+ }
4378
+ if (!projectNeedsFormHandler && needsFormHandler(result.html)) {
4379
+ projectNeedsFormHandler = true;
4380
+ }
4381
+ const scriptPaths = [];
4382
+ if (result.javascript) {
4383
+ const hash = hashContent3(result.javascript);
4384
+ if (!jsContents.has(hash)) jsContents.set(hash, result.javascript);
4385
+ scriptPaths.push(`/_scripts/${hash}.js`);
4386
+ }
4387
+ let rawSlug = item[slugField] ?? item._slug ?? item._id;
4388
+ if (isI18nValue(rawSlug)) {
4389
+ rawSlug = resolveI18nValue(rawSlug, localeCode, i18nConfig);
4390
+ }
4391
+ const slugKey = String(rawSlug);
4392
+ perSlugData[slugKey] = {
4393
+ html: result.html,
4394
+ meta: result.meta,
4395
+ title: result.title,
4396
+ scriptPaths
4397
+ };
4398
+ cmsPageCount++;
4399
+ }
4400
+ if (Object.keys(perSlugData).length > 0) {
4401
+ cmsEmissions.push({
4402
+ schema: cmsSchema,
4403
+ locale: localeCode,
4404
+ pathPrefix,
4405
+ isDefaultLocale: isDefault,
4406
+ perSlugData
4407
+ });
4408
+ }
4409
+ }
4410
+ } catch (error) {
4411
+ const err = error;
4412
+ console.error(` Error processing template ${file}:`, err?.message || error);
4413
+ errorCount++;
4414
+ }
4415
+ }
4416
+ }
4417
+ for (const [hash, js] of jsContents) {
4418
+ writePageScript2(js, scriptsDir);
4419
+ void hash;
4420
+ }
4421
+ const mappingClasses = collectAllMappingClasses(globalComponents, breakpoints, responsiveScales);
4422
+ const fontCSS = generateFontCSS();
4423
+ const themeColorCSS = generateThemeColorVariablesCSS(themeConfig);
4424
+ const variablesCSS = generateVariablesCSS(variablesConfig, breakpoints, responsiveScales);
4425
+ const componentCSSCombined = Array.from(allComponentCSS).join("\n");
4426
+ const utilityCSS = allUtilityClasses.size > 0 ? generateUtilityCSS(allUtilityClasses, breakpoints, responsiveScales, remConversionConfig) : "";
4427
+ const interactiveStylesCSS = allInteractiveStyles.size > 0 ? generateAllInteractiveCSS(allInteractiveStyles, breakpoints, remConversionConfig, responsiveScales) : "";
4428
+ const baseCSS = `@layer base {
4429
+ * { margin: 0; padding: 0; box-sizing: border-box; }
4430
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif; }
4431
+ button { background: none; border: none; padding: 0; font: inherit; cursor: pointer; outline: inherit; }
4432
+ img { max-width: 100%; height: auto; }
4433
+ picture { display: block; }
4434
+ .olink { text-decoration: none; display: block; color: inherit; }
4435
+ .oem { display: inline-block; }
4436
+ }`;
4437
+ const safelistDirectives = Array.from(mappingClasses).map((c) => `@source inline("${c}");`).join("\n");
4438
+ const tailwindDirectives = safelistDirectives ? `@import "tailwindcss";
4439
+
4440
+ ${safelistDirectives}` : `@import "tailwindcss";`;
4441
+ const globalCSS = [
4442
+ tailwindDirectives,
4443
+ fontCSS,
4444
+ themeColorCSS,
4445
+ variablesCSS,
4446
+ baseCSS,
4447
+ utilityCSS,
4448
+ componentCSSCombined,
4449
+ interactiveStylesCSS
4450
+ ].filter(Boolean).join("\n\n");
4451
+ await writeFile3(join2(appDir, "globals.css"), globalCSS, "utf-8");
4452
+ const projectName = projectConfig?.name || "Site";
4453
+ const rootLayoutContent = `// Auto-generated by meno-core/build-next. Do not edit.
4454
+ import './globals.css';
4455
+
4456
+ export const metadata = {
4457
+ title: ${JSON.stringify(projectName)},
4458
+ };
4459
+
4460
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
4461
+ return (
4462
+ <html lang=${JSON.stringify(i18nConfig.defaultLocale)} data-theme=${JSON.stringify(defaultTheme)} suppressHydrationWarning>
4463
+ <head>
4464
+ <meta charSet="UTF-8" />
4465
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
4466
+ </head>
4467
+ <body>{children}</body>
4468
+ </html>
4469
+ );
4470
+ }
4471
+ `;
4472
+ await writeFile3(join2(appDir, "layout.tsx"), rootLayoutContent, "utf-8");
4473
+ const rawHeadContent = `// Auto-generated by meno-core/build-next. Do not edit.
4474
+ // Renders an arbitrary HTML string. Used for head fragments emitted by the
4475
+ // SSR pipeline (font preloads, icon links, customCode.head, library tags)
4476
+ // and per-locale data-theme/lang updates. Because this is a server component,
4477
+ // the HTML appears in the statically exported file as-is.
4478
+
4479
+ export default function RawHead({ html, locale, theme }: { html: string; locale: string; theme: string }) {
4480
+ return (
4481
+ <>
4482
+ {/* Force locale/theme on <html> via a lightweight inline script. */}
4483
+ <script
4484
+ dangerouslySetInnerHTML={{
4485
+ __html: \`document.documentElement.lang=\${JSON.stringify(locale)};document.documentElement.dataset.theme=\${JSON.stringify(theme)};\`,
4486
+ }}
4487
+ />
4488
+ {html ? <span data-meno-head dangerouslySetInnerHTML={{ __html: html }} style={{ display: 'none' }} /> : null}
4489
+ </>
4490
+ );
4491
+ }
4492
+ `;
4493
+ await writeFile3(join2(componentsDir, "RawHead.tsx"), rawHeadContent, "utf-8");
4494
+ const metaTagsContent = `// Auto-generated by meno-core/build-next. Do not edit.
4495
+ // Parses a string of self-closing <meta>/<link>/<title> tags emitted by
4496
+ // meno-core's generateMetaTags() into React elements. React 19 hoists these
4497
+ // to <head> automatically.
4498
+
4499
+ type MetaEl =
4500
+ | { kind: 'title'; text: string }
4501
+ | { kind: 'meta' | 'link'; attrs: Record<string, string> };
4502
+
4503
+ function decodeEntities(s: string): string {
4504
+ return s
4505
+ .replace(/&quot;/g, '"')
4506
+ .replace(/&#39;/g, "'")
4507
+ .replace(/&lt;/g, '<')
4508
+ .replace(/&gt;/g, '>')
4509
+ .replace(/&amp;/g, '&');
4510
+ }
4511
+
4512
+ function parseAttrs(src: string): Record<string, string> {
4513
+ const out: Record<string, string> = {};
4514
+ const re = /([a-zA-Z_:][\\w:.-]*)\\s*=\\s*"([^"]*)"/g;
4515
+ let match: RegExpExecArray | null;
4516
+ while ((match = re.exec(src)) !== null) {
4517
+ out[match[1]] = decodeEntities(match[2]);
4518
+ }
4519
+ return out;
4520
+ }
4521
+
4522
+ const JSX_ATTR_NAMES: Record<string, string> = {
4523
+ 'http-equiv': 'httpEquiv',
4524
+ 'hreflang': 'hrefLang',
4525
+ 'crossorigin': 'crossOrigin',
4526
+ 'referrerpolicy': 'referrerPolicy',
4527
+ 'imagesrcset': 'imageSrcSet',
4528
+ 'imagesizes': 'imageSizes',
4529
+ 'fetchpriority': 'fetchPriority',
4530
+ 'class': 'className',
4531
+ 'for': 'htmlFor',
4532
+ 'charset': 'charSet',
4533
+ };
4534
+
4535
+ function jsxAttrName(name: string): string {
4536
+ return JSX_ATTR_NAMES[name] ?? name;
4537
+ }
4538
+
4539
+ function parseMeta(html: string): MetaEl[] {
4540
+ if (!html) return [];
4541
+ const lines = html.split(/\\r?\\n/).map((l) => l.trim()).filter(Boolean);
4542
+ const out: MetaEl[] = [];
4543
+ for (const line of lines) {
4544
+ const titleMatch = line.match(/^<title>([\\s\\S]*?)<\\/title>$/);
4545
+ if (titleMatch) {
4546
+ out.push({ kind: 'title', text: decodeEntities(titleMatch[1]) });
4547
+ continue;
4548
+ }
4549
+ const selfMatch = line.match(/^<(meta|link)\\s+([\\s\\S]*?)\\s*\\/?>$/);
4550
+ if (selfMatch) {
4551
+ out.push({ kind: selfMatch[1] as 'meta' | 'link', attrs: parseAttrs(selfMatch[2]) });
4552
+ }
4553
+ }
4554
+ return out;
4555
+ }
4556
+
4557
+ export default function MetaTags({ html }: { html: string }) {
4558
+ const els = parseMeta(html);
4559
+ return (
4560
+ <>
4561
+ {els.map((el, i) => {
4562
+ if (el.kind === 'title') return <title key={i}>{el.text}</title>;
4563
+ const attrs: Record<string, string> = {};
4564
+ for (const [k, v] of Object.entries(el.attrs)) attrs[jsxAttrName(k)] = v;
4565
+ if (el.kind === 'meta') return <meta key={i} {...attrs} />;
4566
+ return <link key={i} {...attrs} />;
4567
+ })}
4568
+ </>
4569
+ );
4570
+ }
4571
+ `;
4572
+ await writeFile3(join2(componentsDir, "MetaTags.tsx"), metaTagsContent, "utf-8");
4573
+ for (const result of allResults) {
4574
+ const scriptPaths = result.javascript ? [`/_scripts/${hashContent3(result.javascript)}.js`] : [];
4575
+ const route = urlPathToAppRoute(result.urlPath);
4576
+ const targetDir = route.isRoot ? appDir : join2(appDir, route.dir);
4577
+ if (!existsSync2(targetDir)) {
4578
+ mkdirSync2(targetDir, { recursive: true });
4579
+ }
4580
+ const pageFilePath = join2(targetDir, "page.tsx");
4581
+ const depth = route.isRoot ? 0 : route.dir.split("/").length;
4582
+ const rawHeadImportPath = depth === 0 ? "./components/RawHead" : "../".repeat(depth) + "components/RawHead";
4583
+ let content = emitNextPage({
4584
+ html: result.html,
4585
+ meta: result.meta,
4586
+ title: result.title,
4587
+ locale: result.locale,
4588
+ theme: defaultTheme,
4589
+ fontPreloads,
4590
+ libraryTags,
4591
+ scriptPaths,
4592
+ customCode,
4593
+ iconTagsHtml,
4594
+ formHandlerNeeded: projectNeedsFormHandler
4595
+ });
4596
+ content = content.replace(
4597
+ "import RawHead from '../components/RawHead';",
4598
+ `import RawHead from '${rawHeadImportPath}';`
4599
+ );
4600
+ await writeFile3(pageFilePath, content, "utf-8");
4601
+ }
4602
+ for (const emission of cmsEmissions) {
4603
+ const { locale, pathPrefix, isDefaultLocale, perSlugData } = emission;
4604
+ const segments = [];
4605
+ if (!isDefaultLocale) segments.push(locale);
4606
+ if (pathPrefix) {
4607
+ for (const part of pathPrefix.split("/").filter(Boolean)) {
4608
+ segments.push(part);
4609
+ }
4610
+ }
4611
+ segments.push("[slug]");
4612
+ const routeDir = join2(appDir, ...segments);
4613
+ mkdirSync2(routeDir, { recursive: true });
4614
+ const depth = segments.length;
4615
+ const upToApp = "../".repeat(depth);
4616
+ let content = emitNextCMSPage({
4617
+ slugs: Object.keys(perSlugData),
4618
+ perSlugData,
4619
+ locale,
4620
+ theme: defaultTheme,
4621
+ fontPreloads,
4622
+ libraryTags,
4623
+ customCode,
4624
+ iconTagsHtml,
4625
+ formHandlerNeeded: projectNeedsFormHandler
4626
+ });
4627
+ content = content.replace(
4628
+ "import RawHead from '../../components/RawHead';",
4629
+ `import RawHead from '${upToApp}components/RawHead';`
4630
+ );
4631
+ content = content.replace(
4632
+ "import MetaTags from '../../components/MetaTags';",
4633
+ `import MetaTags from '${upToApp}components/MetaTags';`
4634
+ );
4635
+ const pageFile = join2(routeDir, "page.tsx");
4636
+ await writeFile3(pageFile, content, "utf-8");
4637
+ }
4638
+ const imagesSrcDir = join2(projectPaths.project, "images");
4639
+ if (existsSync2(imagesSrcDir)) {
4640
+ copyDirectory2(imagesSrcDir, join2(publicDir, "images"));
4641
+ }
4642
+ const publicAssetDirs = ["fonts", "icons", "videos", "assets"];
4643
+ for (const dir of publicAssetDirs) {
4644
+ const srcAssetDir = join2(projectPaths.project, dir);
4645
+ if (existsSync2(srcAssetDir)) {
4646
+ copyDirectory2(srcAssetDir, join2(publicDir, dir));
4647
+ }
4648
+ }
4649
+ const librariesDir = join2(projectPaths.project, "libraries");
4650
+ if (existsSync2(librariesDir)) {
4651
+ copyDirectory2(librariesDir, join2(publicDir, "libraries"));
4652
+ }
4653
+ for (const relPath of localLibsToCopy) {
4654
+ const srcPath = join2(projectPaths.project, relPath);
4655
+ const destPath = join2(publicDir, relPath);
4656
+ const destDir = destPath.substring(0, destPath.lastIndexOf("/"));
4657
+ if (destDir && !existsSync2(destDir)) mkdirSync2(destDir, { recursive: true });
4658
+ copyFileSync2(srcPath, destPath);
4659
+ }
4660
+ const packageJson = {
4661
+ name: "next-export",
4662
+ type: "module",
4663
+ version: "0.0.1",
4664
+ private: true,
4665
+ scripts: {
4666
+ dev: "next dev",
4667
+ build: "next build",
4668
+ start: "next start"
4669
+ },
4670
+ dependencies: {
4671
+ next: "^15.0.0",
4672
+ react: "^19.0.0",
4673
+ "react-dom": "^19.0.0",
4674
+ "@tailwindcss/postcss": "^4.0.0",
4675
+ tailwindcss: "^4.0.0"
4676
+ },
4677
+ devDependencies: {
4678
+ "@types/node": "^22.0.0",
4679
+ "@types/react": "^19.0.0",
4680
+ "@types/react-dom": "^19.0.0",
4681
+ typescript: "^5.6.0"
4682
+ }
4683
+ };
4684
+ await writeFile3(join2(outDir, "package.json"), JSON.stringify(packageJson, null, 2), "utf-8");
4685
+ const nextConfig = `/** @type {import('next').NextConfig} */
4686
+ const nextConfig = {
4687
+ output: 'export',${siteUrl ? `
4688
+ // Set NEXT_PUBLIC_SITE_URL=${siteUrl} for absolute URLs in metadata.` : ""}
4689
+ images: { unoptimized: true },
4690
+ trailingSlash: false,
4691
+ // The SSR HTML contains arbitrary inline scripts and styles; disable
4692
+ // automatic font/image transforms so they survive unchanged.
4693
+ experimental: {
4694
+ optimizePackageImports: [],
4695
+ },
4696
+ };
4697
+
4698
+ export default nextConfig;
4699
+ `;
4700
+ await writeFile3(join2(outDir, "next.config.mjs"), nextConfig, "utf-8");
4701
+ const postcssConfig = `export default {
4702
+ plugins: {
4703
+ '@tailwindcss/postcss': {},
4704
+ },
4705
+ };
4706
+ `;
4707
+ await writeFile3(join2(outDir, "postcss.config.mjs"), postcssConfig, "utf-8");
4708
+ const tsConfig = {
4709
+ compilerOptions: {
4710
+ target: "ES2022",
4711
+ lib: ["dom", "dom.iterable", "esnext"],
4712
+ allowJs: true,
4713
+ skipLibCheck: true,
4714
+ strict: true,
4715
+ noEmit: true,
4716
+ esModuleInterop: true,
4717
+ module: "esnext",
4718
+ moduleResolution: "bundler",
4719
+ resolveJsonModule: true,
4720
+ isolatedModules: true,
4721
+ jsx: "preserve",
4722
+ incremental: true,
4723
+ plugins: [{ name: "next" }],
4724
+ paths: { "@/*": ["./*"] }
4725
+ },
4726
+ include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
4727
+ exclude: ["node_modules"]
4728
+ };
4729
+ await writeFile3(join2(outDir, "tsconfig.json"), JSON.stringify(tsConfig, null, 2), "utf-8");
4730
+ await writeFile3(
4731
+ join2(outDir, "next-env.d.ts"),
4732
+ `/// <reference types="next" />
4733
+ /// <reference types="next/image-types/global" />
4734
+ `,
4735
+ "utf-8"
4736
+ );
4737
+ const gitignore = [
4738
+ "node_modules",
4739
+ ".next",
4740
+ "out",
4741
+ ".DS_Store",
4742
+ "*.log"
4743
+ ].join("\n") + "\n";
4744
+ await writeFile3(join2(outDir, ".gitignore"), gitignore, "utf-8");
4745
+ const readme = `# Next.js export
4746
+
4747
+ This project was generated by Meno from the SSR-rendered HTML of your pages.
4748
+
4749
+ ## Run locally
4750
+
4751
+ \`\`\`bash
4752
+ npm install
4753
+ npm run dev
4754
+ \`\`\`
4755
+
4756
+ Open http://localhost:3000.
4757
+
4758
+ ## Build a static site
4759
+
4760
+ \`\`\`bash
4761
+ npm run build
4762
+ \`\`\`
4763
+
4764
+ Output lands in \`./out\` (configured via \`output: 'export'\` in \`next.config.mjs\`).
4765
+
4766
+ ## How this differs from a hand-written Next.js app
4767
+
4768
+ - Each page is a server component that embeds the SSR HTML via \`dangerouslySetInnerHTML\`.
4769
+ - Interactive behavior lives in the inline scripts inside that HTML \u2014 they run when the browser parses the static file.
4770
+ - Tailwind v4 is used for utility classes; safelisted classes referenced only by runtime mappings live in \`app/globals.css\`.
4771
+ - Routing is plain App Router: one \`page.tsx\` per URL, with \`[slug]\` dynamic segments for CMS collections.
4772
+ `;
4773
+ await writeFile3(join2(outDir, "README.md"), readme, "utf-8");
4774
+ const collectionCount = templateSchemas.length;
4775
+ void escapeSingleQuoted;
4776
+ return {
4777
+ pages: allResults.length,
4778
+ cmsPages: cmsPageCount,
4779
+ collections: collectionCount,
4780
+ errors: errorCount
4781
+ };
4782
+ }
4783
+
4784
+ // lib/server/webflow/buildWebflow.ts
4785
+ import { existsSync as existsSync3, readdirSync as readdirSync3 } from "fs";
4786
+ import { join as join4 } from "path";
4787
+ init_constants();
4788
+
4789
+ // lib/server/webflow/nodeToWebflow.ts
4790
+ init_constants();
4791
+
4792
+ // lib/server/webflow/types.ts
4793
+ var MENO_BIND_SENTINEL_PREFIX = "__MENO_BIND__:";
4794
+ var MENO_BIND_SENTINEL_SUFFIX = ":__";
4795
+ var MENO_BIND_SENTINEL_RE = /__MENO_BIND__:([^:]+):__/g;
4796
+ var MENO_BIND_SENTINEL_EXACT_RE = /^__MENO_BIND__:([^:]+):__$/;
4797
+ var COLLECTION_LIST_TAG = "__collection_list__";
4798
+
4799
+ // lib/server/webflow/styleMapper.ts
4800
+ var UNITLESS_PROPERTIES = /* @__PURE__ */ new Set([
4801
+ "opacity",
4802
+ "z-index",
4803
+ "flex-grow",
4804
+ "flex-shrink",
4805
+ "flex",
4806
+ "order",
4807
+ "orphans",
4808
+ "widows",
4809
+ "column-count",
4810
+ "font-weight",
4811
+ "tab-size"
4812
+ ]);
4813
+ var TIME_PROPERTIES = /* @__PURE__ */ new Set([
4814
+ "transition-duration",
4815
+ "transition-delay",
4816
+ "animation-duration",
4817
+ "animation-delay"
4818
+ ]);
4819
+ function normalizeZero(cssProp, cssValue) {
4820
+ if (cssValue !== "0") return cssValue;
4821
+ if (UNITLESS_PROPERTIES.has(cssProp)) return cssValue;
4822
+ if (TIME_PROPERTIES.has(cssProp)) return cssValue;
4823
+ return "0px";
4824
+ }
4825
+ var COLOR_PROPS_CAMEL = /* @__PURE__ */ new Set(["color", "backgroundColor", "borderColor"]);
4826
+ function maybeWrapColorVar(camelProp, value) {
4827
+ if (!COLOR_PROPS_CAMEL.has(camelProp)) return value;
4828
+ if (!value) return value;
4829
+ if (value.startsWith("#")) return value;
4830
+ if (value.startsWith("var(")) return value;
4831
+ if (value.includes("(")) return value;
4832
+ if (isCssNamedColor(value)) return value;
4833
+ return `var(--${value})`;
4834
+ }
4835
+ function isStyleMapping4(value) {
4836
+ return typeof value === "object" && value !== null && "_mapping" in value && value._mapping === true;
4837
+ }
4838
+ function isResponsiveStyle4(style) {
4839
+ return "base" in style || "tablet" in style || "mobile" in style;
4840
+ }
4841
+ function toKebabCase(prop) {
4842
+ return prop.replace(/([A-Z])/g, "-$1").toLowerCase();
4843
+ }
4844
+ function splitTopLevel(value) {
4845
+ const out = [];
4846
+ let depth = 0;
4847
+ let buf = "";
4848
+ for (const ch of value.trim()) {
4849
+ if (ch === "(") depth++;
4850
+ else if (ch === ")") depth--;
4851
+ if (depth === 0 && /\s/.test(ch)) {
4852
+ if (buf) {
4853
+ out.push(buf);
4854
+ buf = "";
4855
+ }
4856
+ continue;
4857
+ }
4858
+ buf += ch;
4859
+ }
4860
+ if (buf) out.push(buf);
4861
+ return out;
4862
+ }
4863
+ function expandShorthand(cssProp, cssValue) {
4864
+ if (cssProp !== "margin" && cssProp !== "padding" && cssProp !== "gap") {
4865
+ return null;
4866
+ }
4867
+ const parts = splitTopLevel(cssValue);
4868
+ if (cssProp === "gap") {
4869
+ if (parts.length === 1) {
4870
+ const v = normalizeZero("row-gap", parts[0]);
4871
+ return { "row-gap": v, "column-gap": v };
4872
+ }
4873
+ if (parts.length === 2) {
4874
+ return {
4875
+ "row-gap": normalizeZero("row-gap", parts[0]),
4876
+ "column-gap": normalizeZero("column-gap", parts[1])
4877
+ };
4878
+ }
4879
+ return null;
4880
+ }
4881
+ let top, right, bottom, left;
4882
+ if (parts.length === 1) {
4883
+ top = right = bottom = left = parts[0];
4884
+ } else if (parts.length === 2) {
4885
+ top = bottom = parts[0];
4886
+ right = left = parts[1];
4887
+ } else if (parts.length === 3) {
4888
+ top = parts[0];
4889
+ right = left = parts[1];
4890
+ bottom = parts[2];
4891
+ } else if (parts.length === 4) {
4892
+ [top, right, bottom, left] = parts;
4893
+ } else {
4894
+ return null;
4895
+ }
4896
+ return {
4897
+ [`${cssProp}-top`]: normalizeZero(`${cssProp}-top`, top),
4898
+ [`${cssProp}-right`]: normalizeZero(`${cssProp}-right`, right),
4899
+ [`${cssProp}-bottom`]: normalizeZero(`${cssProp}-bottom`, bottom),
4900
+ [`${cssProp}-left`]: normalizeZero(`${cssProp}-left`, left)
4901
+ };
4902
+ }
4903
+ function styleObjectToCSS(style) {
4904
+ const css = {};
4905
+ for (const [prop, value] of Object.entries(style)) {
4906
+ if (isStyleMapping4(value)) continue;
4907
+ if (value === "" || value === void 0 || value === null) continue;
4908
+ if (typeof value === "boolean" || typeof value === "object") continue;
3643
4909
  const cssProp = toKebabCase(prop);
3644
4910
  let cssValue;
3645
4911
  if (typeof value === "number") {
@@ -3887,8 +5153,8 @@ function buildInstanceStyleCombo(comboName, rootClassName, style, interactiveSty
3887
5153
  }
3888
5154
 
3889
5155
  // lib/server/webflow/nodeToWebflow.ts
3890
- import { readFile as readFile2 } from "fs/promises";
3891
- import { join as join2, basename, extname } from "path";
5156
+ import { readFile as readFile3 } from "fs/promises";
5157
+ import { join as join3, basename, extname } from "path";
3892
5158
  var PROMOTED_TO_WEBFLOW_COMPONENT = /* @__PURE__ */ new Set(["Navigation", "Footer"]);
3893
5159
  function buildElementClass2(ctx, label) {
3894
5160
  const generated = generateElementClassName({
@@ -4030,10 +5296,10 @@ async function maybeInlineLocalImage(element, src) {
4030
5296
  if (!src || isAbsoluteUrl(src)) return;
4031
5297
  const projectRoot = getProjectRoot();
4032
5298
  const rel = src.replace(/^\/+/, "");
4033
- const abs = join2(projectRoot, rel);
5299
+ const abs = join3(projectRoot, rel);
4034
5300
  if (!abs.startsWith(projectRoot)) return;
4035
5301
  try {
4036
- const buf = await readFile2(abs);
5302
+ const buf = await readFile3(abs);
4037
5303
  const ext = extname(abs).toLowerCase();
4038
5304
  const mime = IMAGE_EXT_MIME[ext] || "application/octet-stream";
4039
5305
  element.imageDataBase64 = buf.toString("base64");
@@ -4112,7 +5378,7 @@ function expandResponsiveVarsInto(baseStyle, responsive, ctx) {
4112
5378
  }
4113
5379
  function substituteVarsInStyle(style, ctx) {
4114
5380
  if (!style) return style;
4115
- if (isResponsiveStyleObject(style)) {
5381
+ if (isResponsiveStyleObject2(style)) {
4116
5382
  const out = {};
4117
5383
  for (const [bp, obj] of Object.entries(style)) {
4118
5384
  if (!obj || typeof obj !== "object") continue;
@@ -4188,12 +5454,12 @@ function menoBreakpointToWebflowTier(bpName, breakpoints) {
4188
5454
  if (w < 1920) return "xl";
4189
5455
  return "xxl";
4190
5456
  }
4191
- function isResponsiveStyleObject(style) {
5457
+ function isResponsiveStyleObject2(style) {
4192
5458
  return "base" in style || "tablet" in style || "mobile" in style;
4193
5459
  }
4194
5460
  function extractBaseColor(style, ctx, instanceProps) {
4195
5461
  if (!style) return void 0;
4196
- const flat = isResponsiveStyleObject(style) ? style.base : style;
5462
+ const flat = isResponsiveStyleObject2(style) ? style.base : style;
4197
5463
  const c = flat?.color;
4198
5464
  if (typeof c === "string") return c;
4199
5465
  if (c && typeof c === "object" && c._mapping === true) {
@@ -4231,7 +5497,7 @@ function resolveTemplatesInStyleObject(style, props) {
4231
5497
  }
4232
5498
  function resolveStyleTemplates(style, props) {
4233
5499
  if (!style || !props) return style;
4234
- if (isResponsiveStyleObject(style)) {
5500
+ if (isResponsiveStyleObject2(style)) {
4235
5501
  const result = {};
4236
5502
  for (const [bp, styleObj] of Object.entries(style)) {
4237
5503
  if (styleObj && typeof styleObj === "object") {
@@ -4282,7 +5548,7 @@ function templatesToSyntheticMappings(style, componentDefaults, instanceProps) {
4282
5548
  }
4283
5549
  function convertStyleTemplatesToMappings(style, componentDefaults, instanceProps) {
4284
5550
  if (!style || !componentDefaults || !instanceProps) return style;
4285
- if (isResponsiveStyleObject(style)) {
5551
+ if (isResponsiveStyleObject2(style)) {
4286
5552
  const result = {};
4287
5553
  for (const [bp, styleObj] of Object.entries(style)) {
4288
5554
  if (styleObj && typeof styleObj === "object") {
@@ -5239,20 +6505,20 @@ async function convertChildren(children, ctx, instanceProps) {
5239
6505
  }
5240
6506
 
5241
6507
  // lib/server/webflow/buildWebflow.ts
5242
- function scanJSONFiles2(dir, prefix = "") {
6508
+ function scanJSONFiles3(dir, prefix = "") {
5243
6509
  const results = [];
5244
- if (!existsSync2(dir)) return results;
5245
- const entries = readdirSync2(dir, { withFileTypes: true });
6510
+ if (!existsSync3(dir)) return results;
6511
+ const entries = readdirSync3(dir, { withFileTypes: true });
5246
6512
  for (const entry of entries) {
5247
6513
  if (entry.isFile() && entry.name.endsWith(".json")) {
5248
6514
  results.push(prefix ? `${prefix}/${entry.name}` : entry.name);
5249
6515
  } else if (entry.isDirectory()) {
5250
- results.push(...scanJSONFiles2(join3(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name));
6516
+ results.push(...scanJSONFiles3(join4(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name));
5251
6517
  }
5252
6518
  }
5253
6519
  return results;
5254
6520
  }
5255
- function isCMSPage2(pageData) {
6521
+ function isCMSPage3(pageData) {
5256
6522
  return pageData.meta?.source === "cms" && !!pageData.meta?.cms;
5257
6523
  }
5258
6524
  function flattenCMSItemForLocale(item, locale, i18nConfig) {
@@ -5297,9 +6563,9 @@ function scanAssets(projectRoot) {
5297
6563
  { dir: "assets", type: "file" }
5298
6564
  ];
5299
6565
  for (const { dir, type } of assetDirs) {
5300
- const fullDir = join3(projectRoot, dir);
5301
- if (!existsSync2(fullDir)) continue;
5302
- const files = scanJSONFiles2(fullDir).map((f) => f.replace(".json", ""));
6566
+ const fullDir = join4(projectRoot, dir);
6567
+ if (!existsSync3(fullDir)) continue;
6568
+ const files = scanJSONFiles3(fullDir).map((f) => f.replace(".json", ""));
5303
6569
  const allFiles = scanAllFiles(fullDir);
5304
6570
  for (const file of allFiles) {
5305
6571
  assets.push({
@@ -5313,14 +6579,14 @@ function scanAssets(projectRoot) {
5313
6579
  }
5314
6580
  function scanAllFiles(dir, prefix = "") {
5315
6581
  const results = [];
5316
- if (!existsSync2(dir)) return results;
5317
- const entries = readdirSync2(dir, { withFileTypes: true });
6582
+ if (!existsSync3(dir)) return results;
6583
+ const entries = readdirSync3(dir, { withFileTypes: true });
5318
6584
  for (const entry of entries) {
5319
6585
  const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
5320
6586
  if (entry.isFile()) {
5321
6587
  results.push(relativePath);
5322
6588
  } else if (entry.isDirectory()) {
5323
- results.push(...scanAllFiles(join3(dir, entry.name), relativePath));
6589
+ results.push(...scanAllFiles(join4(dir, entry.name), relativePath));
5324
6590
  }
5325
6591
  }
5326
6592
  return results;
@@ -5379,10 +6645,10 @@ async function buildWebflowPayload(options) {
5379
6645
  await configService.load();
5380
6646
  const responsiveScales = configService.getResponsiveScales();
5381
6647
  const pagesDir = projectPaths.pages();
5382
- if (!existsSync2(pagesDir)) {
6648
+ if (!existsSync3(pagesDir)) {
5383
6649
  return emptyPayload();
5384
6650
  }
5385
- const pageFiles = scanJSONFiles2(pagesDir);
6651
+ const pageFiles = scanJSONFiles3(pagesDir);
5386
6652
  if (pageFiles.length === 0) {
5387
6653
  return emptyPayload();
5388
6654
  }
@@ -5390,7 +6656,7 @@ async function buildWebflowPayload(options) {
5390
6656
  for (const file of pageFiles) {
5391
6657
  const pageName = file.replace(".json", "");
5392
6658
  const basePath = mapPageNameToPath(pageName);
5393
- const pageContent = await loadJSONFile(join3(pagesDir, file));
6659
+ const pageContent = await loadJSONFile(join4(pagesDir, file));
5394
6660
  if (!pageContent) continue;
5395
6661
  try {
5396
6662
  const pageData = parseJSON(pageContent);
@@ -5412,7 +6678,7 @@ async function buildWebflowPayload(options) {
5412
6678
  for (const file of pageFiles) {
5413
6679
  const pageName = file.replace(".json", "");
5414
6680
  const basePath = mapPageNameToPath(pageName);
5415
- const pageContent = await loadJSONFile(join3(pagesDir, file));
6681
+ const pageContent = await loadJSONFile(join4(pagesDir, file));
5416
6682
  if (!pageContent) continue;
5417
6683
  try {
5418
6684
  const pageData = parseJSON(pageContent);
@@ -5481,15 +6747,15 @@ async function buildWebflowPayload(options) {
5481
6747
  }
5482
6748
  }
5483
6749
  const templatesDir = projectPaths.templates();
5484
- if (existsSync2(templatesDir)) {
5485
- const templateFiles = readdirSync2(templatesDir).filter((f) => f.endsWith(".json"));
6750
+ if (existsSync3(templatesDir)) {
6751
+ const templateFiles = readdirSync3(templatesDir).filter((f) => f.endsWith(".json"));
5486
6752
  for (const file of templateFiles) {
5487
- const templateContent = await loadJSONFile(join3(templatesDir, file));
6753
+ const templateContent = await loadJSONFile(join4(templatesDir, file));
5488
6754
  if (!templateContent) continue;
5489
6755
  try {
5490
6756
  const pageData = parseJSON(templateContent);
5491
6757
  if (pageData.meta?.draft === true) continue;
5492
- if (!isCMSPage2(pageData)) continue;
6758
+ if (!isCMSPage3(pageData)) continue;
5493
6759
  const cmsSchema = pageData.meta.cms;
5494
6760
  const items = await cmsService.queryItems({ collection: cmsSchema.id });
5495
6761
  if (items.length === 0) continue;
@@ -5637,7 +6903,7 @@ function emptyPayload() {
5637
6903
  }
5638
6904
 
5639
6905
  // lib/server/webflow/templateWrapper.ts
5640
- import { readFile as readFile3 } from "fs/promises";
6906
+ import { readFile as readFile4 } from "fs/promises";
5641
6907
  var cachedTemplate = null;
5642
6908
  async function getWebflowTemplate(appName) {
5643
6909
  if (cachedTemplate) return cachedTemplate;
@@ -5649,7 +6915,7 @@ async function getWebflowTemplate(appName) {
5649
6915
  }
5650
6916
  async function wrapInWebflowTemplate(html, manifestPath) {
5651
6917
  try {
5652
- const manifest = JSON.parse(await readFile3(manifestPath, "utf-8"));
6918
+ const manifest = JSON.parse(await readFile4(manifestPath, "utf-8"));
5653
6919
  const template = await getWebflowTemplate(manifest.name || "Meno Import");
5654
6920
  const headMatch = html.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
5655
6921
  const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
@@ -5670,6 +6936,7 @@ export {
5670
6936
  ComponentService,
5671
6937
  ConfigService,
5672
6938
  DEFAULT_SIZES,
6939
+ DraftPageStore,
5673
6940
  EnumService,
5674
6941
  FileSystemCMSProvider,
5675
6942
  FileSystemPageProvider,
@@ -5688,6 +6955,7 @@ export {
5688
6955
  buildComponentHTML,
5689
6956
  buildImageMetadataMap,
5690
6957
  buildLineMap,
6958
+ buildNextProject,
5691
6959
  buildStaticPages,
5692
6960
  buildWebflowPayload,
5693
6961
  bundleFile,