gazetta 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (478) hide show
  1. package/admin-dist/assets/index-BO9-CXmW.css +1 -0
  2. package/admin-dist/assets/index-Ufu8zZH_.js +668 -0
  3. package/admin-dist/assets/{vendor-primevue-BnR1c_bQ.js → vendor-primevue-C0Q_YTCb.js} +330 -431
  4. package/admin-dist/assets/vendor-vue-D3wBSmDf.js +1 -0
  5. package/admin-dist/index.html +4 -4
  6. package/dist/admin-api/error-response.d.ts +21 -0
  7. package/dist/admin-api/error-response.d.ts.map +1 -0
  8. package/dist/admin-api/error-response.js +12 -0
  9. package/dist/admin-api/error-response.js.map +1 -0
  10. package/dist/admin-api/index.d.ts +19 -6
  11. package/dist/admin-api/index.d.ts.map +1 -1
  12. package/dist/admin-api/index.js +137 -21
  13. package/dist/admin-api/index.js.map +1 -1
  14. package/dist/admin-api/routes/assets.d.ts +16 -0
  15. package/dist/admin-api/routes/assets.d.ts.map +1 -0
  16. package/dist/admin-api/routes/assets.js +433 -0
  17. package/dist/admin-api/routes/assets.js.map +1 -0
  18. package/dist/admin-api/routes/compare.d.ts +2 -1
  19. package/dist/admin-api/routes/compare.d.ts.map +1 -1
  20. package/dist/admin-api/routes/compare.js +33 -24
  21. package/dist/admin-api/routes/compare.js.map +1 -1
  22. package/dist/admin-api/routes/fields.d.ts +2 -2
  23. package/dist/admin-api/routes/fields.d.ts.map +1 -1
  24. package/dist/admin-api/routes/fields.js +10 -3
  25. package/dist/admin-api/routes/fields.js.map +1 -1
  26. package/dist/admin-api/routes/fragments.d.ts +2 -3
  27. package/dist/admin-api/routes/fragments.d.ts.map +1 -1
  28. package/dist/admin-api/routes/fragments.js +120 -21
  29. package/dist/admin-api/routes/fragments.js.map +1 -1
  30. package/dist/admin-api/routes/history.d.ts +23 -0
  31. package/dist/admin-api/routes/history.d.ts.map +1 -0
  32. package/dist/admin-api/routes/history.js +143 -0
  33. package/dist/admin-api/routes/history.js.map +1 -0
  34. package/dist/admin-api/routes/pages.d.ts +2 -3
  35. package/dist/admin-api/routes/pages.d.ts.map +1 -1
  36. package/dist/admin-api/routes/pages.js +153 -22
  37. package/dist/admin-api/routes/pages.js.map +1 -1
  38. package/dist/admin-api/routes/preview.d.ts +2 -2
  39. package/dist/admin-api/routes/preview.d.ts.map +1 -1
  40. package/dist/admin-api/routes/preview.js +50 -15
  41. package/dist/admin-api/routes/preview.js.map +1 -1
  42. package/dist/admin-api/routes/publish.d.ts +2 -1
  43. package/dist/admin-api/routes/publish.d.ts.map +1 -1
  44. package/dist/admin-api/routes/publish.js +259 -79
  45. package/dist/admin-api/routes/publish.js.map +1 -1
  46. package/dist/admin-api/routes/site.d.ts +2 -2
  47. package/dist/admin-api/routes/site.d.ts.map +1 -1
  48. package/dist/admin-api/routes/site.js +27 -4
  49. package/dist/admin-api/routes/site.js.map +1 -1
  50. package/dist/admin-api/routes/templates.d.ts +2 -2
  51. package/dist/admin-api/routes/templates.d.ts.map +1 -1
  52. package/dist/admin-api/routes/templates.js +19 -9
  53. package/dist/admin-api/routes/templates.js.map +1 -1
  54. package/dist/admin-api/schemas/assets.d.ts +48 -0
  55. package/dist/admin-api/schemas/assets.d.ts.map +1 -0
  56. package/dist/admin-api/schemas/assets.js +44 -0
  57. package/dist/admin-api/schemas/assets.js.map +1 -0
  58. package/dist/admin-api/schemas/compare.d.ts +29 -0
  59. package/dist/admin-api/schemas/compare.d.ts.map +1 -0
  60. package/dist/admin-api/schemas/compare.js +30 -0
  61. package/dist/admin-api/schemas/compare.js.map +1 -0
  62. package/dist/admin-api/schemas/dependents.d.ts +15 -0
  63. package/dist/admin-api/schemas/dependents.d.ts.map +1 -0
  64. package/dist/admin-api/schemas/dependents.js +14 -0
  65. package/dist/admin-api/schemas/dependents.js.map +1 -0
  66. package/dist/admin-api/schemas/fetch.d.ts +12 -0
  67. package/dist/admin-api/schemas/fetch.d.ts.map +1 -0
  68. package/dist/admin-api/schemas/fetch.js +11 -0
  69. package/dist/admin-api/schemas/fetch.js.map +1 -0
  70. package/dist/admin-api/schemas/fields.d.ts +11 -0
  71. package/dist/admin-api/schemas/fields.d.ts.map +1 -0
  72. package/dist/admin-api/schemas/fields.js +11 -0
  73. package/dist/admin-api/schemas/fields.js.map +1 -0
  74. package/dist/admin-api/schemas/fragments.d.ts +27 -0
  75. package/dist/admin-api/schemas/fragments.d.ts.map +1 -0
  76. package/dist/admin-api/schemas/fragments.js +26 -0
  77. package/dist/admin-api/schemas/fragments.js.map +1 -0
  78. package/dist/admin-api/schemas/history.d.ts +73 -0
  79. package/dist/admin-api/schemas/history.d.ts.map +1 -0
  80. package/dist/admin-api/schemas/history.js +35 -0
  81. package/dist/admin-api/schemas/history.js.map +1 -0
  82. package/dist/admin-api/schemas/index.d.ts +34 -0
  83. package/dist/admin-api/schemas/index.d.ts.map +1 -0
  84. package/dist/admin-api/schemas/index.js +34 -0
  85. package/dist/admin-api/schemas/index.js.map +1 -0
  86. package/dist/admin-api/schemas/pages.d.ts +46 -0
  87. package/dist/admin-api/schemas/pages.d.ts.map +1 -0
  88. package/dist/admin-api/schemas/pages.js +47 -0
  89. package/dist/admin-api/schemas/pages.js.map +1 -0
  90. package/dist/admin-api/schemas/publish.d.ts +67 -0
  91. package/dist/admin-api/schemas/publish.d.ts.map +1 -0
  92. package/dist/admin-api/schemas/publish.js +60 -0
  93. package/dist/admin-api/schemas/publish.js.map +1 -0
  94. package/dist/admin-api/schemas/site.d.ts +28 -0
  95. package/dist/admin-api/schemas/site.d.ts.map +1 -0
  96. package/dist/admin-api/schemas/site.js +24 -0
  97. package/dist/admin-api/schemas/site.js.map +1 -0
  98. package/dist/admin-api/schemas/targets.d.ts +36 -0
  99. package/dist/admin-api/schemas/targets.d.ts.map +1 -0
  100. package/dist/admin-api/schemas/targets.js +19 -0
  101. package/dist/admin-api/schemas/targets.js.map +1 -0
  102. package/dist/admin-api/schemas/templates.d.ts +17 -0
  103. package/dist/admin-api/schemas/templates.d.ts.map +1 -0
  104. package/dist/admin-api/schemas/templates.js +16 -0
  105. package/dist/admin-api/schemas/templates.js.map +1 -0
  106. package/dist/admin-api/source-context.d.ts +158 -0
  107. package/dist/admin-api/source-context.d.ts.map +1 -0
  108. package/dist/admin-api/source-context.js +92 -0
  109. package/dist/admin-api/source-context.js.map +1 -0
  110. package/dist/app.js +1 -1
  111. package/dist/app.js.map +1 -1
  112. package/dist/assemble.d.ts.map +1 -1
  113. package/dist/assemble.js +4 -1
  114. package/dist/assemble.js.map +1 -1
  115. package/dist/assets/analyze-audio.d.ts +3 -0
  116. package/dist/assets/analyze-audio.d.ts.map +1 -0
  117. package/dist/assets/analyze-audio.js +80 -0
  118. package/dist/assets/analyze-audio.js.map +1 -0
  119. package/dist/assets/analyze-image.d.ts +19 -0
  120. package/dist/assets/analyze-image.d.ts.map +1 -0
  121. package/dist/assets/analyze-image.js +123 -0
  122. package/dist/assets/analyze-image.js.map +1 -0
  123. package/dist/assets/analyze.d.ts +94 -0
  124. package/dist/assets/analyze.d.ts.map +1 -0
  125. package/dist/assets/analyze.js +45 -0
  126. package/dist/assets/analyze.js.map +1 -0
  127. package/dist/assets/asset-deps.d.ts +30 -0
  128. package/dist/assets/asset-deps.d.ts.map +1 -0
  129. package/dist/assets/asset-deps.js +42 -0
  130. package/dist/assets/asset-deps.js.map +1 -0
  131. package/dist/assets/asset-paths.d.ts +155 -0
  132. package/dist/assets/asset-paths.d.ts.map +1 -0
  133. package/dist/assets/asset-paths.js +197 -0
  134. package/dist/assets/asset-paths.js.map +1 -0
  135. package/dist/assets/delete.d.ts +75 -0
  136. package/dist/assets/delete.d.ts.map +1 -0
  137. package/dist/assets/delete.js +82 -0
  138. package/dist/assets/delete.js.map +1 -0
  139. package/dist/assets/errors.d.ts +241 -0
  140. package/dist/assets/errors.d.ts.map +1 -0
  141. package/dist/assets/errors.js +300 -0
  142. package/dist/assets/errors.js.map +1 -0
  143. package/dist/assets/find-refs.d.ts +37 -0
  144. package/dist/assets/find-refs.d.ts.map +1 -0
  145. package/dist/assets/find-refs.js +35 -0
  146. package/dist/assets/find-refs.js.map +1 -0
  147. package/dist/assets/hash.d.ts +13 -0
  148. package/dist/assets/hash.d.ts.map +1 -0
  149. package/dist/assets/hash.js +43 -0
  150. package/dist/assets/hash.js.map +1 -0
  151. package/dist/assets/image-metadata.d.ts +11 -0
  152. package/dist/assets/image-metadata.d.ts.map +1 -0
  153. package/dist/assets/image-metadata.js +31 -0
  154. package/dist/assets/image-metadata.js.map +1 -0
  155. package/dist/assets/ingest-locale.d.ts +86 -0
  156. package/dist/assets/ingest-locale.d.ts.map +1 -0
  157. package/dist/assets/ingest-locale.js +209 -0
  158. package/dist/assets/ingest-locale.js.map +1 -0
  159. package/dist/assets/ingest.d.ts +96 -0
  160. package/dist/assets/ingest.d.ts.map +1 -0
  161. package/dist/assets/ingest.js +308 -0
  162. package/dist/assets/ingest.js.map +1 -0
  163. package/dist/assets/kind-compat.d.ts +34 -0
  164. package/dist/assets/kind-compat.d.ts.map +1 -0
  165. package/dist/assets/kind-compat.js +33 -0
  166. package/dist/assets/kind-compat.js.map +1 -0
  167. package/dist/assets/list.d.ts +46 -0
  168. package/dist/assets/list.d.ts.map +1 -0
  169. package/dist/assets/list.js +102 -0
  170. package/dist/assets/list.js.map +1 -0
  171. package/dist/assets/manifest-default.d.ts +56 -0
  172. package/dist/assets/manifest-default.d.ts.map +1 -0
  173. package/dist/assets/manifest-default.js +120 -0
  174. package/dist/assets/manifest-default.js.map +1 -0
  175. package/dist/assets/manifest-filename.d.ts +52 -0
  176. package/dist/assets/manifest-filename.d.ts.map +1 -0
  177. package/dist/assets/manifest-filename.js +104 -0
  178. package/dist/assets/manifest-filename.js.map +1 -0
  179. package/dist/assets/manifest-locale.d.ts +60 -0
  180. package/dist/assets/manifest-locale.d.ts.map +1 -0
  181. package/dist/assets/manifest-locale.js +206 -0
  182. package/dist/assets/manifest-locale.js.map +1 -0
  183. package/dist/assets/manifest-merge.d.ts +66 -0
  184. package/dist/assets/manifest-merge.d.ts.map +1 -0
  185. package/dist/assets/manifest-merge.js +82 -0
  186. package/dist/assets/manifest-merge.js.map +1 -0
  187. package/dist/assets/manifest.d.ts +83 -0
  188. package/dist/assets/manifest.d.ts.map +1 -0
  189. package/dist/assets/manifest.js +93 -0
  190. package/dist/assets/manifest.js.map +1 -0
  191. package/dist/assets/mime-sniff.d.ts +18 -0
  192. package/dist/assets/mime-sniff.d.ts.map +1 -0
  193. package/dist/assets/mime-sniff.js +84 -0
  194. package/dist/assets/mime-sniff.js.map +1 -0
  195. package/dist/assets/preprocess-svg.d.ts +3 -0
  196. package/dist/assets/preprocess-svg.d.ts.map +1 -0
  197. package/dist/assets/preprocess-svg.js +49 -0
  198. package/dist/assets/preprocess-svg.js.map +1 -0
  199. package/dist/assets/preprocess.d.ts +62 -0
  200. package/dist/assets/preprocess.d.ts.map +1 -0
  201. package/dist/assets/preprocess.js +86 -0
  202. package/dist/assets/preprocess.js.map +1 -0
  203. package/dist/assets/publish-plan.d.ts +41 -0
  204. package/dist/assets/publish-plan.d.ts.map +1 -0
  205. package/dist/assets/publish-plan.js +49 -0
  206. package/dist/assets/publish-plan.js.map +1 -0
  207. package/dist/assets/publish.d.ts +33 -0
  208. package/dist/assets/publish.d.ts.map +1 -0
  209. package/dist/assets/publish.js +81 -0
  210. package/dist/assets/publish.js.map +1 -0
  211. package/dist/assets/refs.d.ts +37 -0
  212. package/dist/assets/refs.d.ts.map +1 -0
  213. package/dist/assets/refs.js +33 -0
  214. package/dist/assets/refs.js.map +1 -0
  215. package/dist/assets/remove-override.d.ts +42 -0
  216. package/dist/assets/remove-override.d.ts.map +1 -0
  217. package/dist/assets/remove-override.js +53 -0
  218. package/dist/assets/remove-override.js.map +1 -0
  219. package/dist/assets/rename.d.ts +43 -0
  220. package/dist/assets/rename.d.ts.map +1 -0
  221. package/dist/assets/rename.js +271 -0
  222. package/dist/assets/rename.js.map +1 -0
  223. package/dist/assets/replace.d.ts +37 -0
  224. package/dist/assets/replace.d.ts.map +1 -0
  225. package/dist/assets/replace.js +195 -0
  226. package/dist/assets/replace.js.map +1 -0
  227. package/dist/assets/resolve.d.ts +141 -0
  228. package/dist/assets/resolve.d.ts.map +1 -0
  229. package/dist/assets/resolve.js +381 -0
  230. package/dist/assets/resolve.js.map +1 -0
  231. package/dist/assets/rewrite-manifest-asset-ref.d.ts +44 -0
  232. package/dist/assets/rewrite-manifest-asset-ref.d.ts.map +1 -0
  233. package/dist/assets/rewrite-manifest-asset-ref.js +51 -0
  234. package/dist/assets/rewrite-manifest-asset-ref.js.map +1 -0
  235. package/dist/assets/scan-manifest-for-asset.d.ts +63 -0
  236. package/dist/assets/scan-manifest-for-asset.d.ts.map +1 -0
  237. package/dist/assets/scan-manifest-for-asset.js +105 -0
  238. package/dist/assets/scan-manifest-for-asset.js.map +1 -0
  239. package/dist/assets/serve-route.d.ts +45 -0
  240. package/dist/assets/serve-route.d.ts.map +1 -0
  241. package/dist/assets/serve-route.js +123 -0
  242. package/dist/assets/serve-route.js.map +1 -0
  243. package/dist/assets/svg-sanitize.d.ts +38 -0
  244. package/dist/assets/svg-sanitize.d.ts.map +1 -0
  245. package/dist/assets/svg-sanitize.js +209 -0
  246. package/dist/assets/svg-sanitize.js.map +1 -0
  247. package/dist/assets/update-metadata.d.ts +61 -0
  248. package/dist/assets/update-metadata.d.ts.map +1 -0
  249. package/dist/assets/update-metadata.js +82 -0
  250. package/dist/assets/update-metadata.js.map +1 -0
  251. package/dist/assets/url.d.ts +82 -0
  252. package/dist/assets/url.d.ts.map +1 -0
  253. package/dist/assets/url.js +103 -0
  254. package/dist/assets/url.js.map +1 -0
  255. package/dist/assets/validate.d.ts +74 -0
  256. package/dist/assets/validate.d.ts.map +1 -0
  257. package/dist/assets/validate.js +136 -0
  258. package/dist/assets/validate.js.map +1 -0
  259. package/dist/assets/variants.d.ts +23 -0
  260. package/dist/assets/variants.d.ts.map +1 -0
  261. package/dist/assets/variants.js +74 -0
  262. package/dist/assets/variants.js.map +1 -0
  263. package/dist/cli/assets-cli.d.ts +58 -0
  264. package/dist/cli/assets-cli.d.ts.map +1 -0
  265. package/dist/cli/assets-cli.js +233 -0
  266. package/dist/cli/assets-cli.js.map +1 -0
  267. package/dist/cli/assets-display.d.ts +112 -0
  268. package/dist/cli/assets-display.d.ts.map +1 -0
  269. package/dist/cli/assets-display.js +106 -0
  270. package/dist/cli/assets-display.js.map +1 -0
  271. package/dist/cli/bootstrap.d.ts +46 -0
  272. package/dist/cli/bootstrap.d.ts.map +1 -0
  273. package/dist/cli/bootstrap.js +84 -0
  274. package/dist/cli/bootstrap.js.map +1 -0
  275. package/dist/cli/history.d.ts +45 -0
  276. package/dist/cli/history.d.ts.map +1 -0
  277. package/dist/cli/history.js +165 -0
  278. package/dist/cli/history.js.map +1 -0
  279. package/dist/cli/index.js +689 -128
  280. package/dist/cli/index.js.map +1 -1
  281. package/dist/compare.d.ts +8 -5
  282. package/dist/compare.d.ts.map +1 -1
  283. package/dist/compare.js +62 -20
  284. package/dist/compare.js.map +1 -1
  285. package/dist/content-root.d.ts +38 -0
  286. package/dist/content-root.d.ts.map +1 -0
  287. package/dist/content-root.js +29 -0
  288. package/dist/content-root.js.map +1 -0
  289. package/dist/dep-sidecars.d.ts +127 -0
  290. package/dist/dep-sidecars.d.ts.map +1 -0
  291. package/dist/dep-sidecars.js +122 -0
  292. package/dist/dep-sidecars.js.map +1 -0
  293. package/dist/editor/AssetEmbeddedWidget.d.ts +3 -0
  294. package/dist/editor/AssetEmbeddedWidget.d.ts.map +1 -0
  295. package/dist/editor/AssetEmbeddedWidget.js +146 -0
  296. package/dist/editor/AssetEmbeddedWidget.js.map +1 -0
  297. package/dist/editor/mount.d.ts +12 -1
  298. package/dist/editor/mount.d.ts.map +1 -1
  299. package/dist/editor/mount.js +96 -33
  300. package/dist/editor/mount.js.map +1 -1
  301. package/dist/format.d.ts +44 -0
  302. package/dist/format.d.ts.map +1 -0
  303. package/dist/format.js +65 -0
  304. package/dist/format.js.map +1 -0
  305. package/dist/fragment-deps.d.ts +24 -0
  306. package/dist/fragment-deps.d.ts.map +1 -0
  307. package/dist/fragment-deps.js +20 -0
  308. package/dist/fragment-deps.js.map +1 -0
  309. package/dist/hash.d.ts +34 -9
  310. package/dist/hash.d.ts.map +1 -1
  311. package/dist/hash.js +60 -21
  312. package/dist/hash.js.map +1 -1
  313. package/dist/history-provider.d.ts +49 -0
  314. package/dist/history-provider.d.ts.map +1 -0
  315. package/dist/history-provider.js +248 -0
  316. package/dist/history-provider.js.map +1 -0
  317. package/dist/history-recorder.d.ts +102 -0
  318. package/dist/history-recorder.d.ts.map +1 -0
  319. package/dist/history-recorder.js +168 -0
  320. package/dist/history-recorder.js.map +1 -0
  321. package/dist/history-restorer.d.ts +46 -0
  322. package/dist/history-restorer.d.ts.map +1 -0
  323. package/dist/history-restorer.js +137 -0
  324. package/dist/history-restorer.js.map +1 -0
  325. package/dist/history.d.ts +129 -0
  326. package/dist/history.d.ts.map +1 -0
  327. package/dist/history.js +25 -0
  328. package/dist/history.js.map +1 -0
  329. package/dist/index.d.ts +27 -7
  330. package/dist/index.d.ts.map +1 -1
  331. package/dist/index.js +16 -4
  332. package/dist/index.js.map +1 -1
  333. package/dist/locale.d.ts +94 -0
  334. package/dist/locale.d.ts.map +1 -0
  335. package/dist/locale.js +188 -0
  336. package/dist/locale.js.map +1 -0
  337. package/dist/manifest.d.ts.map +1 -1
  338. package/dist/manifest.js +16 -1
  339. package/dist/manifest.js.map +1 -1
  340. package/dist/providers/_atomic-write.d.ts +9 -0
  341. package/dist/providers/_atomic-write.d.ts.map +1 -0
  342. package/dist/providers/_atomic-write.js +72 -0
  343. package/dist/providers/_atomic-write.js.map +1 -0
  344. package/dist/providers/_rm-ignore-missing.d.ts +31 -0
  345. package/dist/providers/_rm-ignore-missing.d.ts.map +1 -0
  346. package/dist/providers/_rm-ignore-missing.js +12 -0
  347. package/dist/providers/_rm-ignore-missing.js.map +1 -0
  348. package/dist/providers/_stream-interop.d.ts +23 -0
  349. package/dist/providers/_stream-interop.d.ts.map +1 -0
  350. package/dist/providers/_stream-interop.js +21 -0
  351. package/dist/providers/_stream-interop.js.map +1 -0
  352. package/dist/providers/azure-blob.d.ts.map +1 -1
  353. package/dist/providers/azure-blob.js +60 -0
  354. package/dist/providers/azure-blob.js.map +1 -1
  355. package/dist/providers/filesystem.d.ts +4 -0
  356. package/dist/providers/filesystem.d.ts.map +1 -1
  357. package/dist/providers/filesystem.js +63 -2
  358. package/dist/providers/filesystem.js.map +1 -1
  359. package/dist/providers/s3.d.ts.map +1 -1
  360. package/dist/providers/s3.js +107 -16
  361. package/dist/providers/s3.js.map +1 -1
  362. package/dist/publish-locale.d.ts +44 -0
  363. package/dist/publish-locale.d.ts.map +1 -0
  364. package/dist/publish-locale.js +103 -0
  365. package/dist/publish-locale.js.map +1 -0
  366. package/dist/publish-rendered.d.ts +52 -21
  367. package/dist/publish-rendered.d.ts.map +1 -1
  368. package/dist/publish-rendered.js +144 -87
  369. package/dist/publish-rendered.js.map +1 -1
  370. package/dist/publish.d.ts +18 -19
  371. package/dist/publish.d.ts.map +1 -1
  372. package/dist/publish.js +41 -56
  373. package/dist/publish.js.map +1 -1
  374. package/dist/renderer.d.ts +14 -4
  375. package/dist/renderer.d.ts.map +1 -1
  376. package/dist/renderer.js +35 -23
  377. package/dist/renderer.js.map +1 -1
  378. package/dist/resolver.d.ts +17 -2
  379. package/dist/resolver.d.ts.map +1 -1
  380. package/dist/resolver.js +112 -16
  381. package/dist/resolver.js.map +1 -1
  382. package/dist/robots.d.ts +22 -0
  383. package/dist/robots.d.ts.map +1 -0
  384. package/dist/robots.js +25 -0
  385. package/dist/robots.js.map +1 -0
  386. package/dist/schema/dimensions.d.ts +78 -0
  387. package/dist/schema/dimensions.d.ts.map +1 -0
  388. package/dist/schema/dimensions.js +97 -0
  389. package/dist/schema/dimensions.js.map +1 -0
  390. package/dist/schema/helpers.d.ts +108 -0
  391. package/dist/schema/helpers.d.ts.map +1 -0
  392. package/dist/schema/helpers.js +133 -0
  393. package/dist/schema/helpers.js.map +1 -0
  394. package/dist/schema/index.d.ts +27 -0
  395. package/dist/schema/index.d.ts.map +1 -0
  396. package/dist/schema/index.js +25 -0
  397. package/dist/schema/index.js.map +1 -0
  398. package/dist/schema/types.d.ts +390 -0
  399. package/dist/schema/types.d.ts.map +1 -0
  400. package/dist/schema/types.js +25 -0
  401. package/dist/schema/types.js.map +1 -0
  402. package/dist/selector-chain.d.ts +63 -0
  403. package/dist/selector-chain.d.ts.map +1 -0
  404. package/dist/selector-chain.js +58 -0
  405. package/dist/selector-chain.js.map +1 -0
  406. package/dist/seo.d.ts +56 -0
  407. package/dist/seo.d.ts.map +1 -0
  408. package/dist/seo.js +72 -0
  409. package/dist/seo.js.map +1 -0
  410. package/dist/serve.d.ts +41 -3
  411. package/dist/serve.d.ts.map +1 -1
  412. package/dist/serve.js +206 -65
  413. package/dist/serve.js.map +1 -1
  414. package/dist/sidecars.d.ts +26 -21
  415. package/dist/sidecars.d.ts.map +1 -1
  416. package/dist/sidecars.js +143 -45
  417. package/dist/sidecars.js.map +1 -1
  418. package/dist/site-loader.d.ts +74 -6
  419. package/dist/site-loader.d.ts.map +1 -1
  420. package/dist/site-loader.js +138 -28
  421. package/dist/site-loader.js.map +1 -1
  422. package/dist/sitemap.d.ts +45 -0
  423. package/dist/sitemap.d.ts.map +1 -0
  424. package/dist/sitemap.js +67 -0
  425. package/dist/sitemap.js.map +1 -0
  426. package/dist/targets.d.ts +47 -1
  427. package/dist/targets.d.ts.map +1 -1
  428. package/dist/targets.js +83 -36
  429. package/dist/targets.js.map +1 -1
  430. package/dist/template-loader.d.ts +7 -3
  431. package/dist/template-loader.d.ts.map +1 -1
  432. package/dist/template-loader.js +27 -12
  433. package/dist/template-loader.js.map +1 -1
  434. package/dist/templates-scan-worker.js +1 -1
  435. package/dist/templates-scan-worker.js.map +1 -1
  436. package/dist/templates-scan.d.ts.map +1 -1
  437. package/dist/templates-scan.js +1 -1
  438. package/dist/templates-scan.js.map +1 -1
  439. package/dist/themes.d.ts +69 -0
  440. package/dist/themes.d.ts.map +1 -0
  441. package/dist/themes.js +85 -0
  442. package/dist/themes.js.map +1 -0
  443. package/dist/transforms/adapter.d.ts +115 -0
  444. package/dist/transforms/adapter.d.ts.map +1 -0
  445. package/dist/transforms/adapter.js +2 -0
  446. package/dist/transforms/adapter.js.map +1 -0
  447. package/dist/transforms/cloudflare.d.ts +17 -0
  448. package/dist/transforms/cloudflare.d.ts.map +1 -0
  449. package/dist/transforms/cloudflare.js +110 -0
  450. package/dist/transforms/cloudflare.js.map +1 -0
  451. package/dist/transforms/index.d.ts +24 -0
  452. package/dist/transforms/index.d.ts.map +1 -0
  453. package/dist/transforms/index.js +30 -0
  454. package/dist/transforms/index.js.map +1 -0
  455. package/dist/transforms/sharp.d.ts +3 -0
  456. package/dist/transforms/sharp.d.ts.map +1 -0
  457. package/dist/transforms/sharp.js +43 -0
  458. package/dist/transforms/sharp.js.map +1 -0
  459. package/dist/types.d.ts +241 -10
  460. package/dist/types.d.ts.map +1 -1
  461. package/dist/types.js +28 -5
  462. package/dist/types.js.map +1 -1
  463. package/dist/workers/cloudflare-r2.d.ts +11 -2
  464. package/dist/workers/cloudflare-r2.d.ts.map +1 -1
  465. package/dist/workers/cloudflare-r2.js +120 -55
  466. package/dist/workers/cloudflare-r2.js.map +1 -1
  467. package/package.json +30 -2
  468. package/admin-dist/assets/index-BZAFKsUp.js +0 -608
  469. package/admin-dist/assets/index-BpRotMuK.css +0 -1
  470. package/admin-dist/assets/vendor-vue-DSjyxCX6.js +0 -1
  471. package/dist/providers/r2.d.ts +0 -8
  472. package/dist/providers/r2.d.ts.map +0 -1
  473. package/dist/providers/r2.js +0 -83
  474. package/dist/providers/r2.js.map +0 -1
  475. package/dist/source-sidecars.d.ts +0 -13
  476. package/dist/source-sidecars.d.ts.map +0 -1
  477. package/dist/source-sidecars.js +0 -52
  478. package/dist/source-sidecars.js.map +0 -1
package/dist/cli/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { resolve, join, dirname } from 'node:path';
2
+ import { resolve, join, dirname, relative } from 'node:path';
3
3
  import { watch, existsSync, readFileSync } from 'node:fs';
4
4
  import { serve } from '@hono/node-server';
5
5
  import { serveStatic } from '@hono/node-server/serve-static';
@@ -11,18 +11,19 @@ import { resolvePage } from '../resolver.js';
11
11
  import { renderPage } from '../renderer.js';
12
12
  import { createFilesystemProvider } from '../providers/filesystem.js';
13
13
  import { invalidateTemplate, invalidateAllTemplates } from '../template-loader.js';
14
+ import { getEnvironment, getType, isEditable } from '../types.js';
14
15
  import { createAdminApp } from '../admin-api/index.js';
15
16
  // ANSI color helpers — no dependency, suppressed when NO_COLOR or CI
16
17
  const noColor = !!process.env.NO_COLOR || !process.stdout.isTTY;
17
18
  const c = {
18
- bold: (s) => noColor ? s : `\x1b[1m${s}\x1b[22m`,
19
- dim: (s) => noColor ? s : `\x1b[2m${s}\x1b[22m`,
20
- cyan: (s) => noColor ? s : `\x1b[36m${s}\x1b[39m`,
21
- green: (s) => noColor ? s : `\x1b[32m${s}\x1b[39m`,
22
- yellow: (s) => noColor ? s : `\x1b[33m${s}\x1b[39m`,
23
- red: (s) => noColor ? s : `\x1b[31m${s}\x1b[39m`,
24
- magenta: (s) => noColor ? s : `\x1b[35m${s}\x1b[39m`,
25
- bgGreen: (s) => noColor ? s : `\x1b[42m\x1b[30m${s}\x1b[39m\x1b[49m`,
19
+ bold: (s) => (noColor ? s : `\x1b[1m${s}\x1b[22m`),
20
+ dim: (s) => (noColor ? s : `\x1b[2m${s}\x1b[22m`),
21
+ cyan: (s) => (noColor ? s : `\x1b[36m${s}\x1b[39m`),
22
+ green: (s) => (noColor ? s : `\x1b[32m${s}\x1b[39m`),
23
+ yellow: (s) => (noColor ? s : `\x1b[33m${s}\x1b[39m`),
24
+ red: (s) => (noColor ? s : `\x1b[31m${s}\x1b[39m`),
25
+ magenta: (s) => (noColor ? s : `\x1b[35m${s}\x1b[39m`),
26
+ bgGreen: (s) => (noColor ? s : `\x1b[42m\x1b[30m${s}\x1b[39m\x1b[49m`),
26
27
  };
27
28
  const args = process.argv.slice(2);
28
29
  const command = args[0];
@@ -134,11 +135,26 @@ function printHelp() {
134
135
  gazetta serve [target] [site] Serve published pages from target storage
135
136
  gazetta deploy [target] [site] Deploy worker to hosting (one-time setup)
136
137
  gazetta validate [site] Check site for broken references
138
+ gazetta translate <item> --to <locale> [target]
139
+ Create a locale copy of a page or fragment
140
+ gazetta history [target] [site] List revisions on a target
141
+ gazetta undo [target] [site] Restore the previous revision (soft undo)
142
+ gazetta rollback <rev> [target] [site]
143
+ Restore an arbitrary revision by id
144
+ gazetta assets list [target] [site]
145
+ List assets on a target
146
+ gazetta assets info <name> [target] [site]
147
+ Show full detail (variants, overrides, refs) for one asset
148
+ gazetta assets reindex [target] [site]
149
+ Rebuild the asset-refs sidecar index from manifests
137
150
  gazetta help Show this help message
138
151
 
139
152
  Options:
140
153
  --port, -p <port> Server port (default: 3000)
141
154
  --force, -f Publish all items (skip unchanged check)
155
+ --yes, -y Skip confirmation prompt (required in CI
156
+ for undo/rollback on production targets)
157
+ --limit <n> Max revisions to list (default: 50)
142
158
 
143
159
  Auto-detection:
144
160
  Site is auto-detected from sites/ directory. If multiple sites exist,
@@ -155,12 +171,17 @@ function printHelp() {
155
171
  gazetta publish production my-site # publish specific site to production
156
172
  gazetta serve production -p 8080 # serve production on port 8080
157
173
  gazetta validate # check site for errors
174
+ gazetta history # list revisions on default target
175
+ gazetta undo production --yes # undo last write on production (CI-safe)
176
+ gazetta rollback rev-1776337441608 # roll back to a specific revision
158
177
  `);
159
178
  }
160
179
  function parseArgs(input) {
161
180
  const positional = [];
162
181
  let port;
163
182
  let force = false;
183
+ let yes = false;
184
+ let limit;
164
185
  for (let i = 0; i < input.length; i++) {
165
186
  if (input[i] === '--port' || input[i] === '-p') {
166
187
  port = parseInt(input[++i], 10);
@@ -168,11 +189,23 @@ function parseArgs(input) {
168
189
  else if (input[i] === '--force' || input[i] === '-f') {
169
190
  force = true;
170
191
  }
192
+ else if (input[i] === '--yes' || input[i] === '-y') {
193
+ yes = true;
194
+ }
195
+ else if (input[i] === '--limit') {
196
+ limit = parseInt(input[++i], 10);
197
+ }
198
+ else if (input[i] === '--to') {
199
+ i++; // consume the locale value — translate command reads it from raw args
200
+ }
201
+ else if (input[i].startsWith('--to=')) {
202
+ // consumed by translate command directly
203
+ }
171
204
  else if (!input[i].startsWith('-')) {
172
205
  positional.push(input[i]);
173
206
  }
174
207
  }
175
- return { positional, port, force };
208
+ return { positional, port, force, yes, limit };
176
209
  }
177
210
  /**
178
211
  * Resolve the site directory from positional args or auto-detection.
@@ -203,8 +236,7 @@ async function resolveSiteDir(positionalSite) {
203
236
  const sitesDir = resolve('sites');
204
237
  if (existsSync(sitesDir)) {
205
238
  const { readdirSync, statSync } = await import('node:fs');
206
- const sites = readdirSync(sitesDir)
207
- .filter(name => {
239
+ const sites = readdirSync(sitesDir).filter(name => {
208
240
  const dir = join(sitesDir, name);
209
241
  return statSync(dir).isDirectory() && existsSync(join(dir, 'site.yaml'));
210
242
  });
@@ -267,7 +299,7 @@ async function runInit(dir) {
267
299
  }
268
300
  const name = target.split('/').pop() ?? 'my-site';
269
301
  const files = {
270
- 'sites/main/site.yaml': `name: ${name}\nversion: 1.0.0\nsystemPages:\n - "404"\ntargets:\n local:\n storage:\n type: filesystem\n path: ./dist/local\n`,
302
+ 'sites/main/site.yaml': `name: ${name}\nversion: 1.0.0\nsystemPages:\n - "404"\ntargets:\n local:\n storage:\n type: filesystem\n # environment=local, editable=true (defaults); path=./targets/local (default)\n`,
271
303
  'templates/page-layout/index.ts': `import { z } from 'zod'
272
304
  import type { TemplateFunction } from 'gazetta'
273
305
 
@@ -356,20 +388,28 @@ const template: TemplateFunction = ({ content = {} }) => {
356
388
 
357
389
  export default template
358
390
  `,
359
- 'sites/main/fragments/header/fragment.json': JSON.stringify({
391
+ 'sites/main/targets/local/fragments/header/fragment.json': JSON.stringify({
360
392
  template: 'nav',
361
393
  content: { brand: name, links: [{ label: 'Home', href: '/' }] },
362
394
  }, null, 2) + '\n',
363
- 'sites/main/pages/home/page.json': JSON.stringify({
395
+ 'sites/main/targets/local/pages/home/page.json': JSON.stringify({
364
396
  template: 'page-layout',
365
397
  content: { title: name, description: 'A site built with Gazetta' },
366
398
  components: [
367
399
  '@header',
368
- { name: 'hero', template: 'hero', content: { title: `Welcome to ${name}`, subtitle: 'A site built with Gazetta' } },
369
- { name: 'intro', template: 'text-block', content: { body: '<p>Edit this content in the CMS at <a href="/admin">/admin</a>.</p>' } },
400
+ {
401
+ name: 'hero',
402
+ template: 'hero',
403
+ content: { title: `Welcome to ${name}`, subtitle: 'A site built with Gazetta' },
404
+ },
405
+ {
406
+ name: 'intro',
407
+ template: 'text-block',
408
+ content: { body: '<p>Edit this content in the CMS at <a href="/admin">/admin</a>.</p>' },
409
+ },
370
410
  ],
371
411
  }, null, 2) + '\n',
372
- 'sites/main/pages/404/page.json': JSON.stringify({
412
+ 'sites/main/targets/local/pages/404/page.json': JSON.stringify({
373
413
  template: 'page-layout',
374
414
  content: { title: 'Page Not Found', description: "The page you're looking for doesn't exist." },
375
415
  }, null, 2) + '\n',
@@ -414,23 +454,24 @@ export default template
414
454
  outro(`Done! Run: ${c.cyan(`${cdStep}npx gazetta dev`)}`);
415
455
  }
416
456
  async function runPublish(siteDir, targetName, opts = {}) {
417
- const storage = createFilesystemProvider();
418
457
  const projectRoot = detectProjectRoot(siteDir);
419
458
  const templatesDir = join(projectRoot, 'templates');
420
- const site = await loadSite({ siteDir, storage, templatesDir });
421
- // Load target configs from site.yaml
422
- const siteYamlPath = join(siteDir, 'site.yaml');
423
- if (!existsSync(siteYamlPath)) {
424
- console.error(`\n ${c.red('Error:')} No site.yaml found at ${siteDir}\n`);
459
+ // Source comes from the default editable target in site.yaml.
460
+ const { buildSourceContext } = await import('./bootstrap.js');
461
+ let source, manifest, targetConfigs;
462
+ try {
463
+ ;
464
+ ({ source, manifest, targetConfigs } = await buildSourceContext({ projectSiteDir: siteDir }));
465
+ }
466
+ catch (err) {
467
+ console.error(`\n ${c.red('Error:')} ${err.message}\n`);
425
468
  process.exit(1);
426
469
  }
427
- const siteYaml = yaml.load(readFileSync(siteYamlPath, 'utf-8'));
470
+ const storage = source.storage;
471
+ const site = await loadSite({ contentRoot: source.contentRoot, templatesDir, manifest });
472
+ const siteYaml = manifest;
428
473
  if (!siteYaml.targets || Object.keys(siteYaml.targets).length === 0) {
429
- console.error(`\n Error: no targets configured in ${siteYamlPath}`);
430
- console.error(`\n Add a target to site.yaml:\n`);
431
- console.error(` targets:`);
432
- console.error(` staging:`);
433
- console.error(` storage: { type: filesystem, path: ./dist/staging }\n`);
474
+ console.error(`\n Error: no targets configured in site.yaml`);
434
475
  process.exit(1);
435
476
  }
436
477
  // Determine which targets to publish to
@@ -444,7 +485,8 @@ async function runPublish(siteDir, targetName, opts = {}) {
444
485
  // Initialize targets
445
486
  const { createTargetRegistry } = await import('../targets.js');
446
487
  const targets = await createTargetRegistry(Object.fromEntries(targetNames.map(n => [n, siteYaml.targets[n]])), siteDir);
447
- const { publishPageRendered, publishPageStatic, publishFragmentRendered, publishSiteManifest, publishFragmentIndex } = await import('../publish-rendered.js');
488
+ const { publishPageRendered, publishPageStatic, publishFragmentRendered, publishSiteManifest, publishDepIndices } = await import('../publish-rendered.js');
489
+ const { publishPageAllLocales, publishFragmentAllLocales } = await import('../publish-locale.js');
448
490
  const { scanTemplates, templateHashesFrom, reportTemplateErrors } = await import('../templates-scan.js');
449
491
  const { hashManifest } = await import('../hash.js');
450
492
  // Validate + hash templates once for this publish run
@@ -469,10 +511,10 @@ async function runPublish(siteDir, targetName, opts = {}) {
469
511
  continue;
470
512
  }
471
513
  const targetConfig = siteYaml.targets[name];
472
- const { getPublishMode } = await import('../types.js');
473
- const publishMode = targetConfig ? getPublishMode(targetConfig) : 'static';
474
- const isStatic = publishMode === 'static';
475
- console.log(` ${c.bold(name)} ${c.dim(`(${publishMode})`)}`);
514
+ const { getType } = await import('../types.js');
515
+ const targetType = targetConfig ? getType(targetConfig) : 'static';
516
+ const isStatic = targetType === 'static';
517
+ console.log(` ${c.bold(name)} ${c.dim(`(${targetType})`)}`);
476
518
  let totalFiles = 0;
477
519
  let totalRemoved = 0;
478
520
  // Incremental: compare source hashes against target sidecars, skip
@@ -481,18 +523,47 @@ async function runPublish(siteDir, targetName, opts = {}) {
481
523
  if (!opts.force) {
482
524
  const { compareTargets } = await import('../compare.js');
483
525
  const cmp = await compareTargets({
484
- source: storage,
526
+ sourceRoot: source.contentRoot,
485
527
  target: targetStorage,
486
- siteDir,
487
528
  templatesDir,
488
529
  projectRoot,
489
- publishMode,
530
+ type: targetType,
490
531
  scanTemplates: async () => templateInfos,
532
+ manifest,
491
533
  });
492
534
  for (const item of cmp.unchanged)
493
535
  unchanged.add(item);
494
536
  }
495
537
  let skipped = 0;
538
+ const sourceRoot = source.contentRoot;
539
+ // Asset publish — before any page render, so static-mode page HTML
540
+ // doesn't bake in URLs to bytes that aren't on the target yet. Skips
541
+ // assets that are already on target (content-addressed dedupe).
542
+ {
543
+ const { publishAssets } = await import('../assets/publish.js');
544
+ const { createContentRoot } = await import('../content-root.js');
545
+ const targetRoot = createContentRoot(targetStorage);
546
+ const itemNames = [
547
+ ...[...site.pages.keys()].map(n => `pages/${n}`),
548
+ ...[...site.fragments.keys()].map(n => `fragments/${n}`),
549
+ ];
550
+ const assetResult = await publishAssets({ sourceRoot, targetRoot, itemNames });
551
+ if (!assetResult.ok) {
552
+ console.error(` ${c.red('✗')} Asset publish failed: source is missing — ${assetResult.missing.join(', ')}`);
553
+ process.exit(1);
554
+ }
555
+ if (assetResult.copiedAssets > 0) {
556
+ console.log(` ${c.green('✓')} ${assetResult.copiedAssets} asset(s), ${assetResult.copiedFiles} file(s)`);
557
+ }
558
+ totalFiles += assetResult.copiedFiles;
559
+ }
560
+ // SEO context for this target — built once, shared across all page renders.
561
+ const seo = {
562
+ siteName: site.manifest.name,
563
+ siteUrl: targetConfig?.siteUrl,
564
+ locale: site.manifest.locale,
565
+ defaultOgImage: site.manifest.defaultOgImage,
566
+ };
496
567
  if (isStatic) {
497
568
  // Static mode — fully assembled HTML, no fragments needed separately.
498
569
  // Page hash must include fragment hashes so a fragment change
@@ -508,42 +579,182 @@ async function runPublish(siteDir, targetName, opts = {}) {
508
579
  continue;
509
580
  }
510
581
  const manifestHash = hashManifest(page, { templateHashes, fragmentHashes });
511
- const { files } = await publishPageStatic(pageName, storage, siteDir, targetStorage, templatesDir, manifestHash, site);
582
+ const { files } = await publishPageStatic(pageName, sourceRoot, targetStorage, templatesDir, manifestHash, site, seo);
512
583
  totalFiles += files;
513
584
  console.log(` ${c.green('✓')} ${pageName}`);
514
585
  }
515
586
  }
516
587
  else {
517
588
  // ESI mode — fragments separate, pages with placeholders
518
- for (const [fragName, frag] of site.fragments) {
519
- if (unchanged.has(`fragments/${fragName}`)) {
589
+ for (const [fragName] of site.fragments) {
590
+ // Build per-locale unchanged set: null = default, 'fr' = French
591
+ const fragUnchanged = new Set();
592
+ if (unchanged.has(`fragments/${fragName}`))
593
+ fragUnchanged.add(null);
594
+ const fragLocales = site.fragmentLocales.get(fragName);
595
+ if (fragLocales) {
596
+ for (const loc of fragLocales.locales.keys()) {
597
+ if (unchanged.has(`fragments/${fragName}:${loc}`))
598
+ fragUnchanged.add(loc);
599
+ }
600
+ }
601
+ // Skip entirely if all locales unchanged
602
+ const totalFragLocales = 1 + (fragLocales?.locales.size ?? 0);
603
+ if (fragUnchanged.size >= totalFragLocales) {
520
604
  skipped++;
521
605
  continue;
522
606
  }
523
- const manifestHash = hashManifest(frag, { templateHashes });
524
- const { files, removed } = await publishFragmentRendered(fragName, storage, siteDir, targetStorage, templatesDir, manifestHash, site);
607
+ const { files, removed } = await publishFragmentAllLocales(fragName, sourceRoot, targetStorage, site, { templateHashes }, { templatesDir, targetLocales: targetConfig?.locales, unchangedLocales: fragUnchanged });
525
608
  totalFiles += files;
526
609
  totalRemoved += removed;
527
- console.log(` ${c.green('')} @${fragName}`);
610
+ const skippedCount = fragUnchanged.size > 0 ? ` (${fragUnchanged.size} locale${fragUnchanged.size > 1 ? 's' : ''} skipped)` : '';
611
+ console.log(` ${c.green('✓')} @${fragName}${skippedCount}`);
528
612
  }
529
- for (const [pageName, page] of site.pages) {
530
- if (unchanged.has(`pages/${pageName}`)) {
613
+ for (const [pageName] of site.pages) {
614
+ // Build per-locale unchanged set
615
+ const pageUnchanged = new Set();
616
+ if (unchanged.has(`pages/${pageName}`))
617
+ pageUnchanged.add(null);
618
+ const pageLocales = site.pageLocales.get(pageName);
619
+ if (pageLocales) {
620
+ for (const loc of pageLocales.locales.keys()) {
621
+ if (unchanged.has(`pages/${pageName}:${loc}`))
622
+ pageUnchanged.add(loc);
623
+ }
624
+ }
625
+ const totalPageLocales = 1 + (pageLocales?.locales.size ?? 0);
626
+ if (pageUnchanged.size >= totalPageLocales) {
531
627
  skipped++;
532
628
  continue;
533
629
  }
534
- const manifestHash = hashManifest(page, { templateHashes });
535
- const { files, removed } = await publishPageRendered(pageName, storage, siteDir, targetStorage, targetConfig?.cache, templatesDir, manifestHash, site);
630
+ const { files, removed } = await publishPageAllLocales(pageName, sourceRoot, targetStorage, site, { templateHashes }, {
631
+ cache: targetConfig?.cache,
632
+ templatesDir,
633
+ seo,
634
+ targetLocales: targetConfig?.locales,
635
+ unchangedLocales: pageUnchanged,
636
+ });
536
637
  totalFiles += files;
537
638
  totalRemoved += removed;
538
- console.log(` ${c.green('✓')} ${pageName}`);
639
+ const skippedCount = pageUnchanged.size > 0 ? ` (${pageUnchanged.size} locale${pageUnchanged.size > 1 ? 's' : ''} skipped)` : '';
640
+ console.log(` ${c.green('✓')} ${pageName}${skippedCount}`);
539
641
  }
540
642
  }
541
643
  if (skipped > 0)
542
644
  console.log(` ${c.dim(`· ${skipped} unchanged (skipped)`)}`);
543
- // Site manifest + fragment index
544
- await publishSiteManifest(storage, siteDir, targetStorage, site);
545
- await publishFragmentIndex(storage, siteDir, targetStorage, site);
546
- totalFiles += 2;
645
+ // Site manifest + dep-sidecar indices
646
+ await publishSiteManifest(sourceRoot, targetStorage, site);
647
+ await publishDepIndices(sourceRoot, targetStorage, site);
648
+ totalFiles += 1;
649
+ // Sitemap + robots.txt — generated from target sidecars
650
+ const siteUrl = targetConfig?.siteUrl;
651
+ if (siteUrl) {
652
+ const { listSidecars } = await import('../sidecars.js');
653
+ const { generateSitemap } = await import('../sitemap.js');
654
+ const { generateRobotsTxt } = await import('../robots.js');
655
+ const targetPageSidecars = await listSidecars(targetStorage, 'pages');
656
+ // Merge source-side knowledge — listSidecars may miss just-written
657
+ // entries on R2 due to eventual list-after-write consistency. Every
658
+ // page we just published gets an entry even if the listing missed it.
659
+ const now = new Date().toISOString();
660
+ for (const [pageName, page] of site.pages) {
661
+ if (!targetPageSidecars.has(pageName)) {
662
+ targetPageSidecars.set(pageName, {
663
+ hash: '',
664
+ pub: { lastPublished: now, noindex: !!page.metadata?.robots?.includes('noindex') },
665
+ });
666
+ }
667
+ }
668
+ for (const [pageName, localeEntry] of site.pageLocales) {
669
+ for (const [loc, localePage] of localeEntry.locales) {
670
+ const key = `${pageName}:${loc}`;
671
+ if (!targetPageSidecars.has(key)) {
672
+ targetPageSidecars.set(key, {
673
+ hash: '',
674
+ pub: { lastPublished: now, noindex: !!localePage.metadata?.robots?.includes('noindex') },
675
+ });
676
+ }
677
+ }
678
+ }
679
+ const { resolveSiteLocales, defaultLocaleFor } = await import('../locale.js');
680
+ // Build hreflang groups — two strategies:
681
+ // 1. Subpath: same siteUrl, multiple locales → locale-prefixed routes
682
+ // 2. Cross-domain: other targets with different siteUrl → cross-link
683
+ const resolvedLoc = resolveSiteLocales(manifest);
684
+ const defLoc = defaultLocaleFor(manifest);
685
+ const hreflangGroups = new Map();
686
+ if (resolvedLoc) {
687
+ const { localeRoutePrefix } = await import('../locale.js');
688
+ const thisTargetLocales = targetConfig?.locales ?? resolvedLoc.supported;
689
+ const thisTargetDefault = targetConfig?.locale ?? defLoc;
690
+ for (const [pageName, page] of site.pages) {
691
+ if (pageName.includes('['))
692
+ continue;
693
+ const alternates = [];
694
+ // Subpath alternates on this target
695
+ if (thisTargetLocales.length > 1) {
696
+ for (const loc of thisTargetLocales) {
697
+ const prefix = localeRoutePrefix(loc, { ...resolvedLoc, default: thisTargetDefault });
698
+ const route = page.route === '/' ? prefix || '/' : `${prefix}${page.route}`;
699
+ alternates.push({ locale: loc, url: `${siteUrl}${route}` });
700
+ }
701
+ }
702
+ else {
703
+ // Single-locale target — add self
704
+ alternates.push({ locale: thisTargetLocales[0] ?? defLoc, url: `${siteUrl}${page.route}` });
705
+ }
706
+ // Cross-domain alternates from other targets
707
+ for (const [otherName, otherConfig] of Object.entries(siteYaml.targets ?? {})) {
708
+ if (otherName === name)
709
+ continue; // skip self
710
+ if (!otherConfig.siteUrl)
711
+ continue;
712
+ const otherLocales = otherConfig.locales ?? resolvedLoc.supported;
713
+ const otherDefault = otherConfig.locale ?? defLoc;
714
+ for (const loc of otherLocales) {
715
+ // Skip locales already covered by this target
716
+ if (alternates.some(a => a.locale === loc))
717
+ continue;
718
+ const otherResolved = { ...resolvedLoc, default: otherDefault };
719
+ const prefix = localeRoutePrefix(loc, otherResolved);
720
+ const route = page.route === '/' ? prefix || '/' : `${prefix}${page.route}`;
721
+ alternates.push({ locale: loc, url: `${otherConfig.siteUrl}${route}` });
722
+ }
723
+ }
724
+ if (alternates.length > 1) {
725
+ hreflangGroups.set(pageName, alternates);
726
+ }
727
+ }
728
+ }
729
+ const sitemapXml = generateSitemap({
730
+ siteUrl,
731
+ pages: targetPageSidecars,
732
+ systemPages: site.manifest.systemPages,
733
+ hreflangGroups: hreflangGroups.size > 0 ? hreflangGroups : undefined,
734
+ defaultLocale: defLoc,
735
+ });
736
+ if (sitemapXml) {
737
+ await targetStorage.writeFile('sitemap.xml', sitemapXml);
738
+ totalFiles++;
739
+ console.log(` ${c.dim('· sitemap.xml')}`);
740
+ }
741
+ // robots.txt: only at the domain root — Google ignores robots.txt at
742
+ // subpaths. If siteUrl has a path component, the domain root belongs
743
+ // to someone else (host, reverse proxy, another app).
744
+ const isRootDeploy = !new URL(siteUrl).pathname.replace(/\/+$/, '');
745
+ if (isRootDeploy) {
746
+ let robotsTxt;
747
+ try {
748
+ robotsTxt = await source.contentRoot.storage.readFile(source.contentRoot.path('robots.txt'));
749
+ }
750
+ catch {
751
+ robotsTxt = generateRobotsTxt({ siteUrl });
752
+ }
753
+ await targetStorage.writeFile('robots.txt', robotsTxt);
754
+ totalFiles++;
755
+ console.log(` ${c.dim('· robots.txt')}`);
756
+ }
757
+ }
547
758
  const removedMsg = totalRemoved > 0 ? c.dim(` (${totalRemoved} old files cleaned)`) : '';
548
759
  console.log(`\n ${c.green('✓')} ${c.bold(name)}: ${totalFiles} files published${removedMsg}\n`);
549
760
  }
@@ -561,7 +772,8 @@ async function runPublish(siteDir, targetName, opts = {}) {
561
772
  }
562
773
  try {
563
774
  const { lookupCloudflareZoneId } = await import('../publish-rendered.js');
564
- const zoneId = resolveEnvVars(purge.zoneId) ?? (config.siteUrl ? await lookupCloudflareZoneId(config.siteUrl, apiToken) : null);
775
+ const zoneId = resolveEnvVars(purge.zoneId) ??
776
+ (config.siteUrl ? await lookupCloudflareZoneId(config.siteUrl, apiToken) : null);
565
777
  if (!zoneId) {
566
778
  console.log(` ${name}: zone not found, set purge.zoneId or siteUrl`);
567
779
  continue;
@@ -647,8 +859,10 @@ async function runBuild(siteDir) {
647
859
  const editorsDir = join(adminDir, 'editors');
648
860
  const fieldsDir = join(adminDir, 'fields');
649
861
  const entryExtensions = ['.ts', '.tsx', '.jsx'];
650
- const hasEditors = existsSync(editorsDir) && (await import('node:fs')).readdirSync(editorsDir).some(f => entryExtensions.some(ext => f.endsWith(ext)));
651
- const hasFields = existsSync(fieldsDir) && (await import('node:fs')).readdirSync(fieldsDir).some(f => entryExtensions.some(ext => f.endsWith(ext)));
862
+ const hasEditors = existsSync(editorsDir) &&
863
+ (await import('node:fs')).readdirSync(editorsDir).some(f => entryExtensions.some(ext => f.endsWith(ext)));
864
+ const hasFields = existsSync(fieldsDir) &&
865
+ (await import('node:fs')).readdirSync(fieldsDir).some(f => entryExtensions.some(ext => f.endsWith(ext)));
652
866
  if (hasEditors || hasFields) {
653
867
  const { build: esbuild } = await import('esbuild');
654
868
  const { writeFile: writeFileAsync, mkdir: mkdirAsync } = await import('node:fs/promises');
@@ -656,7 +870,7 @@ async function runBuild(siteDir) {
656
870
  await mkdirAsync(sharedDir, { recursive: true });
657
871
  // Build shared dependency bundles (one copy of React, etc.)
658
872
  const sharedDeps = {
659
- 'react': 'export * from "react"; import React from "react"; export default React;',
873
+ react: 'export * from "react"; import React from "react"; export default React;',
660
874
  'react-dom/client': 'export * from "react-dom/client";',
661
875
  'react/jsx-runtime': 'export * from "react/jsx-runtime";',
662
876
  'gazetta/editor': 'export * from "gazetta/editor";',
@@ -682,14 +896,19 @@ async function runBuild(siteDir) {
682
896
  });
683
897
  importMap[specifier] = `/admin/_shared/${safeName}.js`;
684
898
  }
685
- catch { /* skip — dep may not be installed */ }
899
+ catch {
900
+ /* skip — dep may not be installed */
901
+ }
686
902
  await import('node:fs/promises').then(fs => fs.rm(stubFile, { force: true }));
687
903
  }
688
904
  console.log(` ${c.green('✓')} Shared deps: ${Object.keys(importMap).join(', ')}`);
689
905
  // Bundle each custom editor/field with shared deps externalized
690
906
  const externals = Object.keys(importMap);
691
907
  let bundledCount = 0;
692
- for (const [kind, srcDir] of [['editors', editorsDir], ['fields', fieldsDir]]) {
908
+ for (const [kind, srcDir] of [
909
+ ['editors', editorsDir],
910
+ ['fields', fieldsDir],
911
+ ]) {
693
912
  if (!existsSync(srcDir))
694
913
  continue;
695
914
  const { readdirSync } = await import('node:fs');
@@ -738,11 +957,12 @@ async function runAdmin(siteDir, port) {
738
957
  process.exit(1);
739
958
  }
740
959
  const app = new Hono();
741
- app.get('/__reload', (ctx) => ctx.body(null, 204));
742
- const fsStorage = createFilesystemProvider();
743
- await setupProductionMode(app, siteDir, fsStorage, builtAdminDir, templatesDir, adminDir);
960
+ app.get('/__reload', ctx => ctx.body(null, 204));
961
+ const { buildSourceContext } = await import('./bootstrap.js');
962
+ const { source, targetConfigs } = await buildSourceContext({ projectSiteDir: siteDir });
963
+ await setupProductionMode(app, source, siteDir, builtAdminDir, templatesDir, adminDir, targetConfigs);
744
964
  // SPA fallback for non-API admin routes
745
- app.get('*', (ctx) => {
965
+ app.get('*', ctx => {
746
966
  const indexPath = join(builtAdminDir, 'index.html');
747
967
  if (existsSync(indexPath))
748
968
  return ctx.html(readFileSync(indexPath, 'utf-8'));
@@ -757,7 +977,10 @@ async function runAdmin(siteDir, port) {
757
977
  console.log();
758
978
  });
759
979
  for (const signal of ['SIGINT', 'SIGTERM']) {
760
- process.on(signal, () => { console.log(`\n Shutting down...`); server.close(() => process.exit(0)); });
980
+ process.on(signal, () => {
981
+ console.log(`\n Shutting down...`);
982
+ server.close(() => process.exit(0));
983
+ });
761
984
  }
762
985
  }
763
986
  async function runServe(siteDir, port, targetName) {
@@ -778,10 +1001,11 @@ async function runServe(siteDir, port, targetName) {
778
1001
  process.exit(1);
779
1002
  }
780
1003
  const { createStorageProvider } = await import('../targets.js');
781
- const storage = await createStorageProvider(config.storage, siteDir);
782
- const { getPublishMode } = await import('../types.js');
1004
+ // Pass targetName so filesystem path defaults to ./targets/<name> when unset.
1005
+ const storage = await createStorageProvider(config.storage, siteDir, name);
1006
+ const { getType } = await import('../types.js');
783
1007
  const { createServer } = await import('../serve.js');
784
- const app = createServer({ storage, mode: getPublishMode(config) });
1008
+ const app = createServer({ storage, type: getType(config) });
785
1009
  const server = serve({ fetch: app.fetch, port }, () => {
786
1010
  console.log();
787
1011
  console.log(` ${c.bgGreen(c.bold(' gazetta '))} ${c.green('serve')} ${c.dim(siteYaml.name)} ${c.dim(`(${name})`)}`);
@@ -871,16 +1095,17 @@ async function runDeploy(siteDir, targetName) {
871
1095
  console.log(`\n ${c.green('✓')} Worker deployed. Now publish content:\n ${c.cyan(`gazetta publish ${targetName}`)}\n`);
872
1096
  }
873
1097
  async function runValidate(siteDir) {
874
- const storage = createFilesystemProvider();
875
1098
  const projectRoot = detectProjectRoot(siteDir);
876
1099
  const templatesDir = join(projectRoot, 'templates');
877
1100
  console.log();
878
1101
  console.log(` ${c.bgGreen(c.bold(' gazetta '))} ${c.green('validate')} ${c.dim(siteDir)}`);
879
1102
  console.log();
880
- // 1. Check site.yaml
1103
+ // 1. Check site.yaml + load default editable target's content
881
1104
  let site;
882
1105
  try {
883
- site = await loadSite({ siteDir, storage, templatesDir });
1106
+ const { buildSourceContext } = await import('./bootstrap.js');
1107
+ const { source, manifest } = await buildSourceContext({ projectSiteDir: siteDir });
1108
+ site = await loadSite({ contentRoot: source.contentRoot, templatesDir, manifest });
884
1109
  console.log(` ${c.green('✓')} site.yaml ${c.dim(`— ${site.manifest.name}`)}`);
885
1110
  }
886
1111
  catch (err) {
@@ -915,11 +1140,12 @@ async function runValidate(siteDir) {
915
1140
  errors++;
916
1141
  }
917
1142
  }
918
- // 4. List templates
1143
+ // 4. List templates (project-level filesystem, not target content)
1144
+ const projectStorage = createFilesystemProvider();
919
1145
  let templateNames = [];
920
1146
  try {
921
- const entries = await storage.readDir(templatesDir);
922
- templateNames = entries.filter(e => e.isDirectory).map(e => e.name);
1147
+ const entries = await projectStorage.readDir(templatesDir);
1148
+ templateNames = entries.filter(e => e.isDirectory).map((e) => e.name);
923
1149
  console.log(` ${c.green('✓')} ${c.dim(`${templateNames.length} templates`)}`);
924
1150
  }
925
1151
  catch {
@@ -929,7 +1155,9 @@ async function runValidate(siteDir) {
929
1155
  const adminDir = join(projectRoot, 'admin');
930
1156
  const editorsDir = join(adminDir, 'editors');
931
1157
  if (existsSync(editorsDir)) {
932
- const editorFiles = (await import('node:fs')).readdirSync(editorsDir).filter(f => f.endsWith('.ts') || f.endsWith('.tsx'));
1158
+ const editorFiles = (await import('node:fs'))
1159
+ .readdirSync(editorsDir)
1160
+ .filter(f => f.endsWith('.ts') || f.endsWith('.tsx'));
933
1161
  for (const file of editorFiles) {
934
1162
  const editorName = file.replace(/\.(ts|tsx)$/, '');
935
1163
  if (!templateNames.includes(editorName)) {
@@ -939,12 +1167,17 @@ async function runValidate(siteDir) {
939
1167
  }
940
1168
  // 6. Check for missing custom fields (schema references field but file doesn't exist)
941
1169
  const fieldsDir = join(adminDir, 'fields');
942
- const fieldFiles = existsSync(fieldsDir) ? (await import('node:fs')).readdirSync(fieldsDir).filter(f => f.endsWith('.ts') || f.endsWith('.tsx')).map(f => f.replace(/\.(ts|tsx)$/, '')) : [];
1170
+ const fieldFiles = existsSync(fieldsDir)
1171
+ ? (await import('node:fs'))
1172
+ .readdirSync(fieldsDir)
1173
+ .filter(f => f.endsWith('.ts') || f.endsWith('.tsx'))
1174
+ .map(f => f.replace(/\.(ts|tsx)$/, ''))
1175
+ : [];
943
1176
  const { loadTemplate } = await import('../template-loader.js');
944
1177
  const zod = await import('zod');
945
1178
  for (const tplName of templateNames) {
946
1179
  try {
947
- const loaded = await loadTemplate(storage, templatesDir, tplName);
1180
+ const loaded = await loadTemplate(projectStorage, templatesDir, tplName);
948
1181
  const jsonSchema = zod.z.toJSONSchema(loaded.schema);
949
1182
  const props = jsonSchema.properties;
950
1183
  if (!props)
@@ -957,7 +1190,70 @@ async function runValidate(siteDir) {
957
1190
  }
958
1191
  }
959
1192
  }
960
- catch { /* template load errors already caught above */ }
1193
+ catch {
1194
+ /* template load errors already caught above */
1195
+ }
1196
+ }
1197
+ // 7. Locale validation
1198
+ const { defaultLocaleFor } = await import('../locale.js');
1199
+ const defLoc = defaultLocaleFor(site.manifest);
1200
+ const hasI18n = !!site.manifest.locales?.supported?.length;
1201
+ // 7a. Warn about orphaned locale files when i18n is disabled
1202
+ if (!hasI18n && (site.pageLocales.size > 0 || site.fragmentLocales.size > 0)) {
1203
+ const orphanCount = site.pageLocales.size + site.fragmentLocales.size;
1204
+ console.log(` ${c.yellow('⚠')} ${orphanCount} locale file${orphanCount > 1 ? 's' : ''} found but i18n is disabled ${c.dim('— add locales.supported to site.yaml or remove *.locale.json files')}`);
1205
+ }
1206
+ // 7b. Warn about ambiguous page.en.json when en is default
1207
+ if (hasI18n) {
1208
+ for (const [name, entry] of site.pageLocales) {
1209
+ if (entry.locales.has(defLoc)) {
1210
+ console.log(` ${c.yellow('⚠')} page.${defLoc}.json in ${name} is ambiguous ${c.dim(`— "${defLoc}" is the default locale, use page.json instead`)}`);
1211
+ }
1212
+ }
1213
+ for (const [name, entry] of site.fragmentLocales) {
1214
+ if (entry.locales.has(defLoc)) {
1215
+ console.log(` ${c.yellow('⚠')} fragment.${defLoc}.json in ${name} is ambiguous ${c.dim(`— "${defLoc}" is the default locale, use fragment.json instead`)}`);
1216
+ }
1217
+ }
1218
+ }
1219
+ // 7c. Validate locale variant template/fragment refs
1220
+ if (hasI18n) {
1221
+ for (const [pageName, entry] of site.pageLocales) {
1222
+ for (const [locale] of entry.locales) {
1223
+ try {
1224
+ await resolvePage(pageName, site, locale);
1225
+ }
1226
+ catch (err) {
1227
+ console.error(` ${c.red('✗')} ${pageName} (${locale}) ${c.dim(`— ${err.message}`)}`);
1228
+ errors++;
1229
+ }
1230
+ }
1231
+ }
1232
+ }
1233
+ // 8. Cross-domain hreflang bidirectional check
1234
+ // For per-domain targets (each with siteUrl + single locale), verify that
1235
+ // all targets serving the same page cross-link to each other.
1236
+ if (hasI18n && site.manifest.targets) {
1237
+ const targetsWithSiteUrl = Object.entries(site.manifest.targets).filter(([, cfg]) => cfg.siteUrl && cfg.locales?.length === 1);
1238
+ if (targetsWithSiteUrl.length > 1) {
1239
+ const localeToUrl = new Map();
1240
+ for (const [, cfg] of targetsWithSiteUrl) {
1241
+ localeToUrl.set(cfg.locales[0], cfg.siteUrl);
1242
+ }
1243
+ const missingPairs = [];
1244
+ for (const [locA, urlA] of localeToUrl) {
1245
+ for (const [locB, urlB] of localeToUrl) {
1246
+ if (locA === locB)
1247
+ continue;
1248
+ // Each target's sitemap should cross-link to the other
1249
+ // We can't check the actual sitemaps here (would need network),
1250
+ // but we can verify the config is consistent
1251
+ }
1252
+ }
1253
+ if (localeToUrl.size > 1) {
1254
+ console.log(` ${c.green('✓')} cross-domain hreflang: ${[...localeToUrl.entries()].map(([l, u]) => `${l} → ${u}`).join(', ')}`);
1255
+ }
1256
+ }
961
1257
  }
962
1258
  console.log();
963
1259
  if (errors > 0) {
@@ -1014,19 +1310,26 @@ function renderErrorOverlay(err) {
1014
1310
  </html>`;
1015
1311
  }
1016
1312
  async function runDev(siteDir, port) {
1017
- const storage = createFilesystemProvider();
1018
1313
  const projectRoot = detectProjectRoot(siteDir);
1019
1314
  const templatesDir = join(projectRoot, 'templates');
1020
1315
  const adminDir = join(projectRoot, 'admin');
1021
- const site = await loadSite({ siteDir, storage, templatesDir });
1316
+ // Build the source context from the default editable target in site.yaml.
1317
+ // Cloud targets aren't init'd — admin API handles them lazily.
1318
+ const { buildSourceContext } = await import('./bootstrap.js');
1319
+ const { source, manifest, targetConfigs } = await buildSourceContext({ projectSiteDir: siteDir });
1320
+ const site = await loadSite({ contentRoot: source.contentRoot, templatesDir, manifest });
1321
+ const storage = source.storage;
1022
1322
  const app = new Hono();
1023
1323
  // ---- Live reload (SSE) ----
1024
1324
  let reloadId = 0;
1025
1325
  const reloadListeners = new Set();
1026
- function notifyReload() { reloadId++; for (const l of reloadListeners)
1027
- l(); }
1326
+ function notifyReload() {
1327
+ reloadId++;
1328
+ for (const l of reloadListeners)
1329
+ l();
1330
+ }
1028
1331
  const RELOAD_SCRIPT = `<script>new EventSource('/__reload').onmessage = () => location.reload()</script>`;
1029
- app.get('/__reload', (c) => {
1332
+ app.get('/__reload', c => {
1030
1333
  return streamSSE(c, async (stream) => {
1031
1334
  let lastId = reloadId;
1032
1335
  const check = async () => {
@@ -1036,20 +1339,53 @@ async function runDev(siteDir, port) {
1036
1339
  }
1037
1340
  };
1038
1341
  reloadListeners.add(check);
1039
- stream.onAbort(() => { reloadListeners.delete(check); });
1342
+ stream.onAbort(() => {
1343
+ reloadListeners.delete(check);
1344
+ });
1040
1345
  while (true) {
1041
1346
  await stream.sleep(500);
1042
1347
  await check();
1043
1348
  }
1044
1349
  });
1045
1350
  });
1046
- // ---- Site page routes ----
1047
- for (const [pageName, page] of site.pages) {
1351
+ // ---- Trailing slash normalization ----
1352
+ // Strip trailing slashes so /fr/ resolves as /fr and /fr/about/ as
1353
+ // /fr/about. Re-dispatches through the Hono router with the clean URL.
1354
+ // No redirect — preserves POST body and avoids round-trips.
1355
+ app.use(async (c, next) => {
1356
+ const url = new URL(c.req.url);
1357
+ if (url.pathname !== '/' && url.pathname.endsWith('/')) {
1358
+ url.pathname = url.pathname.slice(0, -1);
1359
+ return app.fetch(new Request(url, c.req.raw), c.env);
1360
+ }
1361
+ return next();
1362
+ });
1363
+ // ---- Asset serve route ----
1364
+ // Serves /assets/* from the active source target's storage. Matches the
1365
+ // URL pattern emitted by the asset resolver, so templates rendering
1366
+ // <img src="/assets/hero-a3b2c1d4.jpg"> load from here in dev.
1367
+ const { assetServeRoutes } = await import('../assets/serve-route.js');
1368
+ app.route('/', assetServeRoutes(async () => source.storage));
1369
+ // ---- Site page routes (default + locale variants) ----
1370
+ const { allPageEntries } = await import('../site-loader.js');
1371
+ for (const { name: pageName, page, locale: pageLocale } of allPageEntries(site)) {
1048
1372
  app.get(page.route, async (c) => {
1049
1373
  try {
1050
- const freshSite = await loadSite({ siteDir, storage, templatesDir });
1051
- const resolved = await resolvePage(pageName, freshSite);
1052
- const html = await renderPage(resolved, c.req.param());
1374
+ const freshSite = await loadSite({ contentRoot: source.contentRoot, templatesDir, manifest });
1375
+ const resolved = await resolvePage(pageName, freshSite, pageLocale);
1376
+ const freshPage = pageLocale
1377
+ ? freshSite.pageLocales.get(pageName)?.locales.get(pageLocale)
1378
+ : freshSite.pages.get(pageName);
1379
+ const html = await renderPage(resolved, {
1380
+ routeParams: c.req.param(),
1381
+ metadata: freshPage?.metadata ?? page.metadata,
1382
+ route: freshPage?.route ?? page.route,
1383
+ seo: {
1384
+ siteName: freshSite.manifest.name,
1385
+ locale: pageLocale ?? freshSite.manifest.locale,
1386
+ defaultOgImage: freshSite.manifest.defaultOgImage,
1387
+ },
1388
+ });
1053
1389
  return c.html(html.replace('</body>', `${RELOAD_SCRIPT}\n</body>`));
1054
1390
  }
1055
1391
  catch (err) {
@@ -1057,6 +1393,44 @@ async function runDev(siteDir, port) {
1057
1393
  }
1058
1394
  });
1059
1395
  }
1396
+ // ---- Locale fallback routes for pages without locale variants ----
1397
+ // When a page exists in the default locale but has no page.fr.json,
1398
+ // register /fr{route} that renders the default content with FR locale context.
1399
+ // This prevents 404s on locale-prefixed URLs for untranslated pages.
1400
+ const { resolveSiteLocales } = await import('../locale.js');
1401
+ const resolvedLocales = resolveSiteLocales(manifest);
1402
+ if (resolvedLocales) {
1403
+ const nonDefaultLocales = resolvedLocales.supported.filter(l => l !== resolvedLocales.default);
1404
+ for (const loc of nonDefaultLocales) {
1405
+ for (const [pageName, page] of site.pages) {
1406
+ const hasLocaleVariant = site.pageLocales.get(pageName)?.locales.has(loc);
1407
+ if (hasLocaleVariant)
1408
+ continue; // already registered by allPageEntries
1409
+ const localeRoute = `/${loc}${page.route === '/' ? '' : page.route}`;
1410
+ app.get(localeRoute, async (c) => {
1411
+ try {
1412
+ const freshSite = await loadSite({ contentRoot: source.contentRoot, templatesDir, manifest });
1413
+ const resolved = await resolvePage(pageName, freshSite, loc);
1414
+ const freshPage = freshSite.pages.get(pageName);
1415
+ const html = await renderPage(resolved, {
1416
+ routeParams: c.req.param(),
1417
+ metadata: freshPage?.metadata,
1418
+ route: freshPage?.route,
1419
+ seo: {
1420
+ siteName: freshSite.manifest.name,
1421
+ locale: loc,
1422
+ defaultOgImage: freshSite.manifest.defaultOgImage,
1423
+ },
1424
+ });
1425
+ return c.html(html.replace('</body>', `${RELOAD_SCRIPT}\n</body>`));
1426
+ }
1427
+ catch (err) {
1428
+ return c.html(renderErrorOverlay(err), 500);
1429
+ }
1430
+ });
1431
+ }
1432
+ }
1433
+ }
1060
1434
  // ---- Detect mode: dev (monorepo with apps/admin source) vs production (pre-built) ----
1061
1435
  const cmsWebDir = findCmsDir();
1062
1436
  const cmsStaticDir = findCmsStaticDir();
@@ -1066,14 +1440,14 @@ async function runDev(siteDir, port) {
1066
1440
  let cmsApp = null;
1067
1441
  if (isDevMode) {
1068
1442
  // Dev mode: mount CMS API inline (same process = shared template cache)
1069
- cmsApp = await setupCmsApi(app, siteDir, storage, templatesDir, adminDir);
1443
+ cmsApp = await setupCmsApi(app, source, siteDir, templatesDir, adminDir, targetConfigs);
1070
1444
  }
1071
1445
  else if (cmsStaticDir) {
1072
1446
  // Production mode: inline CMS API + static files
1073
- cmsApp = await setupProductionMode(app, siteDir, storage, cmsStaticDir, templatesDir, adminDir);
1447
+ cmsApp = await setupProductionMode(app, source, siteDir, cmsStaticDir, templatesDir, adminDir, targetConfigs);
1074
1448
  }
1075
1449
  // ---- 404 ----
1076
- app.notFound((c) => {
1450
+ app.notFound(c => {
1077
1451
  const routes = [...site.pages.entries()].map(([n, p]) => ` ${p.route} → ${n}`).join('\n');
1078
1452
  return c.html(`<pre style="padding:2rem">Page not found: ${c.req.path}\n\nAvailable:\n${routes}\n /admin → CMS editor</pre>`, 404);
1079
1453
  });
@@ -1092,6 +1466,37 @@ async function runDev(siteDir, port) {
1092
1466
  console.log();
1093
1467
  console.log(` ${c.dim('┃')} Pages ${[...site.pages.entries()].map(([n, p]) => `${c.dim(p.route)} ${c.dim('→')} ${n}`).join(c.dim(', '))}`);
1094
1468
  console.log(` ${c.dim('┃')} Frags ${c.dim([...site.fragments.keys()].join(', ') || '(none)')}`);
1469
+ // ---- Settings banner ----
1470
+ // Prints resolved configuration at startup so path / target / site
1471
+ // issues are diagnosed immediately instead of via empty API responses.
1472
+ // Opt-in via GAZETTA_QUIET=1 for scripted callers that don't want it.
1473
+ if (!process.env.GAZETTA_QUIET) {
1474
+ const relProject = relative(process.cwd(), projectRoot) || '.';
1475
+ const relSite = relative(projectRoot, siteDir) || '.';
1476
+ const relTemplates = relative(projectRoot, templatesDir) || '.';
1477
+ const sourceName = source.targetName ?? '(none)';
1478
+ const sourceCfg = targetConfigs[sourceName];
1479
+ const sourceEnv = sourceCfg ? getEnvironment(sourceCfg) : 'unknown';
1480
+ const sourceType = sourceCfg ? getType(sourceCfg) : 'unknown';
1481
+ const sourceEditable = sourceCfg ? isEditable(sourceCfg) : false;
1482
+ const sourceRoot = source.contentRoot.rootPath || '.';
1483
+ const targetsCount = Object.keys(targetConfigs).length;
1484
+ console.log();
1485
+ console.log(` ${c.dim('┃')} ${c.bold('Settings')}`);
1486
+ console.log(` ${c.dim('┃')} Project ${c.dim(relProject)}`);
1487
+ console.log(` ${c.dim('┃')} Site ${c.dim(relSite)}`);
1488
+ console.log(` ${c.dim('┃')} Templates ${c.dim(relTemplates)}`);
1489
+ console.log(` ${c.dim('┃')} Source ${sourceName} ${c.dim(`(${sourceEnv}, ${sourceEditable ? 'editable' : 'read-only'}, ${sourceType})`)}`);
1490
+ console.log(` ${c.dim('┃')} Content root ${c.dim(sourceRoot)}`);
1491
+ console.log(` ${c.dim('┃')} Targets (${targetsCount})`);
1492
+ for (const [name, cfg] of Object.entries(targetConfigs)) {
1493
+ const env = getEnvironment(cfg);
1494
+ const type = getType(cfg);
1495
+ const ed = isEditable(cfg) ? 'editable ' : 'read-only';
1496
+ const storagePath = cfg.storage?.type === 'filesystem' ? (cfg.storage.path ?? `targets/${name}`) : `${cfg.storage?.type ?? '?'}`;
1497
+ console.log(` ${c.dim('┃')} ${c.dim('•')} ${name.padEnd(14)} ${c.dim(env.padEnd(11))} ${c.dim(ed)} ${c.dim(type.padEnd(8))} ${c.dim('→ ' + storagePath)}`);
1498
+ }
1499
+ }
1095
1500
  if (isDevMode && cmsWebDir) {
1096
1501
  // While Vite is spinning up (compiling, scanning deps, attaching
1097
1502
  // middleware), any /admin/* request falls through to the site's 404
@@ -1109,7 +1514,11 @@ async function runDev(siteDir, port) {
1109
1514
  return true;
1110
1515
  }
1111
1516
  if (url === '/admin' || url.startsWith('/admin/') || url.startsWith('/@')) {
1112
- res.writeHead(503, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store', 'Retry-After': '2' });
1517
+ res.writeHead(503, {
1518
+ 'Content-Type': 'text/html; charset=utf-8',
1519
+ 'Cache-Control': 'no-store',
1520
+ 'Retry-After': '2',
1521
+ });
1113
1522
  res.end(LOADER_HTML);
1114
1523
  return true;
1115
1524
  }
@@ -1122,6 +1531,7 @@ async function runDev(siteDir, port) {
1122
1531
  if (loaderHandler(req, res))
1123
1532
  return;
1124
1533
  for (const l of originalListeners) {
1534
+ ;
1125
1535
  l(req, res);
1126
1536
  }
1127
1537
  });
@@ -1149,7 +1559,9 @@ async function runDev(siteDir, port) {
1149
1559
  }
1150
1560
  }
1151
1561
  }
1152
- catch { /* ignore */ }
1562
+ catch {
1563
+ /* ignore */
1564
+ }
1153
1565
  }
1154
1566
  }
1155
1567
  const vite = await createViteServer({
@@ -1178,6 +1590,7 @@ async function runDev(siteDir, port) {
1178
1590
  });
1179
1591
  const honoHandler = (req, res) => {
1180
1592
  for (const listener of originalListeners) {
1593
+ ;
1181
1594
  listener(req, res);
1182
1595
  }
1183
1596
  };
@@ -1191,7 +1604,10 @@ async function runDev(siteDir, port) {
1191
1604
  res.end(JSON.stringify({ ready: true }));
1192
1605
  return;
1193
1606
  }
1194
- if (url.startsWith('/admin/api') || url.startsWith('/admin/preview') || url === '/admin/theme.css' || url.startsWith('/admin/theme.css?')) {
1607
+ if (url.startsWith('/admin/api') ||
1608
+ url.startsWith('/admin/preview') ||
1609
+ url === '/admin/theme.css' ||
1610
+ url.startsWith('/admin/theme.css?')) {
1195
1611
  honoHandler(req, res);
1196
1612
  }
1197
1613
  else if (url.startsWith('/admin') || url.startsWith('/@')) {
@@ -1201,6 +1617,26 @@ async function runDev(siteDir, port) {
1201
1617
  honoHandler(req, res);
1202
1618
  }
1203
1619
  });
1620
+ // Force Vite to scan deps + complete initial optimization BEFORE we
1621
+ // mark the CMS ready. Without this, `cmsReady = true` fires the
1622
+ // moment Vite is created — loader page reloads, browser starts
1623
+ // fetching the SPA, and Vite's still building the dep bundle in the
1624
+ // background. The first round of imports arrives, Vite finds new
1625
+ // transitive deps, and fires `optimized dependencies changed.
1626
+ // reloading` mid-page-load. That reload cancels in-flight
1627
+ // `/admin/api/*` requests — silently breaking any component that
1628
+ // doesn't retry (FragmentBlastRadius, for one).
1629
+ //
1630
+ // Warm the SPA's main entry module (not index.html — Vite's
1631
+ // import-analysis plugin treats warmupRequest urls as JS modules
1632
+ // and chokes on HTML). The entry's transitive imports are exactly
1633
+ // what the browser will request on first load, so settling them
1634
+ // here means the browser gets a stable bundle. waitForRequestsIdle
1635
+ // blocks until Vite finishes processing the static-import chain,
1636
+ // which includes dep optimization.
1637
+ const ENTRY = '/src/client/main.ts';
1638
+ await vite.warmupRequest(ENTRY);
1639
+ await vite.waitForRequestsIdle(ENTRY);
1204
1640
  cmsReady = true;
1205
1641
  }
1206
1642
  catch (err) {
@@ -1218,23 +1654,21 @@ async function runDev(siteDir, port) {
1218
1654
  const siteWatcher = watch(siteDir, { recursive: true }, (_event, filename) => {
1219
1655
  if (!filename)
1220
1656
  return;
1657
+ // .gazetta/ is a reserved namespace (history, source-sidecars, etc.) that
1658
+ // the runtime never reads at request time. Writes there are extremely
1659
+ // frequent (one per save/publish × per-target) — treating them as
1660
+ // content changes would flood SSE reloads and reset preview iframe
1661
+ // scroll state mid-test. Filter them out at the watcher boundary.
1662
+ const norm = filename.replace(/\\/g, '/');
1663
+ if (norm.includes('/.gazetta/') || norm.startsWith('.gazetta/'))
1664
+ return;
1221
1665
  if (filename.endsWith('.json') || filename.endsWith('.yaml')) {
1222
1666
  console.log(` Manifest changed: ${filename}`);
1223
1667
  invalidateAllTemplates();
1224
- // Refresh source sidecars for external edits (git pull, direct file
1225
- // edit). PUT routes already handle their own writes — this catches
1226
- // everything outside the admin UI.
1227
- const norm = filename.replace(/\\/g, '/');
1228
- const pageMatch = /^pages\/(.+)\/page\.json$/.exec(norm);
1229
- const fragMatch = /^fragments\/(.+)\/fragment\.json$/.exec(norm);
1230
- if (pageMatch)
1231
- cmsApp?.writeSourceSidecar('page', pageMatch[1]).catch(() => { });
1232
- else if (fragMatch)
1233
- cmsApp?.writeSourceSidecar('fragment', fragMatch[1]).catch(() => { });
1234
1668
  notifyReload();
1235
1669
  }
1236
1670
  });
1237
- siteWatcher.on('error', (err) => console.warn(` File watcher warning (site): ${err.message}`));
1671
+ siteWatcher.on('error', err => console.warn(` File watcher warning (site): ${err.message}`));
1238
1672
  // Watch templates dir for template source changes
1239
1673
  if (existsSync(templatesDir)) {
1240
1674
  const tplWatcher = watch(templatesDir, { recursive: true }, (_event, filename) => {
@@ -1248,12 +1682,11 @@ async function runDev(siteDir, port) {
1248
1682
  // Drop the admin-api's cached scan so next compare/publish
1249
1683
  // rehashes. Cheap (the scan is what's slow, not invalidation).
1250
1684
  cmsApp?.invalidateTemplatesCache();
1251
- cmsApp?.invalidateSourceSidecars();
1252
1685
  notifyReload();
1253
1686
  }
1254
1687
  }
1255
1688
  });
1256
- tplWatcher.on('error', (err) => console.warn(` File watcher warning (templates): ${err.message}`));
1689
+ tplWatcher.on('error', err => console.warn(` File watcher warning (templates): ${err.message}`));
1257
1690
  }
1258
1691
  }
1259
1692
  // ---- Mount CMS API on the main Hono app (shared process = shared template cache) ----
@@ -1265,7 +1698,7 @@ async function runDev(siteDir, port) {
1265
1698
  * so user declarations win the cascade. See #134 and css-theming.md.
1266
1699
  */
1267
1700
  function mountUserThemeRoute(cmsApp, adminDir) {
1268
- cmsApp.get('/theme.css', (c) => {
1701
+ cmsApp.get('/theme.css', c => {
1269
1702
  const themePath = join(adminDir, 'theme.css');
1270
1703
  c.header('Content-Type', 'text/css; charset=utf-8');
1271
1704
  c.header('Cache-Control', 'no-cache');
@@ -1278,35 +1711,22 @@ function mountUserThemeRoute(cmsApp, adminDir) {
1278
1711
  return c.body(readFileSync(themePath, 'utf-8'));
1279
1712
  });
1280
1713
  }
1281
- async function setupCmsApi(app, siteDir, storage, templatesDir, adminDir) {
1282
- const siteYamlPath = join(siteDir, 'site.yaml');
1283
- let targetConfigs;
1284
- if (existsSync(siteYamlPath)) {
1285
- const siteYaml = yaml.load(readFileSync(siteYamlPath, 'utf-8'));
1286
- targetConfigs = siteYaml.targets;
1287
- }
1288
- const cmsApp = createAdminApp({ siteDir, storage, templatesDir, adminDir, targetConfigs });
1714
+ async function setupCmsApi(app, source, siteDir, templatesDir, adminDir, targetConfigs) {
1715
+ const cmsApp = createAdminApp({ source, siteDir, templatesDir, adminDir, targetConfigs });
1289
1716
  mountUserThemeRoute(cmsApp, adminDir);
1290
1717
  app.route('/admin', cmsApp);
1291
1718
  return cmsApp;
1292
1719
  }
1293
1720
  // ---- Production mode: inline CMS API + static files from admin-dist/ ----
1294
- async function setupProductionMode(app, siteDir, storage, cmsStaticDir, templatesDir, adminDir) {
1295
- // Read target configs from site.yaml — targets are initialized lazily on first publish/fetch
1296
- const siteYamlPath = join(siteDir, 'site.yaml');
1297
- let targetConfigs;
1298
- if (existsSync(siteYamlPath)) {
1299
- const siteYaml = yaml.load(readFileSync(siteYamlPath, 'utf-8'));
1300
- targetConfigs = siteYaml.targets;
1301
- }
1721
+ async function setupProductionMode(app, source, siteDir, cmsStaticDir, templatesDir, adminDir, targetConfigs) {
1302
1722
  // Mount CMS API inline at /admin (production mode — bundled editors/fields)
1303
- const cmsApp = createAdminApp({ siteDir, storage, templatesDir, adminDir, production: true, targetConfigs });
1723
+ const cmsApp = createAdminApp({ source, siteDir, templatesDir, adminDir, production: true, targetConfigs });
1304
1724
  mountUserThemeRoute(cmsApp, adminDir);
1305
1725
  app.route('/admin', cmsApp);
1306
1726
  // Serve pre-built CMS static files (includes bundled editors/fields)
1307
1727
  app.use('/admin/*', serveStatic({
1308
1728
  root: cmsStaticDir,
1309
- rewriteRequestPath: (path) => path.replace(/^\/admin/, ''),
1729
+ rewriteRequestPath: path => path.replace(/^\/admin/, ''),
1310
1730
  }));
1311
1731
  // SPA fallback: serve index.html for /admin and unmatched /admin/* routes
1312
1732
  const serveIndex = (c) => {
@@ -1352,11 +1772,15 @@ async function main() {
1352
1772
  }
1353
1773
  const parsed = parseArgs(args.slice(1));
1354
1774
  // Commands that take [target] [site] positional args
1355
- const targetFirstCommands = new Set(['publish', 'serve', 'deploy']);
1775
+ const targetFirstCommands = new Set(['publish', 'serve', 'deploy', 'history', 'undo']);
1356
1776
  // Commands that take [site] positional arg
1357
1777
  const siteOnlyCommands = new Set(['dev', 'validate', 'admin']);
1358
1778
  let siteDir;
1359
1779
  let targetName;
1780
+ // rollback: positional layout is `<rev> [target] [site]`. We stash
1781
+ // the revision id here because the shared positional parser uses
1782
+ // index 0 for target/site; rollback just consumes index 0 first.
1783
+ let rollbackRevisionId;
1360
1784
  if (command === 'init') {
1361
1785
  await runInit(parsed.positional[0] ?? '.');
1362
1786
  return;
@@ -1366,6 +1790,25 @@ async function main() {
1366
1790
  await runBuild(siteDir);
1367
1791
  return;
1368
1792
  }
1793
+ else if (command === 'rollback') {
1794
+ // gazetta rollback <rev> [target] [site]
1795
+ const [rev, second, third] = parsed.positional;
1796
+ if (!rev || !rev.startsWith('rev-')) {
1797
+ console.error(`\n Error: rollback requires a revision id as the first argument (e.g. gazetta rollback rev-1776337441608 [target])\n`);
1798
+ process.exit(1);
1799
+ return;
1800
+ }
1801
+ rollbackRevisionId = rev;
1802
+ const secondIsSite = second && (second.includes('/') || existsSync(join(resolve(second), 'site.yaml')));
1803
+ if (secondIsSite) {
1804
+ siteDir = await resolveSiteDir(second);
1805
+ targetName = await resolveTarget(undefined, siteDir);
1806
+ }
1807
+ else {
1808
+ siteDir = await resolveSiteDir(third);
1809
+ targetName = await resolveTarget(second, siteDir);
1810
+ }
1811
+ }
1369
1812
  else if (targetFirstCommands.has(command)) {
1370
1813
  // gazetta publish [target] [site]
1371
1814
  const [first, second] = parsed.positional;
@@ -1383,6 +1826,37 @@ async function main() {
1383
1826
  else if (siteOnlyCommands.has(command)) {
1384
1827
  siteDir = await resolveSiteDir(parsed.positional[0]);
1385
1828
  }
1829
+ else if (command === 'translate') {
1830
+ // gazetta translate <item> --to <locale> [target]
1831
+ // positional args after the item are the optional target name
1832
+ siteDir = await resolveSiteDir(undefined);
1833
+ // Find the target arg — skip the item (pages/... or fragments/...) and --to/locale flags
1834
+ const translatePositionals = parsed.positional.filter(p => !p.startsWith('pages/') && !p.startsWith('fragments/'));
1835
+ if (translatePositionals.length > 0)
1836
+ targetName = translatePositionals[0];
1837
+ }
1838
+ else if (command === 'assets') {
1839
+ // gazetta assets <subcommand> [args...] [target] [site]
1840
+ //
1841
+ // Subcommand layouts:
1842
+ // assets list [target] [site] → subcmd, target, site
1843
+ // assets info <name> [target] [site] → subcmd, name, target, site
1844
+ // assets reindex [target] [site] → subcmd, target, site
1845
+ //
1846
+ // The dispatcher in the assets-cli module reads the asset name
1847
+ // from its `args` slice; here we resolve target/site by checking
1848
+ // the positional layout.
1849
+ const subcmd = parsed.positional[0];
1850
+ if (subcmd === 'info') {
1851
+ // info has an extra positional (the asset name) before target/site.
1852
+ siteDir = await resolveSiteDir(parsed.positional[3]);
1853
+ targetName = parsed.positional[2] ? await resolveTarget(parsed.positional[2], siteDir) : undefined;
1854
+ }
1855
+ else {
1856
+ siteDir = await resolveSiteDir(parsed.positional[2]);
1857
+ targetName = parsed.positional[1] ? await resolveTarget(parsed.positional[1], siteDir) : undefined;
1858
+ }
1859
+ }
1386
1860
  else {
1387
1861
  console.error(` Unknown command: ${command}\n`);
1388
1862
  printHelp();
@@ -1425,9 +1899,96 @@ async function main() {
1425
1899
  case 'admin':
1426
1900
  await runAdmin(siteDir, parsed.port ?? 3000);
1427
1901
  break;
1902
+ case 'translate': {
1903
+ const itemArg = args[1];
1904
+ const localeArg = args.find(a => a.startsWith('--to='))?.slice(5) ?? args[args.indexOf('--to') + 1];
1905
+ if (!itemArg || !localeArg) {
1906
+ console.error(' Usage: gazetta translate <pages/name|fragments/name> --to <locale>');
1907
+ console.error(' Example: gazetta translate pages/about --to fr');
1908
+ process.exit(1);
1909
+ }
1910
+ const { normalizeLocale, localeFilename, isValidLocale } = await import('../locale.js');
1911
+ if (!isValidLocale(localeArg)) {
1912
+ console.error(` Error: invalid locale code "${localeArg}". Use BCP 47 format (e.g. fr, en-gb, pt-br)`);
1913
+ process.exit(1);
1914
+ }
1915
+ const locale = normalizeLocale(localeArg);
1916
+ const isPage = itemArg.startsWith('pages/');
1917
+ const isFragment = itemArg.startsWith('fragments/');
1918
+ if (!isPage && !isFragment) {
1919
+ console.error(` Error: item must start with pages/ or fragments/ (got "${itemArg}")`);
1920
+ process.exit(1);
1921
+ }
1922
+ // Resolve the content directory — translate operates on a target's filesystem.
1923
+ // Uses the specified target or falls back to the first editable target.
1924
+ const siteYaml = yaml.load(readFileSync(join(siteDir, 'site.yaml'), 'utf-8'));
1925
+ const { isEditable } = await import('../types.js');
1926
+ const resolvedTarget = targetName ?? Object.entries(siteYaml.targets ?? {}).find(([, cfg]) => isEditable(cfg))?.[0];
1927
+ if (!resolvedTarget) {
1928
+ console.error(' Error: no editable target found');
1929
+ process.exit(1);
1930
+ }
1931
+ const targetConfig = siteYaml.targets[resolvedTarget];
1932
+ if (!targetConfig) {
1933
+ console.error(` Error: target "${resolvedTarget}" not found in site.yaml`);
1934
+ process.exit(1);
1935
+ }
1936
+ const storagePath = targetConfig.storage.path ?? join('targets', resolvedTarget);
1937
+ const contentDir = resolve(siteDir, storagePath);
1938
+ const baseName = isPage ? 'page' : 'fragment';
1939
+ const dir = join(contentDir, itemArg);
1940
+ const sourceFile = join(dir, `${baseName}.json`);
1941
+ const destFile = join(dir, localeFilename(baseName, locale));
1942
+ const fs = await import('node:fs/promises');
1943
+ if (!existsSync(sourceFile)) {
1944
+ console.error(` Error: ${sourceFile} not found`);
1945
+ process.exit(1);
1946
+ }
1947
+ if (existsSync(destFile)) {
1948
+ console.error(` Error: ${destFile} already exists`);
1949
+ process.exit(1);
1950
+ }
1951
+ await fs.copyFile(sourceFile, destFile);
1952
+ console.log(` ${c.green('✓')} Created ${relative(process.cwd(), destFile)}`);
1953
+ console.log(` Edit the file to translate the content.`);
1954
+ break;
1955
+ }
1956
+ case 'history':
1957
+ case 'undo':
1958
+ case 'rollback': {
1959
+ const { runHistoryList, runHistoryUndo, runHistoryRollback } = await import('./history.js');
1960
+ const ctx = await resolveHistoryContext(siteDir, targetName);
1961
+ if (command === 'history')
1962
+ await runHistoryList(ctx, { limit: parsed.limit });
1963
+ else if (command === 'undo')
1964
+ await runHistoryUndo(ctx, { yes: parsed.yes });
1965
+ else
1966
+ await runHistoryRollback(ctx, rollbackRevisionId, { yes: parsed.yes });
1967
+ break;
1968
+ }
1969
+ case 'assets': {
1970
+ const { runAssetsSubcommand } = await import('./assets-cli.js');
1971
+ await runAssetsSubcommand({ args: args.slice(1), siteDir, targetName });
1972
+ break;
1973
+ }
1974
+ }
1975
+ }
1976
+ /**
1977
+ * Resolve site + target + config into the shape HistoryCommandContext
1978
+ * expects. Lives here rather than in cli/history.ts so the target-
1979
+ * resolution logic (site.yaml parsing, CI env handling) stays with
1980
+ * the other CLI commands that already do it the same way.
1981
+ */
1982
+ async function resolveHistoryContext(siteDir, targetName) {
1983
+ const { bootstrapFromSiteYaml } = await import('./bootstrap.js');
1984
+ const { targetConfigs } = await bootstrapFromSiteYaml(siteDir);
1985
+ const config = targetConfigs[targetName];
1986
+ if (!config) {
1987
+ throw new Error(`Unknown target "${targetName}". Available: ${Object.keys(targetConfigs).join(', ')}`);
1428
1988
  }
1989
+ return { siteDir, targetName, config };
1429
1990
  }
1430
- main().catch((err) => {
1991
+ main().catch(err => {
1431
1992
  console.error(`\n Error: ${err.message}\n`);
1432
1993
  process.exit(1);
1433
1994
  });