fimo 0.17.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 (159) 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 +3 -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/pnpm-workspace.yaml +0 -3
  120. package/templates/astro/src/components/ProjectOverview.astro +9 -6
  121. package/templates/astro/src/layouts/Base.astro +1 -1
  122. package/templates/astro/src/locales.ts +4 -0
  123. package/templates/astro/src/pages/[locale]/index.astro +19 -0
  124. package/templates/astro/src/pages/[locale]/projects/[slug].astro +35 -0
  125. package/templates/astro/src/pages/[locale]/projects/index.astro +19 -0
  126. package/templates/astro/src/pages/index.astro +5 -2
  127. package/templates/astro/src/pages/projects/[slug].astro +4 -2
  128. package/templates/astro/src/pages/projects/index.astro +5 -2
  129. package/templates/nextjs/.fimo/config.json +2 -1
  130. package/templates/nextjs/AGENTS.md +5 -0
  131. package/templates/nextjs/src/app/(default)/layout.tsx +10 -0
  132. package/templates/nextjs/src/app/[locale]/layout.tsx +22 -0
  133. package/templates/nextjs/src/app/[locale]/page.tsx +1 -0
  134. package/templates/nextjs/src/components/Document.tsx +26 -0
  135. package/templates/nextjs/src/components/FimoPreviewBridge.tsx +13 -0
  136. package/templates/nextjs/src/locales.ts +13 -0
  137. package/templates/react-router/.fimo/config.json +3 -6
  138. package/templates/react-router/AGENTS.md +4 -0
  139. package/templates/react-router/package.json +1 -1
  140. package/templates/react-router/pnpm-workspace.yaml +1 -3
  141. package/templates/react-router/react-router.config.ts +4 -10
  142. package/templates/react-router/src/pages/LocaleLayout.tsx +12 -0
  143. package/templates/react-router/src/root.tsx +2 -4
  144. package/templates/react-router/src/routes.ts +5 -3
  145. package/templates/sveltekit/.fimo/config.json +2 -1
  146. package/templates/sveltekit/AGENTS.md +5 -0
  147. package/templates/sveltekit/src/app.html +1 -1
  148. package/templates/sveltekit/src/hooks.server.ts +11 -0
  149. package/templates/sveltekit/src/locales.ts +8 -0
  150. package/templates/sveltekit/src/params/locale.ts +5 -0
  151. package/templates/sveltekit/src/routes/+layout.server.ts +6 -1
  152. package/templates/sveltekit/src/routes/+layout.svelte +5 -0
  153. package/assets/skills/fimo-migration/references/inventory/coordinator.md +0 -40
  154. package/assets/skills/fimo-migration/references/inventory/locales/next-intl.md +0 -39
  155. package/assets/skills/fimo-migration/references/inventory/locales.md +0 -41
  156. package/assets/skills/fimo-migration/scripts/inventory/locales/next-intl.mjs +0 -459
  157. package/templates/nextjs/src/app/layout.tsx +0 -17
  158. /package/templates/nextjs/src/app/{page.tsx → (default)/page.tsx} +0 -0
  159. /package/templates/sveltekit/src/routes/{+page.svelte → [[locale=locale]]/+page.svelte} +0 -0
@@ -15,6 +15,7 @@ import { ScreenshotProvider } from '../iframe/ScreenshotProvider.js';
15
15
  import { getSafePreviewNavigationPath, isPreviewRuntimeEnabled } from '../preview/bridge.js';
16
16
  import { getPreviewLocationChange, getPreviewParentOrigin } from '../preview/location.js';
17
17
  import { getActiveRouterRoutes } from '../react-router/active-routes.js';
18
+ import { matchFimoRoute } from '../routes/contract.js';
18
19
  import { OptionalTracking } from '../t/OptionalTracking.js';
19
20
  export { getPreviewLocationChange, getPreviewLocationPath } from '../preview/location.js';
20
21
  const titleCase = (seg) => seg
@@ -45,6 +46,21 @@ function getConfigDisplayRoutes() {
45
46
  function getCanonicalRoutePath(path) {
46
47
  return resolveFimoLocale({ pathname: path }, fimoConfig).routePath;
47
48
  }
49
+ /**
50
+ * The locale a pathname renders.
51
+ *
52
+ * The declared route decides it. A route that declares no `:locale` segment is
53
+ * app-owned and renders the default locale whatever its first segment spells,
54
+ * which is what stops `/fr` from switching the page to French when `fr` is
55
+ * really an entry slug under a top-level `/:slug` route.
56
+ *
57
+ * Only a URL that no declared route answers falls back to the compatibility
58
+ * prefix resolver. Existing apps can still rely on that inference while new
59
+ * route-owned integrations pass an explicit locale to `FimoProviders`.
60
+ */
61
+ function getRouteLocale(pathname) {
62
+ return (matchFimoRoute(pathname, fimoConfig)?.locale ?? resolveFimoLocale({ pathname }, fimoConfig).locale);
63
+ }
48
64
  function hasReadableFrameworkManifest(manifest) {
49
65
  return Boolean(manifest &&
50
66
  typeof manifest === 'object' &&
@@ -84,27 +100,20 @@ export function getDisplayRoutes(frameworkManifest) {
84
100
  * `nav/read-routes`) and reads routes through the `readRoutes` callback, so a
85
101
  * future non-React-Router runtime reuses it by supplying its own reader.
86
102
  */
87
- function PreviewBridge({ readRoutes }) {
103
+ function PreviewBridge({ readRoutes, locale }) {
88
104
  const navigate = useNavigate();
89
105
  const location = useLocation();
90
- const localeResolution = useMemo(() => resolveFimoLocale({ pathname: location.pathname }, fimoConfig), [location.pathname]);
91
106
  const sendRoutes = useCallback(() => {
92
107
  // Read at send time: the router can change its route table in place
93
108
  // during HMR, so an explicit nav/read-routes must see the current state.
94
109
  window.parent.postMessage({ type: 'nav/routes', payload: { routes: readRoutes() } }, getPreviewParentOrigin() ?? '*');
95
110
  }, [readRoutes]);
111
+ // `locale` is a dependency on purpose: an app that settles a new locale
112
+ // without moving the pathname still has to tell Studio about it.
96
113
  const sendLocationChange = useCallback(() => {
97
- const locationChange = getPreviewLocationChange(location);
98
114
  const parentOrigin = getPreviewParentOrigin();
99
- window.parent.postMessage({
100
- type: 'nav/locationChange',
101
- payload: {
102
- ...(parentOrigin ? locationChange : { path: locationChange.path }),
103
- routePath: localeResolution.routePath,
104
- locale: localeResolution.locale,
105
- },
106
- }, parentOrigin ?? '*');
107
- }, [location.hash, location.pathname, location.search, localeResolution.locale, localeResolution.routePath]);
115
+ window.parent.postMessage({ type: 'nav/locationChange', payload: getPreviewLocationChange(location, locale, parentOrigin) }, parentOrigin ?? '*');
116
+ }, [location.hash, location.pathname, location.search, locale]);
108
117
  useEffect(sendRoutes, [sendRoutes]);
109
118
  useEffect(sendLocationChange, [sendLocationChange]);
110
119
  useEffect(() => {
@@ -156,7 +165,9 @@ export function FimoProviders({ children, locale }) {
156
165
  const readRoutes = useCallback(() => getDisplayRoutes(frameworkContext?.manifest), [frameworkContext?.manifest]);
157
166
  const routes = readRoutes();
158
167
  const localeResolution = useMemo(() => resolveFimoLocale({ pathname: location.pathname }, fimoConfig), [location.pathname]);
159
- const activeLocale = locale ?? localeResolution.locale;
168
+ // The app is authoritative: an explicit `locale` wins over anything Fimo can
169
+ // infer, and inference itself goes through the declared route contract.
170
+ const activeLocale = useMemo(() => locale ?? getRouteLocale(location.pathname), [locale, location.pathname]);
160
171
  const activeDir = getFimoTextDirection(activeLocale);
161
172
  // @ts-ignore - import.meta.env is provided by Vite and React Router.
162
173
  const previewEnabled = isPreviewRuntimeEnabled(import.meta.env.VITE_PREVIEW_SCRIPT_URL);
@@ -171,5 +182,5 @@ export function FimoProviders({ children, locale }) {
171
182
  ...localeResolution,
172
183
  locale: activeLocale,
173
184
  dir: activeDir,
174
- }, children: _jsx(DataSourcesProvider, { children: _jsx(ScreenshotProvider, { children: _jsx(ViteLifecycles, { children: _jsxs(RefreshContentProvider, { children: [previewEnabled ? _jsx(PreviewBridge, { readRoutes: readRoutes }) : null, _jsx(OptionalTracking, { routes: routes }), _jsx(Suspense, { children: children })] }) }) }) }) }) }) }));
185
+ }, children: _jsx(DataSourcesProvider, { children: _jsx(ScreenshotProvider, { children: _jsx(ViteLifecycles, { children: _jsxs(RefreshContentProvider, { children: [previewEnabled ? _jsx(PreviewBridge, { readRoutes: readRoutes, locale: activeLocale }) : null, _jsx(OptionalTracking, { routes: routes }), _jsx(Suspense, { children: children })] }) }) }) }) }) }) }));
175
186
  }
@@ -1,7 +1,9 @@
1
1
  export declare function resolveFimoLocaleForUrl(url: string): import("./index.js").FimoLocaleResolution;
2
2
  /**
3
3
  * Fetch route data for SEO meta resolution.
4
- * Only fetches data for detail pages (routes with `params` in .fimo/config.json).
4
+ *
5
+ * Only entry params are fetched: a locale param is resolved from the URL and
6
+ * never corresponds to a CMS schema, so it must not produce a request.
5
7
  */
6
8
  export declare function resolveMetaEntries(url: string): Promise<Record<string, unknown> | undefined>;
7
9
  //# sourceMappingURL=prefetch.d.ts.map
@@ -1,57 +1,15 @@
1
1
  // @ts-ignore - virtual module provided by fimoConfigPlugin in fimo/vite
2
2
  import fimoConfig from 'virtual:fimo-config';
3
3
  import { resolveFimoLocale } from '../i18n/locale.js';
4
- /**
5
- * Match a concrete URL against the routes in .fimo/config.json.
6
- * Returns the matching route config or null.
7
- */
8
- function matchRoute(url) {
9
- const config = fimoConfig;
10
- const localeResolution = resolveFimoLocale(url, config);
11
- const pathname = localeResolution.routePath;
12
- const routes = config.routes ?? {};
13
- for (const [id, config] of Object.entries(routes)) {
14
- if (!config.path) {
15
- continue;
16
- }
17
- const patternSegments = config.path.split('/').filter(Boolean);
18
- const urlSegments = pathname.split('/').filter(Boolean);
19
- if (patternSegments.length !== urlSegments.length) {
20
- continue;
21
- }
22
- const paramValues = {};
23
- let match = true;
24
- for (let i = 0; i < patternSegments.length; i++) {
25
- const pattern = patternSegments[i];
26
- const value = urlSegments[i];
27
- if (pattern.startsWith(':')) {
28
- paramValues[pattern.slice(1)] = value;
29
- }
30
- else if (pattern !== value) {
31
- match = false;
32
- break;
33
- }
34
- }
35
- if (match) {
36
- return { id, config, paramValues };
37
- }
38
- }
39
- // Handle index route
40
- if (pathname === '/' || pathname === '') {
41
- for (const [id, config] of Object.entries(routes)) {
42
- if (config.path === '/') {
43
- return { id, config, paramValues: {} };
44
- }
45
- }
46
- }
47
- return null;
48
- }
4
+ import { matchFimoRoute } from '../routes/contract.js';
49
5
  export function resolveFimoLocaleForUrl(url) {
50
6
  return resolveFimoLocale(url, fimoConfig);
51
7
  }
52
8
  /**
53
9
  * Fetch route data for SEO meta resolution.
54
- * Only fetches data for detail pages (routes with `params` in .fimo/config.json).
10
+ *
11
+ * Only entry params are fetched: a locale param is resolved from the URL and
12
+ * never corresponds to a CMS schema, so it must not produce a request.
55
13
  */
56
14
  export async function resolveMetaEntries(url) {
57
15
  // @ts-ignore – import.meta.env is a Vite feature, replaced at build time for both client and SSR
@@ -59,24 +17,22 @@ export async function resolveMetaEntries(url) {
59
17
  if (!base) {
60
18
  return undefined;
61
19
  }
62
- const localeResolution = resolveFimoLocaleForUrl(url);
63
- const route = matchRoute(url);
64
- if (!route?.config.params) {
20
+ const match = matchFimoRoute(url, fimoConfig);
21
+ if (!match || match.contract.entryParams.length === 0) {
65
22
  return undefined;
66
23
  }
67
24
  const data = {};
68
- await Promise.all(Object.entries(route.config.params).map(async ([paramName, paramInfo]) => {
69
- const value = route.paramValues[paramName];
25
+ await Promise.all(match.contract.entryParams.map(async (param) => {
26
+ const value = match.entryParams[param.name];
70
27
  if (!value) {
71
28
  return;
72
29
  }
73
- const { schema, queryField } = paramInfo;
74
- const entryUrl = new URL(`${base}/entries/${schema}/field/${encodeURIComponent(queryField)}/${encodeURIComponent(value)}`);
75
- entryUrl.searchParams.set('locale', localeResolution.locale);
30
+ const entryUrl = new URL(`${base}/entries/${param.schema}/field/${encodeURIComponent(param.queryField)}/${encodeURIComponent(value)}`);
31
+ entryUrl.searchParams.set('locale', match.locale);
76
32
  const res = await fetch(entryUrl);
77
33
  if (res.ok) {
78
34
  const json = await res.json();
79
- data[paramName] = json.data;
35
+ data[param.name] = json.data;
80
36
  }
81
37
  }));
82
38
  return Object.keys(data).length > 0 ? data : undefined;
@@ -1,6 +1,6 @@
1
- import { resolveFimoLocale } from '../i18n/locale.js';
1
+ import { getFimoDefaultLocale } from '../i18n/locale.js';
2
2
  import { getSafePreviewNavigationPath, isPreviewRuntimeEnabled } from '../preview/bridge.js';
3
- import { getPreviewLocationChange, getPreviewParentOrigin } from '../preview/location.js';
3
+ import { getDocumentLocale, getPreviewLocationChange, getPreviewParentOrigin } from '../preview/location.js';
4
4
  const ASTRO_PAGE_LOAD_EVENT = 'astro:page-load';
5
5
  const ASTRO_AFTER_SWAP_EVENT = 'astro:after-swap';
6
6
  const CONNECTION_KEY = '__fimoAstroPreviewDisconnect__';
@@ -43,16 +43,14 @@ export function connectAstroPreview(options, browser = currentBrowser()) {
43
43
  const sendRoutes = () => {
44
44
  postToStudio({ type: 'nav/routes', payload: { routes: options.routes } });
45
45
  };
46
+ // Astro settles the locale server-side and renders it into `<html lang>`, so
47
+ // reading it back reports the locale the document is actually showing rather
48
+ // than one inferred from the URL. `astro:page-load` fires after a ClientRouter
49
+ // swap as well, which is what keeps this correct across in-place navigation.
46
50
  const sendLocation = () => {
47
- const location = getPreviewLocationChange(browser.window.location);
48
- const locale = resolveFimoLocale({ pathname: browser.window.location.pathname }, { i18n: options.i18n });
49
51
  postToStudio({
50
52
  type: 'nav/locationChange',
51
- payload: {
52
- ...(parentOrigin ? location : { path: location.path }),
53
- routePath: locale.routePath,
54
- locale: browser.document.documentElement.lang || locale.locale,
55
- },
53
+ payload: getPreviewLocationChange(browser.window.location, getDocumentLocale(browser.document, getFimoDefaultLocale({ i18n: options.i18n })), parentOrigin),
56
54
  });
57
55
  };
58
56
  const announceDocumentChange = () => {
@@ -203,6 +203,7 @@ function wrapField(field, value, source, resolve) {
203
203
  case 'string':
204
204
  case 'text':
205
205
  case 'date':
206
+ case 'datetime':
206
207
  return value != null ? new FimoString(String(value), source) : value;
207
208
  case 'richtext':
208
209
  return value != null ? new FimoRichText(value, source) : value;
@@ -16,6 +16,10 @@ export interface FimoLocaleRequestLike {
16
16
  export declare function getFimoDefaultLocale(config: Pick<FimoConfig, 'i18n'> | undefined): string;
17
17
  export declare function getFimoLocales(config: Pick<FimoConfig, 'i18n'> | undefined): string[];
18
18
  export declare function resolveFimoLocale(requestLike: string | URL | FimoLocaleRequestLike, config: Pick<FimoConfig, 'i18n'> | undefined): FimoLocaleResolution;
19
+ /**
20
+ * Rewrite a path so it renders in `locale`. The default locale is always
21
+ * unprefixed — `/en/pricing` is not a path Fimo produces or recognizes.
22
+ */
19
23
  export declare function localizeFimoPath(path: string, locale: string, config: Pick<FimoConfig, 'i18n'> | undefined): string;
20
24
  export declare function getFimoTextDirection(locale: string): 'ltr' | 'rtl';
21
25
  export declare function toHtmlLang(locale: string): string;
@@ -16,12 +16,14 @@ export function resolveFimoLocale(requestLike, config) {
16
16
  const pathname = normalizePathname(readPathname(requestLike));
17
17
  const defaultLocale = getFimoDefaultLocale(config);
18
18
  const locales = getFimoLocales(config);
19
- const routing = config?.i18n?.routing;
20
- const strategy = routing?.strategy ?? 'path-prefix';
21
- const prefixDefaultLocale = routing?.prefixDefaultLocale === true;
19
+ // New route-owned projects do not need this setting. Keep reading it for
20
+ // older apps whose provider or navigation still relies on global prefixes.
21
+ const strategy = config?.i18n?.routing?.strategy ?? 'path-prefix';
22
22
  if (strategy === 'path-prefix') {
23
23
  const [firstSegment, ...rest] = pathname.split('/').filter(Boolean);
24
- if (firstSegment && locales.includes(firstSegment)) {
24
+ // The default locale has no prefixed form, so a leading `/en` is an
25
+ // ordinary route segment rather than a locale marker.
26
+ if (firstSegment && firstSegment !== defaultLocale && locales.includes(firstSegment)) {
25
27
  const routePath = normalizePathname(`/${rest.join('/')}`);
26
28
  return {
27
29
  locale: firstSegment,
@@ -30,7 +32,7 @@ export function resolveFimoLocale(requestLike, config) {
30
32
  pathname,
31
33
  routePath,
32
34
  prefix: firstSegment,
33
- isDefaultLocale: firstSegment === defaultLocale,
35
+ isDefaultLocale: false,
34
36
  dir: getFimoTextDirection(firstSegment),
35
37
  };
36
38
  }
@@ -40,7 +42,7 @@ export function resolveFimoLocale(requestLike, config) {
40
42
  locales,
41
43
  pathname,
42
44
  routePath: pathname,
43
- prefix: prefixDefaultLocale ? defaultLocale : null,
45
+ prefix: null,
44
46
  isDefaultLocale: true,
45
47
  dir: getFimoTextDirection(defaultLocale),
46
48
  };
@@ -56,16 +58,14 @@ export function resolveFimoLocale(requestLike, config) {
56
58
  dir: getFimoTextDirection(defaultLocale),
57
59
  };
58
60
  }
61
+ /**
62
+ * Rewrite a path so it renders in `locale`. The default locale is always
63
+ * unprefixed — `/en/pricing` is not a path Fimo produces or recognizes.
64
+ */
59
65
  export function localizeFimoPath(path, locale, config) {
60
66
  const routePath = resolveFimoLocale(path, config).routePath;
61
- const defaultLocale = getFimoDefaultLocale(config);
62
- const routing = config?.i18n?.routing;
63
- const strategy = routing?.strategy ?? 'path-prefix';
64
- const prefixDefaultLocale = routing?.prefixDefaultLocale === true;
65
- if (strategy !== 'path-prefix') {
66
- return routePath;
67
- }
68
- if (locale === defaultLocale && !prefixDefaultLocale) {
67
+ const strategy = config?.i18n?.routing?.strategy ?? 'path-prefix';
68
+ if (strategy !== 'path-prefix' || locale === getFimoDefaultLocale(config)) {
69
69
  return routePath;
70
70
  }
71
71
  return normalizePathname(`/${locale}${routePath === '/' ? '' : routePath}`);
@@ -4,7 +4,11 @@ export const nextClientContentRuntime = {
4
4
  return requireContentApiUrl(process.env.NEXT_PUBLIC_FIMO_API_URL, 'NEXT_PUBLIC_FIMO_API_URL');
5
5
  },
6
6
  defaultLocale() {
7
- return process.env.NEXT_PUBLIC_FIMO_DEFAULT_LOCALE?.trim() || 'en';
7
+ // The application resolves locale from its own configuration and routes and
8
+ // passes it through `FimoProvider`. `undefined` leaves `locale` off the
9
+ // request so the API answers with the project default; an environment
10
+ // variable must never become the project's source of truth for it.
11
+ return undefined;
8
12
  },
9
13
  headers(extra) {
10
14
  const headers = { ...extra };
@@ -13,6 +13,7 @@ export type { CollectionContentCore, CollectionPage, CollectionPageInfo, Content
13
13
  export { FIMO_PARTS, FIMO_SOURCE, FimoBoolean, FimoDate, FimoMedia, FimoRichText, FimoString, getFimoParts, getFimoSource, hasFimoSource, isFimoDate, isFimoString, } from '../content/index.js';
14
14
  export { Boolean } from '../react/primitives/editable/Boolean.js';
15
15
  export { Date } from '../react/primitives/editable/Date.js';
16
+ export { DateTime } from '../react/primitives/editable/DateTime.js';
16
17
  export { Image } from '../react/primitives/editable/Image.js';
17
18
  export { Json } from '../react/primitives/editable/Json.js';
18
19
  export { RichText } from '../react/primitives/editable/RichText.js';
@@ -21,7 +22,7 @@ export { Text } from '../react/primitives/editable/Text.js';
21
22
  export { Video } from '../react/primitives/editable/Video.js';
22
23
  export type { BoolOps, DateOps, Field, Fields, FilterFor, FimoPart, FimoPolymorphicReference, FimoReference, FimoRichTextContent, FimoSource, NullOnlyOps, NumberOps, Populate, Populated, PopulatedResult, Projected, ProjectedWithPopulate, Query, ReferenceField, ReferenceMap, ScalarOps, Sort, SortField, StringOps, Where, WithReferences, } from '../content/index.js';
23
24
  export type { FimoRichTextComponents, FimoRichTextMarkProps, FimoRichTextNodeProps, } from '../react/primitives/contracts.js';
24
- export { FimoProvider, useLabels } from './provider.js';
25
- export type { FimoProviderProps } from './provider.js';
25
+ export { FimoPreviewBridge, FimoProvider, useLabels } from './provider.js';
26
+ export type { FimoPreviewBridgeProps, FimoProviderProps } from './provider.js';
26
27
  export type { Labels, LabelsSnapshot } from '../labels/core.js';
27
28
  //# sourceMappingURL=client.d.ts.map
@@ -15,10 +15,11 @@ export { createContentCore, createSingletonCore } from '../content/index.js';
15
15
  export { FIMO_PARTS, FIMO_SOURCE, FimoBoolean, FimoDate, FimoMedia, FimoRichText, FimoString, getFimoParts, getFimoSource, hasFimoSource, isFimoDate, isFimoString, } from '../content/index.js';
16
16
  export { Boolean } from '../react/primitives/editable/Boolean.js';
17
17
  export { Date } from '../react/primitives/editable/Date.js';
18
+ export { DateTime } from '../react/primitives/editable/DateTime.js';
18
19
  export { Image } from '../react/primitives/editable/Image.js';
19
20
  export { Json } from '../react/primitives/editable/Json.js';
20
21
  export { RichText } from '../react/primitives/editable/RichText.js';
21
22
  export { StaticImage } from '../react/primitives/renderers/StaticImage.js';
22
23
  export { Text } from '../react/primitives/editable/Text.js';
23
24
  export { Video } from '../react/primitives/editable/Video.js';
24
- export { FimoProvider, useLabels } from './provider.js';
25
+ export { FimoPreviewBridge, FimoProvider, useLabels } from './provider.js';
@@ -1,7 +1,7 @@
1
1
  import { type GetLabelsOptions } from '../labels/fetch.js';
2
2
  /**
3
- * Read the labels for an application-resolved locale. When omitted, locale
4
- * comes from the runtime's default-locale variable.
3
+ * Read the labels for an application-resolved locale. When omitted, the request
4
+ * carries no locale and the Fimo API answers with the project default locale.
5
5
  */
6
6
  export declare function getLabels(options?: GetLabelsOptions): Promise<import("./client.js").Labels>;
7
7
  //# sourceMappingURL=labels.d.ts.map
@@ -1,8 +1,8 @@
1
1
  import { fetchLabels } from '../labels/fetch.js';
2
2
  import { nextServerContentRuntime } from './server-runtime.js';
3
3
  /**
4
- * Read the labels for an application-resolved locale. When omitted, locale
5
- * comes from the runtime's default-locale variable.
4
+ * Read the labels for an application-resolved locale. When omitted, the request
5
+ * carries no locale and the Fimo API answers with the project default locale.
6
6
  */
7
7
  export function getLabels(options) {
8
8
  return fetchLabels(nextServerContentRuntime, options);
@@ -0,0 +1,13 @@
1
+ interface NextPreviewBrowser {
2
+ readonly window: Pick<Window, 'addEventListener' | 'location' | 'parent' | 'removeEventListener' | 'sessionStorage'>;
3
+ readonly document: Pick<Document, 'referrer'>;
4
+ }
5
+ type NextPreviewConnection = {
6
+ readonly locale: string;
7
+ readonly navigate: (to: string) => unknown;
8
+ readonly pathname: string;
9
+ readonly previewScriptUrl: string | undefined;
10
+ };
11
+ export declare function connectNextPreview({ locale, navigate, pathname, previewScriptUrl }: NextPreviewConnection, browser?: NextPreviewBrowser): () => void;
12
+ export {};
13
+ //# sourceMappingURL=preview.d.ts.map
@@ -0,0 +1,34 @@
1
+ import { getSafePreviewNavigationPath, isPreviewRuntimeEnabled } from '../preview/bridge.js';
2
+ import { getPreviewLocationChange, getPreviewParentOrigin } from '../preview/location.js';
3
+ export function connectNextPreview({ locale, navigate, pathname, previewScriptUrl }, browser = { window, document }) {
4
+ if (!isPreviewRuntimeEnabled(previewScriptUrl)) {
5
+ return () => undefined;
6
+ }
7
+ const parentOrigin = getPreviewParentOrigin(browser);
8
+ const sendLocationChange = () => {
9
+ browser.window.parent.postMessage({
10
+ type: 'nav/locationChange',
11
+ payload: getPreviewLocationChange({ pathname, search: browser.window.location.search, hash: browser.window.location.hash }, locale, parentOrigin),
12
+ }, parentOrigin ?? '*');
13
+ };
14
+ const handleMessage = (event) => {
15
+ if (event.source !== browser.window.parent || !parentOrigin || event.origin !== parentOrigin) {
16
+ return;
17
+ }
18
+ if (event.data?.type === 'nav/navigate') {
19
+ const target = getSafePreviewNavigationPath(event.data?.payload?.to, browser.window.location.origin);
20
+ if (target) {
21
+ navigate(target);
22
+ }
23
+ return;
24
+ }
25
+ if (event.data?.type === 'nav/read-routes') {
26
+ sendLocationChange();
27
+ }
28
+ };
29
+ browser.window.addEventListener('message', handleMessage);
30
+ sendLocationChange();
31
+ return () => {
32
+ browser.window.removeEventListener('message', handleMessage);
33
+ };
34
+ }
@@ -2,6 +2,8 @@ import { type ElementType } from 'react';
2
2
  import type { BooleanProps, DateProps, ImageProps, JsonProps, RichTextProps, VideoProps } from '../../react/primitives/contracts.js';
3
3
  export declare function Boolean<T extends ElementType = 'span'>({ value, as, children, ...props }: BooleanProps<T>): import("react/jsx-runtime").JSX.Element | null;
4
4
  export declare function Date({ value, ...props }: DateProps): import("react/jsx-runtime").JSX.Element | null;
5
+ /** Renders a `datetime` field: the `dateTime` attribute keeps the whole instant. */
6
+ export declare function DateTime({ value, ...props }: DateProps): import("react/jsx-runtime").JSX.Element | null;
5
7
  export declare function Image(props: ImageProps): import("react/jsx-runtime").JSX.Element | null;
6
8
  export declare function Json<T extends ElementType = 'span'>({ value, as, children, ...props }: JsonProps<T>): import("react/jsx-runtime").JSX.Element | null;
7
9
  export declare function RichText({ value, components, ...props }: RichTextProps): import("react/jsx-runtime").JSX.Element | null;
@@ -19,6 +19,14 @@ export function Date({ value, ...props }) {
19
19
  const source = typeof value === 'object' && FIMO_SOURCE in value ? (value[FIMO_SOURCE] ?? undefined) : undefined;
20
20
  return _jsx(DateRenderer, { ...props, value: value, source: source });
21
21
  }
22
+ /** Renders a `datetime` field: the `dateTime` attribute keeps the whole instant. */
23
+ export function DateTime({ value, ...props }) {
24
+ if (!value) {
25
+ return null;
26
+ }
27
+ const source = typeof value === 'object' && FIMO_SOURCE in value ? (value[FIMO_SOURCE] ?? undefined) : undefined;
28
+ return _jsx(DateRenderer, { ...props, value: value, source: source, precision: "instant" });
29
+ }
22
30
  export function Image(props) {
23
31
  if (!props.value) {
24
32
  return null;
@@ -8,6 +8,11 @@ export interface FimoProviderProps {
8
8
  queryClient?: QueryClient;
9
9
  }
10
10
  export declare function FimoProvider({ children, labels, locale, queryClient }: FimoProviderProps): import("react/jsx-runtime").JSX.Element;
11
+ export interface FimoPreviewBridgeProps {
12
+ readonly navigate: (to: string) => unknown;
13
+ readonly pathname: string;
14
+ }
15
+ export declare function FimoPreviewBridge({ navigate, pathname }: FimoPreviewBridgeProps): ReactNode;
11
16
  export declare function useNextContentLocale(requestedLocale: string | undefined, defaultLocale: string): string;
12
17
  export declare function useLabels(): Labels;
13
18
  //# sourceMappingURL=provider.d.ts.map
@@ -1,9 +1,16 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4
- import { createContext, useContext, useMemo, useState } from 'react';
4
+ import { createContext, useContext, useEffect, useMemo, useState } from 'react';
5
5
  import { createLabels } from '../labels/core.js';
6
- import { nextClientContentRuntime } from './client-runtime.js';
6
+ import { connectNextPreview } from './preview.js';
7
+ /**
8
+ * Last resort when an application renders `FimoProvider` without a locale and
9
+ * without labels. The project's default locale lives in
10
+ * `.fimo/config.json#i18n.defaultLocale`, which the application reads and passes
11
+ * in; no environment variable participates.
12
+ */
13
+ const UNRESOLVED_LOCALE = 'en';
7
14
  const FimoNextContext = createContext({});
8
15
  export function FimoProvider({ children, labels, locale, queryClient }) {
9
16
  const [client] = useState(() => queryClient ?? new QueryClient());
@@ -11,13 +18,20 @@ export function FimoProvider({ children, labels, locale, queryClient }) {
11
18
  const value = useMemo(() => ({ labels, locale: activeLocale }), [activeLocale, labels]);
12
19
  return (_jsx(FimoNextContext.Provider, { value: value, children: _jsx(QueryClientProvider, { client: client, children: children }) }));
13
20
  }
21
+ export function FimoPreviewBridge({ navigate, pathname }) {
22
+ const context = useContext(FimoNextContext);
23
+ const locale = context.locale ?? UNRESOLVED_LOCALE;
24
+ const previewScriptUrl = process.env.NEXT_PUBLIC_FIMO_PREVIEW_SCRIPT_URL;
25
+ useEffect(() => connectNextPreview({ locale, navigate, pathname, previewScriptUrl }), [locale, navigate, pathname, previewScriptUrl]);
26
+ return null;
27
+ }
14
28
  export function useNextContentLocale(requestedLocale, defaultLocale) {
15
29
  const context = useContext(FimoNextContext);
16
30
  return requestedLocale ?? context.locale ?? defaultLocale;
17
31
  }
18
32
  export function useLabels() {
19
33
  const context = useContext(FimoNextContext);
20
- const locale = context.locale ?? nextClientContentRuntime.defaultLocale() ?? 'en';
34
+ const locale = context.locale ?? UNRESOLVED_LOCALE;
21
35
  const snapshot = context.labels?.locale === locale
22
36
  ? context.labels
23
37
  : { locale, labels: {}, updatedAt: null };
@@ -4,7 +4,10 @@ export const nextServerContentRuntime = {
4
4
  return requireContentApiUrl(process.env.FIMO_CONTENT_API_URL, 'FIMO_CONTENT_API_URL');
5
5
  },
6
6
  defaultLocale() {
7
- return process.env.FIMO_DEFAULT_LOCALE?.trim() || undefined;
7
+ // `undefined` leaves `locale` off the request so the tenant API answers with
8
+ // the project default declared in `.fimo/config.json#i18n.defaultLocale`.
9
+ // Route-resolved locales reach Fimo through `createFimo`/`getLabels`.
10
+ return undefined;
8
11
  },
9
12
  headers(extra) {
10
13
  const headers = { ...extra };
@@ -1,7 +1,7 @@
1
1
  import type { CollectionContentCore, ContentClientConfig, SingletonContentCore } from '../content/client/core-types.js';
2
2
  export { StaticImage } from '../react/primitives/renderers/StaticImage.js';
3
3
  export { Text } from './primitives/Text.js';
4
- export { Boolean, Date, Image, Json, RichText, Video } from './primitives/server.js';
4
+ export { Boolean, Date, DateTime, Image, Json, RichText, Video } from './primitives/server.js';
5
5
  export { createFimo } from './facade.js';
6
6
  export type { CreateFimoOptions, NextContentRegistry } from './facade.js';
7
7
  export { getLabels } from './labels.js';
@@ -3,7 +3,7 @@ import { createFormClient } from '../forms/index.js';
3
3
  import { nextServerContentRuntime } from './server-runtime.js';
4
4
  export { StaticImage } from '../react/primitives/renderers/StaticImage.js';
5
5
  export { Text } from './primitives/Text.js';
6
- export { Boolean, Date, Image, Json, RichText, Video } from './primitives/server.js';
6
+ export { Boolean, Date, DateTime, Image, Json, RichText, Video } from './primitives/server.js';
7
7
  export { createFimo } from './facade.js';
8
8
  export { getLabels } from './labels.js';
9
9
  export function createContentClient(config) {
@@ -5,11 +5,12 @@ import type { FimoPath, GetFimoPathsOptions } from './types.js';
5
5
  * robots.txt, or any per-route logic.
6
6
  *
7
7
  * - Static paths in config: emitted as-is.
8
- * - Dynamic paths (with `:slug`-style params): expanded by fetching schema
9
- * entries from `VITE_API_URL`. One returned entry per dynamic expansion.
10
- * - With `i18n.routing.strategy: "path-prefix"`, paths are emitted for every
11
- * configured locale: default locale unprefixed by default, non-default
12
- * locales prefixed (`/es/...`). The app router still needs matching routes.
8
+ * - Dynamic paths (with `:slug`-style entry params): expanded by fetching
9
+ * schema entries from `VITE_API_URL`. One returned entry per expansion.
10
+ * - Locales come from the route contract, not from the project locale list: a
11
+ * route is emitted once per locale only when its path declares a `:locale`
12
+ * segment. The default locale is always unprefixed. A route without a
13
+ * `:locale` segment yields exactly one default-locale path.
13
14
  * - Pure-string return shape (`p.path`) is suitable for RR's `prerender`.
14
15
  *
15
16
  * If no API URL is set, dynamic routes are skipped with a console warning.