fimo 0.16.0 → 0.18.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 (162) hide show
  1. package/assets/astro/components/DateTime.astro +22 -0
  2. package/assets/astro/components/index.d.ts +1 -0
  3. package/assets/astro/components/index.js +1 -0
  4. package/assets/content-templates/shared/types.eta +2 -0
  5. package/assets/skills/fimo-cli/SKILL.md +5 -2
  6. package/assets/skills/fimo-cli/references/content.md +2 -2
  7. package/assets/skills/fimo-cli/references/deploy.md +2 -0
  8. package/assets/skills/fimo-cli/references/init.md +11 -4
  9. package/assets/skills/fimo-cli/references/locales.md +38 -14
  10. package/assets/skills/fimo-cli/references/pages.md +70 -0
  11. package/assets/skills/fimo-cli/references/webhooks.md +1 -1
  12. package/assets/skills/fimo-code/SKILL.md +11 -0
  13. package/assets/skills/fimo-code/assets/react-router-locales/LocaleLayout.tsx +12 -0
  14. package/assets/skills/fimo-code/assets/react-router-locales/routes.ts +7 -6
  15. package/assets/skills/fimo-code/references/best-practices/astro.md +47 -0
  16. package/assets/skills/fimo-code/references/best-practices/next.md +42 -0
  17. package/assets/skills/fimo-code/references/best-practices/sveltekit.md +17 -0
  18. package/assets/skills/fimo-code/references/content/richtext.md +5 -0
  19. package/assets/skills/fimo-code/references/content/schema.md +4 -1
  20. package/assets/skills/fimo-code/references/frameworks/astro.md +5 -3
  21. package/assets/skills/fimo-code/references/frameworks/hosting.md +5 -2
  22. package/assets/skills/fimo-code/references/frameworks/next/app-router.md +6 -0
  23. package/assets/skills/fimo-code/references/frameworks/next/locales.md +12 -2
  24. package/assets/skills/fimo-code/references/frameworks/next.md +7 -0
  25. package/assets/skills/fimo-code/references/frameworks/react-router.md +15 -7
  26. package/assets/skills/fimo-code/references/frameworks/sveltekit.md +15 -8
  27. package/assets/skills/fimo-code/references/labels.md +9 -1
  28. package/assets/skills/fimo-code/references/locales.md +8 -2
  29. package/assets/skills/fimo-code/references/migrations/react-router-route-ownership.md +93 -0
  30. package/assets/skills/fimo-code/references/ui.md +21 -2
  31. package/assets/skills/fimo-migration/SKILL.md +276 -51
  32. package/assets/skills/fimo-migration/references/content.md +119 -35
  33. package/assets/skills/fimo-migration/references/decision.md +72 -0
  34. package/assets/skills/fimo-migration/references/forms.md +38 -6
  35. package/assets/skills/fimo-migration/references/frameworks/nextjs.md +179 -23
  36. package/assets/skills/fimo-migration/references/import-contract.md +71 -19
  37. package/assets/skills/fimo-migration/references/manifest.md +11 -161
  38. package/assets/skills/fimo-migration/references/media.md +33 -1
  39. package/assets/skills/fimo-migration/references/platforms.md +26 -2
  40. package/assets/skills/fimo-migration/references/qa.md +167 -13
  41. package/assets/skills/fimo-migration/references/workers/content-execution.md +39 -0
  42. package/assets/skills/fimo-migration/references/workers/content-planner.md +64 -0
  43. package/assets/skills/fimo-migration/references/workers/framework.md +37 -0
  44. package/assets/skills/fimo-migration/references/workers/validator.md +25 -0
  45. package/assets/skills/fimo-migration/references/workers.md +82 -0
  46. package/assets/skills/fimo-migration/references/workflow.md +140 -192
  47. package/assets/skills/fimo-migration/scripts/apply-decision.mjs +534 -0
  48. package/assets/skills/fimo-migration/scripts/build-canonical-manifest.mjs +7 -4
  49. package/assets/skills/fimo-migration/scripts/write-migration-report.mjs +191 -4
  50. package/assets/skills/fimo-studio/references/cms.md +25 -7
  51. package/assets/skills/fimo-studio/references/webhooks.md +1 -1
  52. package/assets/sveltekit/components/DateTime.svelte +35 -0
  53. package/assets/sveltekit/components/FimoProvider.svelte +2 -2
  54. package/assets/sveltekit/components/index.d.ts +1 -0
  55. package/assets/sveltekit/components/index.js +1 -0
  56. package/dist/build/next/config.d.ts +4 -1
  57. package/dist/build/next/config.js +21 -5
  58. package/dist/build/sveltekit/vite.d.ts +3 -4
  59. package/dist/build/sveltekit/vite.js +3 -4
  60. package/dist/build/vite/plugins/runtime-env.d.ts +2 -1
  61. package/dist/build/vite/plugins/runtime-env.js +14 -10
  62. package/dist/cli/bundle.json +2 -2
  63. package/dist/cli/index.js +76 -78
  64. package/dist/runtime/app/FimoProviders.js +25 -14
  65. package/dist/runtime/app/prefetch.d.ts +3 -1
  66. package/dist/runtime/app/prefetch.js +11 -55
  67. package/dist/runtime/astro/preview.js +7 -9
  68. package/dist/runtime/content/client/internal.js +1 -0
  69. package/dist/runtime/i18n/locale.d.ts +4 -0
  70. package/dist/runtime/i18n/locale.js +14 -14
  71. package/dist/runtime/next/client-runtime.js +5 -1
  72. package/dist/runtime/next/client.d.ts +3 -2
  73. package/dist/runtime/next/client.js +2 -1
  74. package/dist/runtime/next/labels.d.ts +2 -2
  75. package/dist/runtime/next/labels.js +2 -2
  76. package/dist/runtime/next/preview.d.ts +13 -0
  77. package/dist/runtime/next/preview.js +34 -0
  78. package/dist/runtime/next/primitives/server.d.ts +2 -0
  79. package/dist/runtime/next/primitives/server.js +8 -0
  80. package/dist/runtime/next/provider.d.ts +5 -0
  81. package/dist/runtime/next/provider.js +17 -3
  82. package/dist/runtime/next/server-runtime.js +4 -1
  83. package/dist/runtime/next/server.d.ts +1 -1
  84. package/dist/runtime/next/server.js +1 -1
  85. package/dist/runtime/paths/get-fimo-paths.d.ts +6 -5
  86. package/dist/runtime/paths/get-fimo-paths.js +43 -42
  87. package/dist/runtime/preview/location.d.ts +19 -2
  88. package/dist/runtime/preview/location.js +16 -2
  89. package/dist/runtime/primitives/index.d.ts +1 -0
  90. package/dist/runtime/primitives/index.js +1 -0
  91. package/dist/runtime/react/primitives/editable/DateTime.d.ts +4 -0
  92. package/dist/runtime/react/primitives/editable/DateTime.js +13 -0
  93. package/dist/runtime/react/primitives/renderers/DateRenderer.d.ts +7 -1
  94. package/dist/runtime/react/primitives/renderers/DateRenderer.js +11 -8
  95. package/dist/runtime/react-router/{routes.d.ts → legacy/routes.d.ts} +6 -1
  96. package/dist/runtime/react-router/{routes.js → legacy/routes.js} +22 -7
  97. package/dist/runtime/routes/contract.d.ts +116 -0
  98. package/dist/runtime/routes/contract.js +377 -0
  99. package/dist/runtime/routes/index.d.ts +3 -0
  100. package/dist/runtime/routes/index.js +3 -0
  101. package/dist/runtime/routing/AppRoutesProvider.js +4 -3
  102. package/dist/runtime/seo/resolver.d.ts +5 -0
  103. package/dist/runtime/seo/resolver.js +10 -42
  104. package/dist/runtime/shared/fimo-config.types.d.ts +34 -8
  105. package/dist/runtime/shared/preview-location.d.ts +1 -22
  106. package/dist/runtime/shared/preview-location.js +5 -93
  107. package/dist/runtime/sveltekit/preview.d.ts +12 -3
  108. package/dist/runtime/sveltekit/preview.js +13 -8
  109. package/dist/scripts/sync-forms.js +1 -1
  110. package/dist/scripts/sync-schemas.js +1 -4
  111. package/dist/scripts/validate-fimo-config.js +5 -1
  112. package/dist/scripts/validate-page-registry.d.ts +2 -0
  113. package/dist/scripts/validate-page-registry.js +167 -0
  114. package/dist/scripts/validate-route-metadata.js +132 -41
  115. package/package.json +7 -3
  116. package/templates/astro/.fimo/config.json +13 -5
  117. package/templates/astro/AGENTS.md +5 -1
  118. package/templates/astro/astro.config.mjs +7 -0
  119. package/templates/astro/package.json +1 -1
  120. package/templates/astro/pnpm-workspace.yaml +0 -3
  121. package/templates/astro/src/components/ProjectOverview.astro +9 -6
  122. package/templates/astro/src/layouts/Base.astro +1 -1
  123. package/templates/astro/src/locales.ts +4 -0
  124. package/templates/astro/src/pages/[locale]/index.astro +19 -0
  125. package/templates/astro/src/pages/[locale]/projects/[slug].astro +35 -0
  126. package/templates/astro/src/pages/[locale]/projects/index.astro +19 -0
  127. package/templates/astro/src/pages/index.astro +5 -2
  128. package/templates/astro/src/pages/projects/[slug].astro +4 -2
  129. package/templates/astro/src/pages/projects/index.astro +5 -2
  130. package/templates/nextjs/.fimo/config.json +2 -1
  131. package/templates/nextjs/AGENTS.md +5 -0
  132. package/templates/nextjs/package.json +1 -1
  133. package/templates/nextjs/src/app/(default)/layout.tsx +10 -0
  134. package/templates/nextjs/src/app/[locale]/layout.tsx +22 -0
  135. package/templates/nextjs/src/app/[locale]/page.tsx +1 -0
  136. package/templates/nextjs/src/components/Document.tsx +26 -0
  137. package/templates/nextjs/src/components/FimoPreviewBridge.tsx +13 -0
  138. package/templates/nextjs/src/locales.ts +13 -0
  139. package/templates/react-router/.fimo/config.json +3 -6
  140. package/templates/react-router/AGENTS.md +4 -0
  141. package/templates/react-router/package.json +2 -2
  142. package/templates/react-router/pnpm-workspace.yaml +1 -3
  143. package/templates/react-router/react-router.config.ts +4 -10
  144. package/templates/react-router/src/pages/LocaleLayout.tsx +12 -0
  145. package/templates/react-router/src/root.tsx +2 -4
  146. package/templates/react-router/src/routes.ts +5 -3
  147. package/templates/sveltekit/.fimo/config.json +2 -1
  148. package/templates/sveltekit/AGENTS.md +5 -0
  149. package/templates/sveltekit/package.json +1 -1
  150. package/templates/sveltekit/src/app.html +1 -1
  151. package/templates/sveltekit/src/hooks.server.ts +11 -0
  152. package/templates/sveltekit/src/locales.ts +8 -0
  153. package/templates/sveltekit/src/params/locale.ts +5 -0
  154. package/templates/sveltekit/src/routes/+layout.server.ts +6 -1
  155. package/templates/sveltekit/src/routes/+layout.svelte +5 -0
  156. package/assets/skills/fimo-migration/references/inventory/coordinator.md +0 -40
  157. package/assets/skills/fimo-migration/references/inventory/locales/next-intl.md +0 -39
  158. package/assets/skills/fimo-migration/references/inventory/locales.md +0 -41
  159. package/assets/skills/fimo-migration/scripts/inventory/locales/next-intl.mjs +0 -459
  160. package/templates/nextjs/src/app/layout.tsx +0 -17
  161. /package/templates/nextjs/src/app/{page.tsx → (default)/page.tsx} +0 -0
  162. /package/templates/sveltekit/src/routes/{+page.svelte → [[locale=locale]]/+page.svelte} +0 -0
@@ -11,17 +11,19 @@ function main() {
11
11
  const manifest = readJson(manifestPath);
12
12
 
13
13
  const checks = buildChecks(manifest, projectRoot);
14
+ const dimensions = buildDimensions(manifest);
14
15
  const status = determineStatus(checks, manifest);
15
16
  const updatedManifest = {
16
17
  ...manifest,
17
18
  qa: {
18
19
  status,
19
20
  checks,
21
+ dimensions,
20
22
  reportPath: toPosix(path.relative(projectRoot, reportPath)),
21
23
  updatedAt: new Date().toISOString(),
22
24
  },
23
25
  };
24
- const report = buildReport(updatedManifest, checks);
26
+ const report = buildReport(updatedManifest, checks, dimensions, readTimings(projectRoot));
25
27
 
26
28
  fs.mkdirSync(path.dirname(reportPath), { recursive: true });
27
29
  fs.writeFileSync(reportPath, report, 'utf8');
@@ -167,7 +169,169 @@ function determineStatus(checks, manifest) {
167
169
  return 'passed-with-unresolved';
168
170
  }
169
171
 
170
- function buildReport(manifest, checks) {
172
+ // Each migration surface is reported on its own row. There is deliberately no
173
+ // aggregate score: a perfect route coverage must never average away missing
174
+ // entries or an unuploaded asset.
175
+ const DIMENSIONS = [
176
+ { id: 'routes', gate: 'blocking', items: (manifest) => manifest.routes },
177
+ { id: 'content-types', gate: 'blocking', items: (manifest) => manifest.contentTypes },
178
+ { id: 'entries', gate: 'blocking', items: (manifest) => manifest.entries },
179
+ { id: 'media', gate: 'blocking', items: (manifest) => manifest.media },
180
+ { id: 'forms', gate: 'blocking', items: (manifest) => manifest.forms },
181
+ { id: 'labels', gate: 'advisory', items: (manifest) => manifest.labels },
182
+ { id: 'locales', gate: 'advisory', items: (manifest) => manifest.locales },
183
+ { id: 'redirects', gate: 'advisory', items: (manifest) => manifest.redirects },
184
+ { id: 'seo', gate: 'advisory', items: (manifest) => manifest.seo },
185
+ ];
186
+
187
+ const UNRESOLVED_DIMENSION_AREAS = {
188
+ routes: ['route', 'routes'],
189
+ 'content-types': ['content-type', 'schema'],
190
+ entries: ['content', 'entry'],
191
+ media: ['asset', 'media'],
192
+ forms: ['form', 'forms'],
193
+ labels: ['label', 'labels'],
194
+ locales: ['locale', 'locales'],
195
+ redirects: ['redirect', 'redirects'],
196
+ seo: ['seo'],
197
+ };
198
+
199
+ const MIGRATED_STATUSES = ['imported', 'verified'];
200
+
201
+ function buildDimensions(manifest) {
202
+ return DIMENSIONS.map((dimension) => {
203
+ const items = asArray(dimension.items(manifest));
204
+ const unresolved = unresolvedForDimension(manifest, dimension.id, items);
205
+ const withStatus = (status) => items.filter((item) => item.status === status).length;
206
+ const discovered = items.length + unresolved.length;
207
+ const migrated = items.filter((item) => MIGRATED_STATUSES.includes(item.status)).length;
208
+ const unresolvedCount = withStatus('unresolved') + unresolved.length;
209
+ const unsupported = withStatus('unsupported');
210
+ // Deliberate drops are out of scope, so they do not count against coverage.
211
+ const denominator = discovered - withStatus('ignored');
212
+ const accounted = migrated + unresolvedCount + unsupported;
213
+ return {
214
+ dimension: dimension.id,
215
+ gate: dimension.gate,
216
+ discovered,
217
+ migrated,
218
+ verified: withStatus('verified'),
219
+ unresolved: unresolvedCount,
220
+ unsupported,
221
+ denominator,
222
+ coverage: denominator === 0 ? null : Math.round((accounted / denominator) * 1000) / 1000,
223
+ };
224
+ });
225
+ }
226
+
227
+ function unresolvedForDimension(manifest, dimensionId, items) {
228
+ const areas = UNRESOLVED_DIMENSION_AREAS[dimensionId] ?? [];
229
+ const itemIds = new Set(items.map((item) => item.id).filter(isNonEmptyString));
230
+ return asArray(manifest.unresolved).filter(
231
+ (item) => areas.includes(item.area) && (!isNonEmptyString(item.id) || !itemIds.has(item.id))
232
+ );
233
+ }
234
+
235
+ function dimensionTable(dimensions) {
236
+ const columns = [
237
+ 'dimension',
238
+ 'gate',
239
+ 'discovered',
240
+ 'migrated',
241
+ 'verified',
242
+ 'unresolved',
243
+ 'unsupported',
244
+ 'denominator',
245
+ 'coverage',
246
+ ];
247
+ return [
248
+ `| ${columns.join(' | ')} |`,
249
+ `| ${columns.map(() => '---').join(' | ')} |`,
250
+ ...dimensions.map(
251
+ (row) => `| ${columns.map((column) => (row[column] === null ? 'n/a' : String(row[column]))).join(' | ')} |`
252
+ ),
253
+ ].join('\n');
254
+ }
255
+
256
+ /**
257
+ * Durations live in run receipts, never in the manifest, so the canonical
258
+ * ledger stays byte-stable across identical reruns. The harness timing file is
259
+ * included when a standalone agent acceptance produced one.
260
+ */
261
+ function readTimings(projectRoot) {
262
+ return [...readHarnessTimings(projectRoot), ...readWorkerTimings(projectRoot), ...readApplyTimings(projectRoot)];
263
+ }
264
+
265
+ function readHarnessTimings(projectRoot) {
266
+ const timing = readJsonOrNull(path.join(projectRoot, '.fimo/migration/timings.json'));
267
+ if (!timing || typeof timing !== 'object') {
268
+ return [];
269
+ }
270
+ return [
271
+ {
272
+ stage: 'agent',
273
+ step: timing.model ?? 'claude',
274
+ durationMs: numberOr(timing.wallTimeMs, 0),
275
+ status: timing.timedOut ? 'timeout' : 'complete',
276
+ },
277
+ ];
278
+ }
279
+
280
+ function readWorkerTimings(projectRoot) {
281
+ const workersDir = path.join(projectRoot, '.fimo/migration/workers');
282
+ if (!fs.existsSync(workersDir)) {
283
+ return [];
284
+ }
285
+ return fs
286
+ .readdirSync(workersDir)
287
+ .filter((file) => file.endsWith('.json'))
288
+ .sort()
289
+ .flatMap((file) => {
290
+ const receipt = readJsonOrNull(path.join(workersDir, file));
291
+ if (!receipt) {
292
+ return [];
293
+ }
294
+ return [
295
+ {
296
+ stage: 'worker',
297
+ step: receipt.id ?? path.basename(file, '.json'),
298
+ durationMs: numberOr(receipt.durationMs, 0),
299
+ status: receipt.status ?? 'complete',
300
+ },
301
+ ];
302
+ });
303
+ }
304
+
305
+ function readApplyTimings(projectRoot) {
306
+ const run = readJsonOrNull(path.join(projectRoot, '.fimo/migration/apply-run.json'));
307
+ return asArray(run?.phases).map((item) => ({
308
+ stage: 'apply',
309
+ step: item.phase ?? 'unknown',
310
+ durationMs: numberOr(item.durationMs, 0),
311
+ status: item.status ?? 'complete',
312
+ }));
313
+ }
314
+
315
+ function readJsonOrNull(filePath) {
316
+ try {
317
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
318
+ } catch {
319
+ return null;
320
+ }
321
+ }
322
+
323
+ function timingSection(timings) {
324
+ if (timings.length === 0) {
325
+ return '- No run receipts recorded.';
326
+ }
327
+ return [
328
+ '| stage | step | status | duration_ms |',
329
+ '| --- | --- | --- | --- |',
330
+ ...timings.map((item) => `| ${item.stage} | ${item.step} | ${item.status} | ${item.durationMs} |`),
331
+ ].join('\n');
332
+ }
333
+
334
+ function buildReport(manifest, checks, dimensions, timings) {
171
335
  const statusLabel =
172
336
  manifest.qa.status === 'passed'
173
337
  ? 'complete'
@@ -188,6 +352,11 @@ function buildReport(manifest, checks) {
188
352
  bullet('Platform', manifest.source?.platform),
189
353
  bullet('Public URL', manifest.source?.publicUrl),
190
354
  bullet('Root path', manifest.source?.rootPath),
355
+ bullet('CLI', manifest.source?.cli),
356
+ '',
357
+ '## Coverage by dimension',
358
+ '',
359
+ dimensionTable(dimensions),
191
360
  '',
192
361
  '## Routes',
193
362
  '',
@@ -211,6 +380,11 @@ function buildReport(manifest, checks) {
211
380
  `- SEO items: ${asArray(manifest.seo).length}`,
212
381
  `- Redirects: ${asArray(manifest.redirects).length}`,
213
382
  `- Locales: ${asArray(manifest.locales).length}`,
383
+ `- Labels: ${asArray(manifest.labels).length}`,
384
+ '',
385
+ '## Timing',
386
+ '',
387
+ timingSection(timings),
214
388
  '',
215
389
  '## Checks',
216
390
  '',
@@ -335,7 +509,7 @@ function contentCountsPass(manifest) {
335
509
  function schemasPushedPass(manifest) {
336
510
  const requiredTypes = entryBatchTypesNeedingSchema(manifest);
337
511
  if (requiredTypes.size === 0) return true;
338
- const byName = new Map(asArray(manifest.contentTypes).map((type) => [type.name ?? type.uid, type]));
512
+ const byName = contentTypeIndex(manifest);
339
513
  for (const name of requiredTypes) {
340
514
  const type = byName.get(name);
341
515
  if (!type || !['imported', 'verified'].includes(type.status)) return false;
@@ -343,9 +517,22 @@ function schemasPushedPass(manifest) {
343
517
  return true;
344
518
  }
345
519
 
520
+ // An entry batch names its content type by uid (that is what `fimo entries`
521
+ // takes), while a manifest content type also carries a human display name.
522
+ // Index both so neither spelling looks like a missing schema.
523
+ function contentTypeIndex(manifest) {
524
+ const index = new Map();
525
+ for (const type of asArray(manifest.contentTypes)) {
526
+ for (const key of [type.uid, type.name]) {
527
+ if (isNonEmptyString(key)) index.set(key, type);
528
+ }
529
+ }
530
+ return index;
531
+ }
532
+
346
533
  function schemaPushStats(manifest) {
347
534
  const requiredTypes = entryBatchTypesNeedingSchema(manifest);
348
- const byName = new Map(asArray(manifest.contentTypes).map((type) => [type.name ?? type.uid, type]));
535
+ const byName = contentTypeIndex(manifest);
349
536
  const pushed = [...requiredTypes].filter((name) => ['imported', 'verified'].includes(byName.get(name)?.status));
350
537
  return {
351
538
  requiredTypes: requiredTypes.size,
@@ -1,24 +1,41 @@
1
1
  ## How the CMS Works
2
2
 
3
- The CMS is where you manage your site's structured content. It separates content by whether a schema is repeatable:
3
+ The CMS is where you manage your site's structured content:
4
4
 
5
+ - **Pages** compose declared Labels, Singles, and Collection queries around the routes where they appear.
5
6
  - **Collections** hold multiple entries of the same kind, such as Blog posts, Team members, or Testimonials.
6
7
  - **Singles** hold one set of content, such as Site settings, a Homepage, or an About page.
7
8
  - **Entries** are the individual records inside a Collection.
8
9
 
9
10
  Both use schemas to define their fields. The difference is whether you edit one set of content or manage a repeatable list.
10
11
 
12
+ Pages is still a pre-release Studio surface. It is visible in development and staging, but hidden in production until the public release. If Pages is absent, use Collections, Singles, and Labels & Text normally.
13
+
11
14
  ## Browsing Your Content
12
15
 
13
16
  1. Click the **Content tab** (the database icon) in the top navigation bar.
14
- 2. Choose **Collections** or **Singles** in the left sidebar. **Forms** and **Labels & Text** follow them in the same list.
17
+ 2. Choose **Pages**, **Collections**, or **Singles** in the left sidebar. **Forms** and **Labels & Text** follow them in the same list.
15
18
  3. Hover over or focus the info icon beside any item for a short explanation.
16
- 4. Each destination shows cards for the schemas in that group.
17
- 5. Click a Collection to see its entries, then click an entry to edit it.
18
- 6. Click a Single to edit it directly.
19
+ 4. Pages shows the site's route tree. Collections and Singles show cards for the schemas in that group.
20
+ 5. Select a page to edit its declared sources together. For a dynamic route, choose the concrete entry from the header selector.
21
+ 6. Click a Collection to see its entries, then click an entry to edit it.
22
+ 7. Click a Single to edit it directly.
19
23
 
20
24
  Collection cards show their entry count. Single cards are labeled **Single** instead because they always represent one set of content.
21
25
 
26
+ ## Editing by Page
27
+
28
+ 1. Open **Content → Pages**.
29
+ 2. Select a route from the page tree. The homepage stays first; related routes can be grouped into folders.
30
+ 3. Edit its Labels, Singles, bound entry, or Collection-source results in one page-shaped view.
31
+ 4. Use **Open in Preview** to switch to the selected route.
32
+
33
+ Agents declare each route's `params` and `sources`; Studio uses those declarations to build the Pages tree and show the content editors manage for that route.
34
+
35
+ Label and Single sources use the page as their editing home. Collection sources remain reusable and stay visible in Collections; Pages only shows the entries selected by the declared query. For a dynamic page, selecting a concrete entry resolves any `$params.<param>.<field>` values and refreshes dependent sources.
36
+
37
+ A route with several parameters, such as `/shop/:category/:product`, is chosen left to right. Studio asks for the category first, then offers only products matching `where: { "category": "$params.category.id" }`. Changing the category clears the product selection so the page cannot keep a product from another category.
38
+
22
39
  ## Editing a Single
23
40
 
24
41
  1. Open **Content → Singles**.
@@ -53,7 +70,7 @@ Fimo does not send the prompt automatically. After the AI finishes, translated e
53
70
  2. Click the **+** (plus) icon button at the top of the Collection (labeled **New entry** on hover). If the Collection is empty, use the **Create entry** button in the center of the empty state instead.
54
71
  3. Fill in the form fields (text, images, dates, whatever the collection defines).
55
72
 
56
- Changes save automatically as you fill in each field (the save is debounced), so there is no Save button. The save status appears in the top bar (Saving / Saved). An incomplete new entry waits until its required fields are valid before it becomes part of the Collection. If the Collection is wired to a page on your site, new content shows up automatically.
73
+ Changes save automatically as you fill in each field (the save is debounced), so there is no Save button. The save status appears in the top bar (Saving / Saved). An incomplete new entry waits until its required fields are valid before it becomes part of the Collection. A page source can make matching entries visible from the Pages workspace.
57
74
 
58
75
  ## Editing an Entry
59
76
 
@@ -100,7 +117,8 @@ The AI chat can help you manage content without navigating through menus:
100
117
 
101
118
  - **Collections** are best for repeatable content. Open one to search, sort, create, and manage its entries.
102
119
  - **Singles** are best for one-off content. Open one and edit it directly.
103
- - **Content appears automatically** if its schema is connected (wired) to a page on your site. You do not need to manually link each entry.
120
+ - **Pages** organize existing labels and entries around the structure of the site.
121
+ - **Page sources organize usage, not entry membership.** A Collection query decides which reusable entries the Pages workspace shows; the entries remain available under Collections.
104
122
  - **Forms tab**: View and manage form submissions collected from your site's forms.
105
123
  - **Labels & Text tab**: Edit your site's UI labels and static text strings as key-and-value rows. This is for your site's interface copy, not for translated copies of collection entries.
106
124
  - **Search and sort**: Use the search bar to filter entries and the sort dropdown to order them quickly.
@@ -1,6 +1,6 @@
1
1
  # Webhooks
2
2
 
3
- > **Beta:** Test each destination and review its delivery history before depending on it in production.
3
+ > Test each destination and review its delivery history before depending on it in production.
4
4
 
5
5
  Webhooks send project activity to an external HTTPS endpoint. Use them for focused reactions such as adding a waitlist submission to an email tool, notifying Slack after a publish, or starting a GitHub workflow after an agent review.
6
6
 
@@ -0,0 +1,35 @@
1
+ <script lang="ts">
2
+ import type { HTMLTimeAttributes } from 'svelte/elements';
3
+
4
+ import type { FimoString } from 'fimo/sveltekit';
5
+ import { getFimoSource } from 'fimo/sveltekit';
6
+
7
+ type Props = Omit<HTMLTimeAttributes, 'datetime' | 'children'> & {
8
+ value: FimoString | globalThis.Date | string | number | null | undefined;
9
+ locale?: string;
10
+ options?: Intl.DateTimeFormatOptions;
11
+ };
12
+
13
+ let { value, locale, options, ...props }: Props = $props();
14
+ const source = $derived(getFimoSource(value));
15
+ const rendered = $derived.by(() => {
16
+ if (value == null) {
17
+ return null;
18
+ }
19
+ const raw = value instanceof String ? value.toString() : value;
20
+ const date = new globalThis.Date(raw as string | number | globalThis.Date);
21
+ if (Number.isNaN(date.getTime())) {
22
+ return null;
23
+ }
24
+ return {
25
+ // The whole instant, not just the day: a `datetime` field exists to name
26
+ // a moment, and truncating it here would throw the time away.
27
+ datetime: date.toISOString(),
28
+ text: date.toLocaleString(locale, options ?? { dateStyle: 'medium', timeStyle: 'short' }),
29
+ };
30
+ });
31
+ </script>
32
+
33
+ {#if rendered}
34
+ <time {...props} datetime={rendered.datetime} data-fimo-source={source}>{rendered.text}</time>
35
+ {/if}
@@ -22,12 +22,12 @@
22
22
  return;
23
23
  }
24
24
 
25
- return connectPreview({ goto, invalidateAll });
25
+ return connectPreview({ goto, invalidateAll, locale: () => labels.locale });
26
26
  });
27
27
 
28
28
  afterNavigate(() => {
29
29
  if (previewEnabled) {
30
- sendPreviewLocation();
30
+ sendPreviewLocation(labels.locale);
31
31
  }
32
32
  });
33
33
  </script>
@@ -1,5 +1,6 @@
1
1
  export { default as Boolean } from './Boolean.svelte';
2
2
  export { default as Date } from './Date.svelte';
3
+ export { default as DateTime } from './DateTime.svelte';
3
4
  export { default as FimoProvider } from './FimoProvider.svelte';
4
5
  export { default as Image } from './Image.svelte';
5
6
  export { default as Json } from './Json.svelte';
@@ -1,5 +1,6 @@
1
1
  export { default as Boolean } from './Boolean.svelte';
2
2
  export { default as Date } from './Date.svelte';
3
+ export { default as DateTime } from './DateTime.svelte';
3
4
  export { default as FimoProvider } from './FimoProvider.svelte';
4
5
  export { default as Image } from './Image.svelte';
5
6
  export { default as Json } from './Json.svelte';
@@ -5,8 +5,11 @@ export interface FimoNextConfigContext {
5
5
  export type FimoNextConfigFactory<TConfig extends object = object> = (phase: string, context: FimoNextConfigContext) => TConfig | Promise<TConfig>;
6
6
  export type FimoNextConfig<TConfig extends object = object> = TConfig | FimoNextConfigFactory<TConfig>;
7
7
  /**
8
- * Connect a Next development server to Fimo's hosted preview gateway.
8
+ * Wire a Next application to its linked Fimo project.
9
9
  *
10
+ * Fills the Fimo runtime env from the local link in `next dev`, `next build`,
11
+ * and `next start` — per key, only when nothing else set it — and, in
12
+ * development only, connects the dev server to Fimo's hosted preview gateway.
10
13
  * The platform supplies allowed preview origins at runtime, so applications do
11
14
  * not need to know which preview provider Fimo currently uses.
12
15
  */
@@ -1,5 +1,13 @@
1
1
  import { findStaleFimoOverride, resolveLocalRuntimeEnv, staleOverrideMessage, } from '../../runtime/shared/local-runtime-env.js';
2
2
  const NEXT_DEVELOPMENT_PHASE = 'phase-development-server';
3
+ // The content runtime reads process.env only, so every phase that runs
4
+ // application code needs the derived env: the dev server, the production build
5
+ // (prerendering + NEXT_PUBLIC_* inlining), and the production server.
6
+ const RUNTIME_ENV_PHASES = new Set([
7
+ NEXT_DEVELOPMENT_PHASE,
8
+ 'phase-production-build',
9
+ 'phase-production-server',
10
+ ]);
3
11
  const PREVIEW_ORIGINS_ENV = 'FIMO_PREVIEW_ALLOWED_ORIGINS';
4
12
  function readPreviewOrigins() {
5
13
  return (process.env[PREVIEW_ORIGINS_ENV] ?? '')
@@ -8,8 +16,11 @@ function readPreviewOrigins() {
8
16
  .filter(Boolean);
9
17
  }
10
18
  /**
11
- * Connect a Next development server to Fimo's hosted preview gateway.
19
+ * Wire a Next application to its linked Fimo project.
12
20
  *
21
+ * Fills the Fimo runtime env from the local link in `next dev`, `next build`,
22
+ * and `next start` — per key, only when nothing else set it — and, in
23
+ * development only, connects the dev server to Fimo's hosted preview gateway.
13
24
  * The platform supplies allowed preview origins at runtime, so applications do
14
25
  * not need to know which preview provider Fimo currently uses.
15
26
  */
@@ -18,7 +29,7 @@ export function withFimo(applicationConfig) {
18
29
  const config = typeof applicationConfig === 'function'
19
30
  ? await applicationConfig(phase, context)
20
31
  : applicationConfig;
21
- if (phase !== NEXT_DEVELOPMENT_PHASE) {
32
+ if (!RUNTIME_ENV_PHASES.has(phase)) {
22
33
  return config;
23
34
  }
24
35
  // Next has already loaded `.env*` into `process.env` by the time this runs,
@@ -26,9 +37,11 @@ export function withFimo(applicationConfig) {
26
37
  // env files supplied it — only then does the git branch decide, per key.
27
38
  const derived = resolveLocalRuntimeEnv({ cwd: process.cwd() });
28
39
  if (derived) {
29
- const stale = findStaleFimoOverride(process.env, derived);
30
- if (stale) {
31
- console.warn(staleOverrideMessage(stale));
40
+ if (phase === NEXT_DEVELOPMENT_PHASE) {
41
+ const stale = findStaleFimoOverride(process.env, derived);
42
+ if (stale) {
43
+ console.warn(staleOverrideMessage(stale));
44
+ }
32
45
  }
33
46
  for (const [key, value] of Object.entries(derived.values)) {
34
47
  if (!process.env[key]) {
@@ -36,6 +49,9 @@ export function withFimo(applicationConfig) {
36
49
  }
37
50
  }
38
51
  }
52
+ if (phase !== NEXT_DEVELOPMENT_PHASE) {
53
+ return config;
54
+ }
39
55
  const previewOrigins = readPreviewOrigins();
40
56
  if (previewOrigins.length === 0) {
41
57
  return config;
@@ -1,11 +1,10 @@
1
1
  import type { PluginOption } from 'vite';
2
2
  /**
3
- * Narrow SvelteKit build connector. Points the dev server at the git branch's
4
- * Fimo env (`PUBLIC_FIMO_API_URL`, `FIMO_ENV`), and tags native Svelte
3
+ * Narrow SvelteKit build connector. Points dev and production builds at the
4
+ * git branch's Fimo env (`PUBLIC_FIMO_API_URL`, `FIMO_ENV`), and tags native Svelte
5
5
  * template elements with deterministic `data-fimo-id="<path>#<line>-<column>"`
6
6
  * attributes during `vite dev` so the Fimo preview can map DOM nodes back to
7
- * source. Explicit `data-fimo-id` attributes are preserved and production
8
- * builds are untouched.
7
+ * source. Explicit `data-fimo-id` attributes are preserved.
9
8
  *
10
9
  * Usage in the app's vite.config.ts: `plugins: [fimo(), sveltekit()]`.
11
10
  */
@@ -1,12 +1,11 @@
1
1
  import runtimeEnvPlugin from '../vite/plugins/runtime-env.js';
2
2
  import svelteDataIdPlugin from './data-id.js';
3
3
  /**
4
- * Narrow SvelteKit build connector. Points the dev server at the git branch's
5
- * Fimo env (`PUBLIC_FIMO_API_URL`, `FIMO_ENV`), and tags native Svelte
4
+ * Narrow SvelteKit build connector. Points dev and production builds at the
5
+ * git branch's Fimo env (`PUBLIC_FIMO_API_URL`, `FIMO_ENV`), and tags native Svelte
6
6
  * template elements with deterministic `data-fimo-id="<path>#<line>-<column>"`
7
7
  * attributes during `vite dev` so the Fimo preview can map DOM nodes back to
8
- * source. Explicit `data-fimo-id` attributes are preserved and production
9
- * builds are untouched.
8
+ * source. Explicit `data-fimo-id` attributes are preserved.
10
9
  *
11
10
  * Usage in the app's vite.config.ts: `plugins: [fimo(), sveltekit()]`.
12
11
  */
@@ -5,7 +5,8 @@ import { type Plugin } from 'vite';
5
5
  * One rule, per variable: anything already set — by the process env (sandbox,
6
6
  * shell) or the user's own `.env*` files, read through Vite's `loadEnv` — is
7
7
  * left alone; derivation fills only what is missing. A value a user wrote by
8
- * hand is an override, and this plugin must never win over it.
8
+ * hand is an override, and this plugin must never win over it. Empty or
9
+ * `undefined` `define` placeholders generated from a missing env are not values.
9
10
  *
10
11
  * Two injection paths, both needed:
11
12
  * - `process.env`, so Vite's own `VITE_*` exposure, `loadEnv`, and server-side
@@ -6,7 +6,8 @@ import { findStaleFimoOverride, resolveLocalRuntimeEnv, staleOverrideMessage, }
6
6
  * One rule, per variable: anything already set — by the process env (sandbox,
7
7
  * shell) or the user's own `.env*` files, read through Vite's `loadEnv` — is
8
8
  * left alone; derivation fills only what is missing. A value a user wrote by
9
- * hand is an override, and this plugin must never win over it.
9
+ * hand is an override, and this plugin must never win over it. Empty or
10
+ * `undefined` `define` placeholders generated from a missing env are not values.
10
11
  *
11
12
  * Two injection paths, both needed:
12
13
  * - `process.env`, so Vite's own `VITE_*` exposure, `loadEnv`, and server-side
@@ -33,24 +34,27 @@ export default function runtimeEnvPlugin() {
33
34
  // `import.meta.env`, which `define` patches at transform time.
34
35
  order: 'pre',
35
36
  handler(config, { command, mode }) {
36
- // Production builds get their env from the platform. Only a local dev
37
- // server has a git branch worth deriving from.
38
- if (command !== 'serve') {
39
- return;
40
- }
41
37
  const cwd = config.root ?? process.cwd();
42
38
  const derived = resolveLocalRuntimeEnv({ cwd });
43
39
  if (!derived) {
44
40
  return;
45
41
  }
46
42
  const explicit = { ...loadEnv(mode, config.envDir ?? cwd, ''), ...process.env };
47
- const stale = findStaleFimoOverride(explicit, derived);
48
- if (stale) {
49
- console.warn(staleOverrideMessage(stale));
43
+ if (command === 'serve') {
44
+ const stale = findStaleFimoOverride(explicit, derived);
45
+ if (stale) {
46
+ console.warn(staleOverrideMessage(stale));
47
+ }
50
48
  }
51
49
  // Decide what is missing BEFORE writing to process.env, or the define
52
50
  // step below would see the values this loop just set.
53
- const missing = Object.entries(derived.values).filter(([key]) => !explicit[key]);
51
+ const missing = Object.entries(derived.values).filter(([key]) => {
52
+ if (explicit[key]) {
53
+ return false;
54
+ }
55
+ const defined = config.define?.[`import.meta.env.${key}`];
56
+ return defined === undefined || defined === 'undefined' || defined === JSON.stringify('');
57
+ });
54
58
  for (const [key, value] of missing) {
55
59
  process.env[key] = value;
56
60
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "bundled": true,
3
3
  "bundler": "esbuild",
4
- "bundledAt": "2026-08-19T15:51:49.987Z",
5
- "cliVersion": "0.16.0",
4
+ "bundledAt": "2026-08-20T15:31:22.295Z",
5
+ "cliVersion": "0.18.0",
6
6
  "external": [
7
7
  "oxc-parser",
8
8
  "fsevents",