fimo 0.6.0 → 0.7.0-experimental.2

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 (145) hide show
  1. package/README.md +1 -1
  2. package/assets/content-templates/hooks-template.eta +73 -27
  3. package/assets/content-templates/types-template.eta +4 -1
  4. package/assets/skills/fimo/references/content.md +46 -42
  5. package/assets/skills/fimo-cli/references/content.md +14 -14
  6. package/assets/skills/fimo-cli/references/domains.md +1 -1
  7. package/assets/skills/fimo-studio/references/custom-domains.md +1 -1
  8. package/dist/cli/bundle.json +3 -2
  9. package/dist/cli/index.js +1203 -110450
  10. package/dist/runtime/content/client/createContentClient.d.ts +5 -0
  11. package/dist/runtime/content/client/createContentClient.js +300 -0
  12. package/dist/runtime/content/client/createSingletonClient.d.ts +5 -0
  13. package/dist/runtime/content/client/createSingletonClient.js +108 -0
  14. package/dist/runtime/content/client/internal.d.ts +44 -0
  15. package/dist/runtime/content/client/internal.js +236 -0
  16. package/dist/runtime/content/client/types.d.ts +136 -0
  17. package/dist/runtime/content/client/useQueryLocale.d.ts +2 -0
  18. package/dist/runtime/content/client/useQueryLocale.js +6 -0
  19. package/dist/runtime/content/utils/retrieve-data-sources.d.ts +1 -0
  20. package/dist/runtime/content/utils/retrieve-data-sources.js +27 -4
  21. package/dist/runtime/iframe/runtime/overlay-hmr.js +3 -0
  22. package/dist/runtime/index.d.ts +3 -0
  23. package/dist/runtime/index.js +2 -0
  24. package/dist/runtime/primitives/lib/primitives.d.ts +2 -2
  25. package/dist/runtime/primitives/lib/query.d.ts +5 -3
  26. package/package.json +31 -13
  27. package/release.json +2 -2
  28. package/templates/react-router/package.json +1 -1
  29. package/templates/react-router/src/pages/Index.tsx +1 -1
  30. package/dist/build/vite/index.d.ts.map +0 -1
  31. package/dist/build/vite/plugins/content-sync.d.ts.map +0 -1
  32. package/dist/build/vite/plugins/content-sync.test.d.ts +0 -2
  33. package/dist/build/vite/plugins/content-sync.test.d.ts.map +0 -1
  34. package/dist/build/vite/plugins/content-sync.test.js +0 -64
  35. package/dist/build/vite/plugins/data-id.d.ts.map +0 -1
  36. package/dist/build/vite/plugins/fimo-config.d.ts.map +0 -1
  37. package/dist/build/vite/plugins/fimo-config.test.d.ts +0 -2
  38. package/dist/build/vite/plugins/fimo-config.test.d.ts.map +0 -1
  39. package/dist/build/vite/plugins/fimo-config.test.js +0 -46
  40. package/dist/build/vite/plugins/overlay.d.ts.map +0 -1
  41. package/dist/build/vite/plugins/robots.d.ts.map +0 -1
  42. package/dist/build/vite/plugins/seo.d.ts.map +0 -1
  43. package/dist/build/vite/plugins/server-config.d.ts.map +0 -1
  44. package/dist/build/vite/plugins/sitemap.d.ts.map +0 -1
  45. package/dist/build/vite/plugins/translations.d.ts.map +0 -1
  46. package/dist/build/vite/plugins/translations.test.d.ts +0 -2
  47. package/dist/build/vite/plugins/translations.test.d.ts.map +0 -1
  48. package/dist/build/vite/plugins/translations.test.js +0 -266
  49. package/dist/runtime/app/FimoProviders.d.ts.map +0 -1
  50. package/dist/runtime/app/FimoScripts.d.ts.map +0 -1
  51. package/dist/runtime/app/index.d.ts.map +0 -1
  52. package/dist/runtime/app/prefetch.d.ts.map +0 -1
  53. package/dist/runtime/content/DataSourcesProvider.d.ts.map +0 -1
  54. package/dist/runtime/content/RefreshContentProvider.d.ts.map +0 -1
  55. package/dist/runtime/content/RefreshContentProvider.test.d.ts +0 -2
  56. package/dist/runtime/content/RefreshContentProvider.test.d.ts.map +0 -1
  57. package/dist/runtime/content/RefreshContentProvider.test.js +0 -33
  58. package/dist/runtime/content/parseCollectionResponse.d.ts.map +0 -1
  59. package/dist/runtime/content/parseCollectionResponse.test.d.ts +0 -2
  60. package/dist/runtime/content/parseCollectionResponse.test.d.ts.map +0 -1
  61. package/dist/runtime/content/parseCollectionResponse.test.js +0 -20
  62. package/dist/runtime/content/utils/retrieve-data-sources.d.ts.map +0 -1
  63. package/dist/runtime/core/AppErrorBoundary.d.ts.map +0 -1
  64. package/dist/runtime/core/ViteLifecycles.d.ts.map +0 -1
  65. package/dist/runtime/i18n/FimoLocaleContext.d.ts.map +0 -1
  66. package/dist/runtime/i18n/locale.d.ts.map +0 -1
  67. package/dist/runtime/i18n/locale.test.d.ts +0 -2
  68. package/dist/runtime/i18n/locale.test.d.ts.map +0 -1
  69. package/dist/runtime/i18n/locale.test.js +0 -58
  70. package/dist/runtime/iframe/ConsoleErrorProvider.d.ts.map +0 -1
  71. package/dist/runtime/iframe/ScreenshotProvider.d.ts.map +0 -1
  72. package/dist/runtime/iframe/runtime/overlay-hmr.d.ts.map +0 -1
  73. package/dist/runtime/iframe/runtime/overlay-runtime.d.ts.map +0 -1
  74. package/dist/runtime/index.d.ts.map +0 -1
  75. package/dist/runtime/paths/get-fimo-paths.d.ts.map +0 -1
  76. package/dist/runtime/paths/get-fimo-paths.test.d.ts +0 -2
  77. package/dist/runtime/paths/get-fimo-paths.test.d.ts.map +0 -1
  78. package/dist/runtime/paths/get-fimo-paths.test.js +0 -69
  79. package/dist/runtime/paths/index.d.ts.map +0 -1
  80. package/dist/runtime/paths/types.d.ts.map +0 -1
  81. package/dist/runtime/primitives/components/Boolean.d.ts.map +0 -1
  82. package/dist/runtime/primitives/components/Date.d.ts.map +0 -1
  83. package/dist/runtime/primitives/components/Image.d.ts.map +0 -1
  84. package/dist/runtime/primitives/components/RichText.d.ts.map +0 -1
  85. package/dist/runtime/primitives/components/StaticImage.d.ts.map +0 -1
  86. package/dist/runtime/primitives/components/Text.d.ts.map +0 -1
  87. package/dist/runtime/primitives/components/Video.d.ts.map +0 -1
  88. package/dist/runtime/primitives/components/index.d.ts.map +0 -1
  89. package/dist/runtime/primitives/index.d.ts.map +0 -1
  90. package/dist/runtime/primitives/lib/image-optimization.d.ts.map +0 -1
  91. package/dist/runtime/primitives/lib/index.d.ts.map +0 -1
  92. package/dist/runtime/primitives/lib/primitives.d.ts.map +0 -1
  93. package/dist/runtime/primitives/lib/query.d.ts.map +0 -1
  94. package/dist/runtime/primitives/lib/template.d.ts.map +0 -1
  95. package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts.map +0 -1
  96. package/dist/runtime/primitives/public-surface.test.d.ts +0 -2
  97. package/dist/runtime/primitives/public-surface.test.d.ts.map +0 -1
  98. package/dist/runtime/primitives/public-surface.test.js +0 -45
  99. package/dist/runtime/primitives/translations.d.ts.map +0 -1
  100. package/dist/runtime/react-router/index.d.ts.map +0 -1
  101. package/dist/runtime/react-router/navigation-utils.d.ts.map +0 -1
  102. package/dist/runtime/react-router/navigation.d.ts.map +0 -1
  103. package/dist/runtime/react-router/navigation.test.d.ts +0 -2
  104. package/dist/runtime/react-router/navigation.test.d.ts.map +0 -1
  105. package/dist/runtime/react-router/navigation.test.js +0 -35
  106. package/dist/runtime/react-router/routes.d.ts.map +0 -1
  107. package/dist/runtime/react-router/routes.test.d.ts +0 -2
  108. package/dist/runtime/react-router/routes.test.d.ts.map +0 -1
  109. package/dist/runtime/react-router/routes.test.js +0 -67
  110. package/dist/runtime/routing/AppResetScrollOnNavigate.d.ts.map +0 -1
  111. package/dist/runtime/routing/AppRoutesProvider.d.ts.map +0 -1
  112. package/dist/runtime/seo/buildSeoForPath.d.ts.map +0 -1
  113. package/dist/runtime/seo/htmlProps.d.ts.map +0 -1
  114. package/dist/runtime/seo/index.d.ts.map +0 -1
  115. package/dist/runtime/seo/resolver.d.ts.map +0 -1
  116. package/dist/runtime/shared/fimo-config.server.d.ts.map +0 -1
  117. package/dist/runtime/shared/fimo-config.types.d.ts.map +0 -1
  118. package/dist/runtime/t/OptionalTracking.d.ts.map +0 -1
  119. package/dist/runtime/t/OptionalTracking.test.d.ts +0 -2
  120. package/dist/runtime/t/OptionalTracking.test.d.ts.map +0 -1
  121. package/dist/runtime/t/OptionalTracking.test.js +0 -11
  122. package/dist/runtime/t/Pageview.d.ts.map +0 -1
  123. package/dist/runtime/templates.d.ts.map +0 -1
  124. package/dist/scripts/generated-types.test.js +0 -212
  125. package/dist/scripts/lint-translation-keys.test.d.ts +0 -1
  126. package/dist/scripts/lint-translation-keys.test.js +0 -16
  127. package/dist/scripts/sync-schemas.test.d.ts +0 -1
  128. package/dist/scripts/sync-schemas.test.js +0 -124
  129. package/dist/scripts/validate-route-metadata.test.d.ts +0 -1
  130. package/dist/scripts/validate-route-metadata.test.js +0 -116
  131. package/dist/scripts/validate-schemas.test.d.ts +0 -1
  132. package/dist/scripts/validate-schemas.test.js +0 -263
  133. package/scripts/add-ext.mjs +0 -47
  134. package/scripts/bump-version.mjs +0 -58
  135. package/scripts/bundle-cli.mjs +0 -123
  136. package/scripts/install-cli.mjs +0 -36
  137. package/scripts/lib/channels.mjs +0 -70
  138. package/scripts/lib/dev-release-state.mjs +0 -44
  139. package/scripts/lib/dev-release-state.test.ts +0 -36
  140. package/scripts/lib/postinstall-onboarding.test.ts +0 -82
  141. package/scripts/lib/release-core.mjs +0 -225
  142. package/scripts/lib/release-core.test.ts +0 -147
  143. package/scripts/migration-qa-gate.test.ts +0 -155
  144. package/scripts/publish-npm.mjs +0 -180
  145. /package/dist/{scripts/generated-types.test.d.ts → runtime/content/client/types.js} +0 -0
package/README.md CHANGED
@@ -85,4 +85,4 @@ fimo domains add example.com
85
85
 
86
86
  ## Documentation
87
87
 
88
- Full documentation lives at **[docs.fimo.ai](https://docs.fimo.ai)**. Run `fimo --help` to see every command.
88
+ Full documentation lives at **[fimo.ai/docs](https://fimo.ai/docs)**. Run `fimo --help` to see every command.
@@ -11,7 +11,7 @@
11
11
  // AUTO-GENERATED ⚠️ – do not edit by hand
12
12
  import { <% if (!model.isSingleton) { %>useInfiniteQuery, <% } %>useSuspenseQuery, useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
13
13
  <% if (!model.isSingleton) { -%>
14
- import type { InfiniteData, UseInfiniteQueryResult, UseQueryResult } from "@tanstack/react-query";
14
+ import type { InfiniteData, UseInfiniteQueryResult, UseQueryResult, UseSuspenseQueryResult } from "@tanstack/react-query";
15
15
  <% } -%>
16
16
  import { FimoString, FimoMedia, FimoBoolean, FimoRichText, useLocale<% if (!model.isSingleton) { %>, parseCollectionResponse<% } %> } from "fimo/ui";
17
17
  // @ts-ignore - virtual module provided by Fimo's Vite plugin
@@ -37,15 +37,21 @@ export type { <%= model.pascal %>, <%= model.pascal %>Fields, <%= model.pascal %
37
37
  */
38
38
  export function wrapWithSource(entry: Record<string, any>): <%= model.pascal %> {
39
39
  const id = entry.id;
40
- const sourcePrefix = `<%= model.pascal %>.${id}`;
40
+ const entryId = entry.entryId;
41
+ const documentId = entry.documentId ?? id;
42
+ const locale = entry.locale ?? defaultLocale();
43
+ const sourcePrefix = `<%= model.pascal %>.id:${id}:${encodeURIComponent(locale)}`;
41
44
  const entryFimo = entry.__fimo && typeof entry.__fimo === "object" ? entry.__fimo : {};
42
45
 
43
46
  return {
44
47
  id,
45
- documentId: entry.documentId,
48
+ entryId,
49
+ documentId,
46
50
  slug: entry.slug != null ? new FimoString(entry.slug, `${sourcePrefix}.slug`) : entry.slug,
47
- <% if (model.localized) { -%>
48
- locale: entry.locale,
51
+ <% if (!model.isSingleton || model.localized) { -%>
52
+ locale,
53
+ <% } else { -%>
54
+ ...(entry.locale ? { locale } : {}),
49
55
  <% } -%>
50
56
  createdAt: entry.createdAt,
51
57
  updatedAt: entry.updatedAt,
@@ -101,11 +107,11 @@ if (model.fields) {
101
107
  const wrapper = field.target === model.uid ? 'wrapWithSource' : `wrap${field.targetPascal}WithSource`;
102
108
  if (field.many) {
103
109
  -%>
104
- <%= key %>: Array.isArray(entry.<%= key %>) ? entry.<%= key %>.map((value: any) => value != null && typeof value === "object" && typeof value.id === "string" ? <%= wrapper %>(value) : value) : entry.<%= key %>,
110
+ <%= key %>: Array.isArray(entry.<%= key %>) ? entry.<%= key %>.map((value: any) => isPopulatedEntry(value) ? <%= wrapper %>(value) : value) : entry.<%= key %>,
105
111
  <%
106
112
  } else {
107
113
  -%>
108
- <%= key %>: entry.<%= key %> != null && typeof entry.<%= key %> === "object" && typeof entry.<%= key %>.id === "string" ? <%= wrapper %>(entry.<%= key %>) : entry.<%= key %>,
114
+ <%= key %>: isPopulatedEntry(entry.<%= key %>) ? <%= wrapper %>(entry.<%= key %>) : entry.<%= key %>,
109
115
  <%
110
116
  }
111
117
  break;
@@ -121,6 +127,23 @@ if (model.fields) {
121
127
  } as <%= model.pascal %>;
122
128
  }
123
129
 
130
+ interface PopulatedEntryRecord extends Record<string, unknown> {
131
+ id: string;
132
+ __fimo: { contentType: string };
133
+ }
134
+
135
+ function isPopulatedEntry(value: unknown): value is PopulatedEntryRecord {
136
+ if (value == null || typeof value !== "object") {
137
+ return false;
138
+ }
139
+ const entry = value as Record<string, unknown>;
140
+ const fimo = entry.__fimo;
141
+ return typeof entry.id === "string"
142
+ && fimo != null
143
+ && typeof fimo === "object"
144
+ && typeof (fimo as Record<string, unknown>).contentType === "string";
145
+ }
146
+
124
147
  <%
125
148
  const hasPolymorphicReferences = model.fields && Object.values(model.fields).some((field) => field.type === 'reference' && Array.isArray(field.targetRefs) && field.targetRefs.length > 0);
126
149
  if (hasPolymorphicReferences) {
@@ -132,7 +155,9 @@ if (hasPolymorphicReferences) {
132
155
  * (unpopulated) values pass through untouched.
133
156
  */
134
157
  function wrapPolymorphic(value: any, wrappers: Record<string, (entry: Record<string, any>) => any>): any {
135
- if (value == null || typeof value !== "object" || typeof value.id !== "string") return value;
158
+ if (!isPopulatedEntry(value)) {
159
+ return value;
160
+ }
136
161
  const contentType = value.__fimo && typeof value.__fimo.contentType === "string" ? value.__fimo.contentType : null;
137
162
  const wrapper = contentType ? wrappers[contentType] : undefined;
138
163
  if (!wrapper) return value;
@@ -159,6 +184,11 @@ function useQueryLocale(locale?: string): string {
159
184
  return locale ?? activeLocale ?? defaultLocale();
160
185
  }
161
186
 
187
+ function withMutationLocale<T extends object>(payload: T, activeLocale: string): T & { locale: string } {
188
+ const locale = (payload as { locale?: string }).locale;
189
+ return { ...payload, locale: locale ?? activeLocale };
190
+ }
191
+
162
192
  const LOCALIZED_CONTENT = <%= model.localized ? 'true' : 'false' %>;
163
193
  const LOCALIZED_REFERENCE_FIELDS = new Set<string>(<%= JSON.stringify(model.localizedReferenceFields) %>);
164
194
 
@@ -172,8 +202,12 @@ function readLocale(
172
202
  ownerUsesLocale: boolean,
173
203
  fallbackLocale = defaultLocale()
174
204
  ): string | undefined {
205
+ <% if (!model.isSingleton) { -%>
206
+ return options.locale ?? fallbackLocale;
207
+ <% } else { -%>
175
208
  const populatesLocalizedTarget = populateFields(options.populate).some((field) => LOCALIZED_REFERENCE_FIELDS.has(field));
176
209
  return ownerUsesLocale || populatesLocalizedTarget ? options.locale ?? fallbackLocale : undefined;
210
+ <% } -%>
177
211
  }
178
212
 
179
213
  function fimoHeaders(extra?: Record<string, string>): Record<string, string> {
@@ -202,6 +236,7 @@ function withReadOptions<TPopulate extends <%= model.pascal %>Populate | undefin
202
236
  function splitWritePayload(payload: Partial<<%= model.pascal %>Input>): Record<string, unknown> {
203
237
  const {
204
238
  id,
239
+ entryId,
205
240
  documentId,
206
241
  locale,
207
242
  createdAt,
@@ -253,7 +288,7 @@ export async function getById<TPopulate extends <%= model.pascal %>Populate | un
253
288
  id: string,
254
289
  options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}
255
290
  ): Promise<<%= model.pascal %>Result<undefined, TPopulate>> {
256
- const res = await fetch(`${base}${withReadOptions(`/entries/<%= model.uid %>/${id}`, options, false)}`, { headers: fimoHeaders() });
291
+ const res = await fetch(`${base}${withReadOptions(`/entries/<%= model.uid %>/${id}`, options)}`, { headers: fimoHeaders() });
257
292
  if (!res.ok) throw new Error(await res.text());
258
293
  const data = await res.json();
259
294
  return wrapWithSource(data.data) as <%= model.pascal %>Result<undefined, TPopulate>;
@@ -354,18 +389,21 @@ export async function create(payload: <%= model.pascal %>Input): Promise<<%= mod
354
389
  }
355
390
 
356
391
  export async function update(id: string, payload: Partial<<%= model.pascal %>Input>): Promise<<%= model.pascal %>> {
357
- const res = await fetch(`${base}/entries/<%= model.uid %>/${id}`, {
392
+ const write = splitWritePayload(payload) as { data: Record<string, unknown>; locale?: string };
393
+ const url = withReadOptions(`/entries/<%= model.uid %>/${id}`, { locale: write.locale });
394
+ const res = await fetch(`${base}${url}`, {
358
395
  method: "PUT",
359
396
  headers: fimoHeaders({ "Content-Type": "application/json" }),
360
- body: JSON.stringify(splitWritePayload(payload))
397
+ body: JSON.stringify({ data: write.data })
361
398
  });
362
399
  if (!res.ok) throw new Error(await res.text());
363
400
  const data = await res.json();
364
401
  return wrapWithSource(data.data);
365
402
  }
366
403
 
367
- export async function remove(id: string): Promise<void> {
368
- const res = await fetch(`${base}/entries/<%= model.uid %>/${id}`, { method: "DELETE", headers: fimoHeaders() });
404
+ export async function remove(id: string, options: { locale?: string } = {}): Promise<void> {
405
+ const url = withReadOptions(`/entries/<%= model.uid %>/${id}`, options);
406
+ const res = await fetch(`${base}${url}`, { method: "DELETE", headers: fimoHeaders() });
369
407
  if (!res.ok) throw new Error(await res.text());
370
408
  }
371
409
  <% } else { -%>
@@ -399,7 +437,7 @@ export async function update(payload: Partial<<%= model.pascal %>Input>): Promis
399
437
  <% if (!model.isSingleton) { -%>
400
438
  const qk = {
401
439
  root : ["<%= model.pascal %>"] as const,
402
- byId : <TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(id: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) => ["<%= model.pascal %>", id, options.locale, options.populate] as const,
440
+ byId : <TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(id: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) => ["<%= model.pascal %>", "id", id, options.locale, options.populate] as const,
403
441
  bySlug : <TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(slug: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) => ["<%= model.pascal %>", "slug", slug, options.locale, options.populate] as const,
404
442
  byField : <TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(field: string, value: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) => ["<%= model.pascal %>", field, value, options.locale, options.populate] as const,
405
443
  list : (params: <%= model.pascal %>Query) => ["<%= model.pascal %>", params] as const,
@@ -409,8 +447,8 @@ const qk = {
409
447
  >(params: InfiniteListQuery<TFields, TPopulate>) => ["<%= model.pascal %>", "infinite", params] as const
410
448
  };
411
449
 
412
- export function useGetById<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(id: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
413
- const locale = readLocale(options, false, useQueryLocale(options.locale));
450
+ export function useGetById<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(id: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}): UseQueryResult<<%= model.pascal %>Result<undefined, TPopulate>, Error> {
451
+ const locale = useQueryLocale(options.locale);
414
452
  const localizedOptions = { ...options, locale };
415
453
  return useQuery<<%= model.pascal %>Result<undefined, TPopulate>, Error>({ queryKey: qk.byId(id, localizedOptions), queryFn: () => getById(id, localizedOptions) });
416
454
  }
@@ -475,8 +513,8 @@ export function useInfiniteList<
475
513
  };
476
514
  }
477
515
 
478
- export function useSuspenseGetById<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(id: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}) {
479
- const locale = readLocale(options, false, useQueryLocale(options.locale));
516
+ export function useSuspenseGetById<TPopulate extends <%= model.pascal %>Populate | undefined = undefined>(id: string, options: { locale?: string; populate?: TPopulate; localeFallbacks?: boolean } = {}): UseSuspenseQueryResult<<%= model.pascal %>Result<undefined, TPopulate>, Error> {
517
+ const locale = useQueryLocale(options.locale);
480
518
  const localizedOptions = { ...options, locale };
481
519
  return useSuspenseQuery<<%= model.pascal %>Result<undefined, TPopulate>, Error>({ queryKey: qk.byId(id, localizedOptions), queryFn: () => getById(id, localizedOptions) });
482
520
  }
@@ -508,30 +546,37 @@ export function useSuspenseGet<
508
546
 
509
547
  export function useCreate() {
510
548
  const qc = useQueryClient();
549
+ const activeLocale = useQueryLocale();
511
550
  return useMutation<<%= model.pascal %>, Error, <%= model.pascal %>Input>({
512
- mutationFn : create,
551
+ mutationFn : (payload) => create(withMutationLocale(payload, activeLocale)),
513
552
  onSuccess : () => qc.invalidateQueries({ queryKey: qk.root })
514
553
  });
515
554
  }
516
555
 
517
556
  export function useUpdate() {
518
557
  const qc = useQueryClient();
519
- return useMutation<<%= model.pascal %>, Error, { id: string } & Partial<<%= model.pascal %>Input>>({
520
- mutationFn : ({ id, ...patch }) => update(id, patch),
558
+ const activeLocale = useQueryLocale();
559
+ return useMutation<<%= model.pascal %>, Error, { id: string; locale?: string } & Partial<<%= model.pascal %>Input>>({
560
+ mutationFn : ({ id, ...patch }) => update(id, withMutationLocale(patch, activeLocale)),
521
561
  onSuccess : (_data, vars) => {
522
562
  qc.invalidateQueries({ queryKey: qk.root });
523
- qc.invalidateQueries({ queryKey: qk.byId(vars.id) });
563
+ qc.invalidateQueries({ queryKey: qk.byId(vars.id, { locale: vars.locale ?? activeLocale }) });
524
564
  }
525
565
  });
526
566
  }
527
567
 
528
568
  export function useRemove() {
529
569
  const qc = useQueryClient();
530
- return useMutation<void, Error, string>({
531
- mutationFn : remove,
532
- onSuccess : (_data, id) => {
570
+ const activeLocale = useQueryLocale();
571
+ return useMutation<void, Error, string | { id: string; locale?: string }>({
572
+ mutationFn : (value) => {
573
+ const { id, locale } = typeof value === "string" ? { id: value, locale: activeLocale } : { ...value, locale: value.locale ?? activeLocale };
574
+ return remove(id, { locale });
575
+ },
576
+ onSuccess : (_data, value) => {
577
+ const { id, locale } = typeof value === "string" ? { id: value, locale: activeLocale } : { ...value, locale: value.locale ?? activeLocale };
533
578
  qc.invalidateQueries({ queryKey: qk.root });
534
- qc.removeQueries({ queryKey: ["<%= model.pascal %>", id], exact: false });
579
+ qc.removeQueries({ queryKey: qk.byId(id, { locale }), exact: false });
535
580
  }
536
581
  });
537
582
  }
@@ -555,8 +600,9 @@ export function useSuspenseGet<TPopulate extends <%= model.pascal %>Populate | u
555
600
 
556
601
  export function useUpdate() {
557
602
  const qc = useQueryClient();
603
+ const activeLocale = useQueryLocale();
558
604
  return useMutation<<%= model.pascal %>, Error, Partial<<%= model.pascal %>Input>>({
559
- mutationFn: update,
605
+ mutationFn: (payload) => update(withMutationLocale(payload, activeLocale)),
560
606
  onSuccess: () => qc.invalidateQueries({ queryKey: qk.root })
561
607
  });
562
608
  }
@@ -33,10 +33,13 @@ export interface <%= model.pascal %>FimoMeta {
33
33
 
34
34
  export interface <%= model.pascal %> extends WithReferences<<%= model.pascal %>References> {
35
35
  id: string;
36
+ entryId: string;
36
37
  documentId: string;
37
38
  slug: FimoString;
38
- <% if (model.localized) { -%>
39
+ <% if (!model.isSingleton || model.localized) { -%>
39
40
  locale: string;
41
+ <% } else { -%>
42
+ locale?: string;
40
43
  <% } -%>
41
44
  createdAt: string;
42
45
  updatedAt: string;
@@ -34,9 +34,9 @@ Every entry already carries this metadata automatically. Declaring any of them a
34
34
  schema field is rejected by `fimo schemas push` with a 400 (e.g. `fields.slug: "slug" is
35
35
  reserved entry metadata and cannot be used as a content field.`):
36
36
 
37
- `id`, `documentId`, `slug`, `locale`, `createdAt`, `updatedAt`, `__fimo`.
37
+ `id`, `entryId`, `documentId`, `slug`, `locale`, `createdAt`, `updatedAt`, `__fimo`.
38
38
 
39
- Localized entries expose `locale`; non-localized entries omit it. It remains reserved in both schemas.
39
+ Collection entries expose the effective `locale`. It remains reserved for both localized and shared schemas.
40
40
 
41
41
  In particular **do not add a `slug` field** — every entry gets a `slug` for free. You still
42
42
  pass `slug` inside `data` when _creating an entry_ (see "Entry body shape" below); it just
@@ -45,16 +45,16 @@ something else (e.g. `handle`, `permalink`).
45
45
 
46
46
  ## Field types
47
47
 
48
- | Type | `data` JSON shape | Use for |
49
- | ----------- | ------------------------------------------------------------------------------------- | --------------------------------- |
50
- | `string` | `"..."` | Titles, short text |
51
- | `richtext` | Tiptap JSONContent (see below) | Blog body, descriptions, articles |
52
- | `number` | `42` | Counts, quantities |
53
- | `boolean` | `true` / `false` | Flags |
54
- | `date` | ISO date string, e.g. `"2026-06-03"` | Publish dates, timestamps |
55
- | `media` | `{ "source": "local"\|"external", "url": "https://...", "alt": "..." }` | Images, video |
56
- | `json` | any JSON | Structured metadata |
57
- | `reference` | `{ "documentId": "..." }`, `null`, or an array of reference objects when `many: true` | References to another schema |
48
+ | Type | `data` JSON shape | Use for |
49
+ | ----------- | ----------------------------------------------------------------------------- | --------------------------------- |
50
+ | `string` | `"..."` | Titles, short text |
51
+ | `richtext` | Tiptap JSONContent (see below) | Blog body, descriptions, articles |
52
+ | `number` | `42` | Counts, quantities |
53
+ | `boolean` | `true` / `false` | Flags |
54
+ | `date` | ISO date string, e.g. `"2026-06-03"` | Publish dates, timestamps |
55
+ | `media` | `{ "source": "local"\|"external", "url": "https://...", "alt": "..." }` | Images, video |
56
+ | `json` | any JSON | Structured metadata |
57
+ | `reference` | `{ "id": "..." }`, `null`, or an array of reference objects when `many: true` | References to another schema |
58
58
 
59
59
  A `date` field currently generates as `any` in the TS client, so for typed access (and the `<Date>` component) prefer a `string` field holding an ISO date — which is what the `publishDate` examples below do.
60
60
 
@@ -79,7 +79,7 @@ Rules:
79
79
  - `target` is required and must be the PascalCase schema `uid` of the referenced content type.
80
80
  - Omit `many` for a to-one reference. Use `"many": true` for a to-many reference.
81
81
  - Self-references are allowed, e.g. a `Category` schema can have `parent: { "type": "reference", "target": "Category" }`.
82
- - Entry values store reference objects with the target `documentId`, not row `id` values: `{ "documentId": "..." }`.
82
+ - Entry values use the target's stable `id`, never the variant `entryId`: `{ "id": "..." }`.
83
83
  - Do not include `contentType` for normal fixed-target references; the schema's `target` already defines it.
84
84
  - Do not add `localized` or field-level `i18n` to a reference. The reference value belongs to the entry variant that stores it.
85
85
  - Use `null` for an empty to-one reference and `[]` for an empty to-many reference.
@@ -92,22 +92,22 @@ Entry payload example:
92
92
  "data": {
93
93
  "slug": "reference-example",
94
94
  "title": "Reference example",
95
- "author": { "documentId": "750e8400-e29b-41d4-a716-446655440000" },
96
- "contributors": [{ "documentId": "750e8400-e29b-41d4-a716-446655440000" }]
95
+ "author": { "id": "750e8400-e29b-41d4-a716-446655440000" },
96
+ "contributors": [{ "id": "750e8400-e29b-41d4-a716-446655440000" }]
97
97
  }
98
98
  }
99
99
  ```
100
100
 
101
101
  ## Schema transitions
102
102
 
103
- After editing schema JSON, run `fimo schemas push {Uid}` and then `fimo validate`. Push regenerates the schema client and applies the schema change to stored content automatically:
103
+ After editing schema JSON, run `fimo schemas push {Uid}` and then `fimo validate`. Push regenerates the schema client and applies the schema change to existing content automatically:
104
104
 
105
105
  - Adding fields and changing labels preserve entries.
106
106
  - Removing a field removes only that field's values.
107
107
  - A one-to-many reference change wraps each existing reference in an array.
108
108
  - A many-to-one change, reference target change, scalar type change, or scalar/reference change clears only the changed field.
109
109
  - Collection-to-singleton removes that type's entries and cleans references to those documents.
110
- - Singleton-to-collection preserves the existing row.
110
+ - Singleton-to-collection preserves the existing entry.
111
111
 
112
112
  There are no reset or data-loss flags. Do not invent one or promise that incompatible field values survive; the schema change itself selects the behavior above.
113
113
 
@@ -192,9 +192,10 @@ Import from the public schema module, e.g. `import BlogPost from '@/schemas/Blog
192
192
  ```ts
193
193
  interface BlogPost {
194
194
  id: string;
195
+ entryId: string;
195
196
  documentId: string;
196
197
  slug: FimoString;
197
- locale: string; // localized schemas only; omitted when i18n.enabled is false
198
+ locale: string;
198
199
  createdAt: string;
199
200
  updatedAt: string;
200
201
  __fimo: {
@@ -213,13 +214,14 @@ Content fields live at the top level. Fimo-owned metadata that is not part of th
213
214
 
214
215
  ## Localized and shared content
215
216
 
216
- Localized content is stored per locale. Generated hooks read the active Fimo locale automatically.
217
+ Localized content has one variant per locale. Generated hooks read the active Fimo locale automatically.
217
218
 
218
219
  When a project has multiple configured locales, create and update each localized content variant explicitly. Use the default locale first, then create non-default variants with the same `documentId` and the target `locale` through the content tools or `fimo entries` commands.
219
220
 
220
221
  For shared content, set `i18n: { "enabled": false }` on the schema and create each entry once. Shared entries:
221
222
 
222
- - have no public `locale` property;
223
+ - send and expose the effective active/default locale in generated collection queries and models;
224
+ - omit `locale` from raw API responses because there is no localized variant;
223
225
  - are returned in every locale context without duplication;
224
226
  - ignore locale on owner reads and writes;
225
227
  - still use the active/requested locale when a populated reference targets a localized schema.
@@ -228,7 +230,7 @@ Do not duplicate shared entries across locales or add field-level localization s
228
230
 
229
231
  A populated reference whose target has no variant in the requested locale resolves to null. Reads accept an opt-in `localeFallbacks: true` that resolves those to the best available variant instead (project default first), marked by its `locale` field — never implement fallbacks by hand; public reads stay strict unless the option is passed.
230
232
 
231
- **Polymorphic references:** a field may declare `targets: ["Post", "Video"]` instead of a single `target` (never both) when one slot genuinely holds different types — mixed feeds, hero blocks, curated collections. Prefer a fixed `target` whenever every value is the same type: fixed fields keep `where` filtering and simpler generated types. Polymorphic values store `{ contentType, documentId }`; populated values in generated hooks carry a top-level `$type` that narrows the union:
233
+ **Polymorphic references:** a field may declare `targets: ["Post", "Video"]` instead of a single `target` (never both) when one slot genuinely holds different types — mixed feeds, hero blocks, curated collections. Prefer a fixed `target` whenever every value is the same type: fixed fields keep `where` filtering and simpler generated types. Polymorphic values use `{ contentType, id }`; populated values in generated hooks carry a top-level `$type` that narrows the union:
232
234
 
233
235
  ```ts
234
236
  for (const item of page.featured) {
@@ -245,19 +247,21 @@ Filtering on polymorphic fields is not supported yet.
245
247
 
246
248
  **Generated API:**
247
249
 
250
+ Collection responses use `id` as the stable identity shared by all locale variants. `locale` selects the variant, and generated hooks use the active Fimo locale when it is omitted. `entryId` identifies the exact variant returned by Fimo and is only for diagnostics or migrations. `documentId` remains a compatibility alias for `id`; reference inputs use `{ id: entry.id }`. Filters and sorts follow the same rule: `where.id` and `sort: "id"` use stable identity, while `entryId` addresses an exact variant.
251
+
248
252
  ```ts
249
253
  // low-level fetch helpers
250
- getById<TPopulate extends BlogPostPopulate | undefined>(id: string, options?: { populate?: TPopulate }): Promise<BlogPostResult<undefined, TPopulate>>
254
+ getById<TPopulate extends BlogPostPopulate | undefined>(id: string, options?: { locale?: string; populate?: TPopulate }): Promise<BlogPostResult<undefined, TPopulate>>
251
255
  getBySlug<TPopulate extends BlogPostPopulate | undefined>(slug: string, options?: { populate?: TPopulate }): Promise<BlogPostResult<undefined, TPopulate> | null>
252
256
  getByField<TPopulate extends BlogPostPopulate | undefined>(fieldName: string, value: string, options?: { populate?: TPopulate }): Promise<BlogPostResult<undefined, TPopulate> | null>
253
257
  list<TFields extends BlogPostFields | undefined, TPopulate extends BlogPostPopulate | undefined>(params?: BlogPostQuery<TFields, TPopulate>): Promise<CollectionPage<BlogPostResult<TFields, TPopulate>>>
254
258
  get<TFields extends BlogPostFields | undefined, TPopulate extends BlogPostPopulate | undefined>(params?: BlogPostQuery<TFields, TPopulate>): Promise<BlogPostResult<TFields, TPopulate>[]>
255
259
  create(payload: BlogPostInput): Promise<BlogPost>
256
260
  update(id: string, payload: Partial<BlogPostInput>): Promise<BlogPost>
257
- remove(id: string): Promise<void>
261
+ remove(id: string, options?: { locale?: string }): Promise<void>
258
262
 
259
263
  // react-query hooks
260
- useGetById(id, { populate? }) // → { data: BlogPostResult<undefined, TPopulate> | undefined, isLoading, ... }
264
+ useGetById(id, { locale?, populate? }) // → { data: BlogPostResult<undefined, TPopulate> | undefined, isLoading, ... }
261
265
  useGetBySlug(slug, { populate? }) // → { data: BlogPostResult<undefined, TPopulate> | null | undefined, ... } — ideal for /:slug detail routes
262
266
  useGetByField(fieldName, value, { populate? })
263
267
  useList<TFields, TPopulate>(params?) // → { data: CollectionPage<BlogPostResult<TFields, TPopulate>> | undefined, ... }
@@ -279,7 +283,7 @@ update(payload: Partial<SettingsInput>): Promise<Settings>
279
283
  useGet(options?) / useSuspenseGet(options?) / useUpdate()
280
284
  ```
281
285
 
282
- Singleton clients do not expose `create`, `remove`, `getById`, or list queries. Localized singletons have one variant per locale; non-localized singletons have one locale-free row.
286
+ Singleton clients do not expose `create`, `remove`, `getById`, or list queries. Localized singletons have one variant per locale; shared singletons have one entry across locales.
283
287
 
284
288
  Choose the collection interface from the UI behavior. Use `useList` when the UI controls a page, and read pagination state from `pageInfo`:
285
289
 
@@ -335,10 +339,10 @@ BlogPost.useGet({
335
339
  ```
336
340
 
337
341
  Operators: scalar equality, `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$contains`, `$startsWith`, `$null`.
338
- Reference field values are reference objects, but reference filters use the target `documentId` string, for example `where: { author: "750e..." }`; for to-many references that means "contains this target document".
342
+ Reference field values are reference objects, but reference filters use the target `id` string, for example `where: { author: author.id }`; for to-many references that means "contains this target".
339
343
  For to-many references, `$eq` and `$contains` mean membership, `$in` means any listed document is present, `$ne` and `$nin` mean none are present, and `$null: true` means empty or missing. Reference fields cannot be sorted.
340
344
 
341
- Use `fields` to keep list payloads small. The API always keeps `id`, and returns only the requested fields beyond that:
345
+ Use `fields` to keep list payloads small. The API always keeps `id`, `entryId`, `documentId`, and `locale`, and returns only the requested fields beyond that:
342
346
 
343
347
  ```tsx
344
348
  const { data: summaries = [] } = BlogPost.useGet({
@@ -346,7 +350,7 @@ const { data: summaries = [] } = BlogPost.useGet({
346
350
  sort: '-publishedAt',
347
351
  limit: 12,
348
352
  });
349
- // summaries: Array<Pick<BlogPost, 'id' | 'slug' | 'title' | 'excerpt' | 'coverImage'>>
353
+ // summaries: Array<Pick<BlogPost, 'id' | 'entryId' | 'documentId' | 'locale' | 'slug' | 'title' | 'excerpt' | 'coverImage'>>
350
354
  ```
351
355
 
352
356
  Use `populate` to embed reference targets in read results:
@@ -356,7 +360,7 @@ const { data: posts = [] } = BlogPost.useGet({
356
360
  fields: ['slug', 'title', 'author'] as const,
357
361
  populate: ['author'] as const,
358
362
  });
359
- // Without populate: post.author is { documentId: "..." }.
363
+ // Without populate: post.author is { id: "..." }.
360
364
  // With populate: post.author is the target Author entry object, or null.
361
365
  ```
362
366
 
@@ -384,16 +388,16 @@ Populate is depth-one. It only works for reference fields; trying to populate a
384
388
 
385
389
  The entry objects a hook returns are **not** plain JS values. The generated client wraps each _trackable_ field in a `fimo/ui` primitive that carries source metadata for inline editing. Which fields wrap, and which stay plain:
386
390
 
387
- | Schema field type | Field value you get | Notes |
388
- | ----------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------- |
389
- | `string` | `FimoString` | `extends String` — **not** a primitive `string` |
390
- | `richtext` | `FimoRichText` | Tiptap JSONContent + source metadata |
391
- | `boolean` | `FimoBoolean` | class; `.valueOf()` → `boolean` |
392
- | `media` | `FimoMedia` / `FimoMedia[]` | shape + rendering in `references/assets.md` |
393
- | `number` | `number` | plain — not wrapped |
394
- | `json` | `Record<string, any>` | plain — not wrapped |
395
- | `date` | `any` (raw ISO date string) | not wrapped (`FimoDate` is not emitted — see below) |
396
- | `reference` | `FimoReference` / `FimoReference[]` | reference object(s) with target `documentId`; use `populate` when embedded target rows are needed |
391
+ | Schema field type | Field value you get | Notes |
392
+ | ----------------- | ----------------------------------- | ------------------------------------------------------------------------------------- |
393
+ | `string` | `FimoString` | `extends String` — **not** a primitive `string` |
394
+ | `richtext` | `FimoRichText` | Tiptap JSONContent + source metadata |
395
+ | `boolean` | `FimoBoolean` | class; `.valueOf()` → `boolean` |
396
+ | `media` | `FimoMedia` / `FimoMedia[]` | shape + rendering in `references/assets.md` |
397
+ | `number` | `number` | plain — not wrapped |
398
+ | `json` | `Record<string, any>` | plain — not wrapped |
399
+ | `date` | `any` (raw ISO date string) | not wrapped (`FimoDate` is not emitted — see below) |
400
+ | `reference` | `FimoReference` / `FimoReference[]` | reference object(s) with target `id`; use `populate` when embedded targets are needed |
397
401
 
398
402
  Rules of thumb:
399
403
 
@@ -434,7 +438,7 @@ export function BlogList() {
434
438
  return (
435
439
  <ul>
436
440
  {page.items.map((post) => (
437
- <li key={post.id}>
441
+ <li key={`${post.id}:${post.locale}`}>
438
442
  <Text value={post.title} as="h2" />
439
443
  <Image value={post.coverImage} width={800} height={400} fit="cover" />
440
444
  <RichText value={post.body} className="prose" />
@@ -451,9 +455,9 @@ For the `<Text>` / `<RichText>` / `<Image>` / `<Date>` components and the rules
451
455
  ## Read path (published app, no auth)
452
456
 
453
457
  ```ts
454
- const res = await fetch(`${import.meta.env.VITE_API_URL}/entries/<typeName>?fields=slug,title&limit=12`);
458
+ const res = await fetch(`${import.meta.env.VITE_API_URL}/entries/<typeName>?locale=en&fields=slug,title&limit=12`);
455
459
  const { data, meta } = await res.json();
456
- // data: Entry[] — fields flattened at top level (id, slug, title, ...)
460
+ // data: Entry[] — fields flattened at top level (id, entryId, documentId, locale, slug, title, ...)
457
461
  // meta: { limit, offset, total, hasMore }
458
462
  ```
459
463
 
@@ -32,7 +32,7 @@ The command is `fimo schemas` (plural). There is no singular `fimo schema` alias
32
32
 
33
33
  All commands accept `-C, --cwd <dir>` to point at a different project directory.
34
34
 
35
- Schema transitions are automatic. Adding fields and changing labels preserve content. Removing a field removes only that field's stored values. One-to-many reference changes wrap existing references in arrays. Field type changes, scalar/reference changes, reference target changes, and many-to-one changes clear only the changed field. Collection-to-singleton removes that type's entries and cleans inbound references; singleton-to-collection preserves its row. There are no reset or data-loss flags: changing the schema is the migration request. Run `fimo validate` after pushing to verify the regenerated client and whole project.
35
+ Schema transitions are automatic. Adding fields and changing labels preserve content. Removing a field removes only that field's values. One-to-many reference changes wrap existing references in arrays. Field type changes, scalar/reference changes, reference target changes, and many-to-one changes clear only the changed field. Collection-to-singleton removes that type's entries and cleans inbound references; singleton-to-collection preserves its entry. There are no reset or data-loss flags: changing the schema is the migration request. Run `fimo validate` after pushing to verify the regenerated client and whole project.
36
36
 
37
37
  ## Entries CLI commands
38
38
 
@@ -43,9 +43,9 @@ fimo entries create <TypeName> --body '<json>'
43
43
  fimo entries create <TypeName> --locale es --document-id <documentId> --body '<json>'
44
44
  fimo entries list <TypeName> [--limit N] [--offset N] [--search Q] [--where JSON] [--sort F] [--fields FIELDS] [--populate FIELDS]
45
45
  fimo entries list <TypeName> --locale es
46
- fimo entries get <TypeName> <id>
47
- fimo entries update <TypeName> <id> --locale es --body '<json>'
48
- fimo entries delete <TypeName> <id>
46
+ fimo entries get <TypeName> <id> [--locale es]
47
+ fimo entries update <TypeName> <id> [--locale es] --body '<json>'
48
+ fimo entries delete <TypeName> <id> [--locale es]
49
49
  ```
50
50
 
51
51
  `--body` is a **required** option on `create` and `update`. Its JSON shape (where `slug` goes, how `media` fields look, Tiptap richtext format) lives in `fimo/references/content.md`. **Always read that first before constructing an entry payload — slug must be inside `data`, media has a specific shape, richtext must be Tiptap JSONContent.**
@@ -65,12 +65,12 @@ fimo entries list BlogPost --fields slug,title,author --populate author
65
65
  Use the canonical read query flags: `--search`, `--sort`, `--where`, `--limit`, `--offset`, `--fields`, and `--populate`.
66
66
  `--where` is a JSON object keyed by schema field name. Simple values mean equality and stay typed inside the JSON (`{"featured":true}`, `{"category":"react"}`, `{"rank":{"$gte":3}}`); operator objects support `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$contains`, `$startsWith`, and `$null`, subject to the field type. Public Fimo-owned metadata lives under `__fimo`; currently `__fimo.contentType` is filterable.
67
67
 
68
- Reference filters take the target `documentId` string even though stored values are objects (`{"author":"750e..."}`). On a to-many field, `$eq` and `$contains` mean membership, `$in` means any listed document is present, `$ne` and `$nin` mean none are present, and `$null: true` means empty or missing. Reference fields cannot be sorted; use `--sort field` / `--sort -field` on scalar fields.
69
- `--fields` is a comma-separated projection for list payloads. The API always keeps `id`; pass only the fields your view needs, such as `slug,title,excerpt,coverImage`.
68
+ Reference filters take the target `id` string (`{"author":"750e..."}`). On a to-many field, `$eq` and `$contains` mean membership, `$in` means any listed target is present, `$ne` and `$nin` mean none are present, and `$null: true` means empty or missing. Reference fields cannot be sorted; use `--sort field` / `--sort -field` on scalar fields.
69
+ `--fields` is a comma-separated projection for list payloads. The API always keeps `id`, `entryId`, `documentId`, and `locale`; pass only the other fields your view needs, such as `slug,title,excerpt,coverImage`.
70
70
 
71
- `--populate` is a comma-separated list of reference fields to embed. Without it, reference fields return reference objects like `{ "documentId": "..." }`; with `--populate author,contributors`, those fields return the target entry objects for the requested locale. Only reference fields can be populated (scalar fields 400), and populate never selects which target fields come back. A populated target missing in the requested locale resolves to null unless you pass `localeFallbacks=true`, which falls back to the best available locale variant, marked by its `locale` field. Limits: 200 references per to-many write, 500 distinct populated documents per field per request (400 past either).
71
+ `--populate` is a comma-separated list of reference fields to embed. Without it, reference fields return reference objects like `{ "id": "..." }`; with `--populate author,contributors`, those fields return the target entry objects for the requested locale. Only reference fields can be populated (scalar fields 400), and populate never selects which target fields come back. A populated target missing in the requested locale resolves to null unless you pass `localeFallbacks=true`, which falls back to the best available locale variant, marked by its `locale` field. Limits: 200 references per to-many write, 500 distinct populated documents per field per request (400 past either).
72
72
 
73
- Polymorphic references declare `targets: ["Post", "Product"]` instead of `target` (never both) and their values carry both parts: `{ "contentType": "Post", "documentId": "..." }`. Populate returns mixed full entries, and generated hooks expose `$type` on each populated value to branch on. `--where` on polymorphic fields is not supported yet and fails with a clear error.
73
+ Polymorphic references declare `targets: ["Post", "Product"]` instead of `target` (never both) and their values carry both parts: `{ "contentType": "Post", "id": "..." }`. Populate returns mixed full entries, and generated hooks expose `$type` on each populated value to branch on. `--where` on polymorphic fields is not supported yet and fails with a clear error.
74
74
 
75
75
  In generated React code, the equivalent is `populate: ['author', 'contributors'] as const` in the schema hook/query params. Let hooks infer the result when possible; if a named TypeScript shape is needed, use the generated `BlogPostResult<TFields, TPopulate>` type documented in `fimo/references/content.md`. Writes use the separate generated `BlogPostInput` type.
76
76
 
@@ -85,28 +85,28 @@ For reference schema JSON and entry value shape, load `fimo/references/content.m
85
85
  }
86
86
  ```
87
87
 
88
- Entry bodies store reference objects with target `documentId` values:
88
+ Entry bodies use reference objects with target `id` values:
89
89
 
90
90
  ```bash
91
- fimo entries create BlogPost --body '{"data":{"slug":"hello","title":"Hello","author":{"documentId":"750e8400-e29b-41d4-a716-446655440000"},"contributors":[]}}'
91
+ fimo entries create BlogPost --body '{"data":{"slug":"hello","title":"Hello","author":{"id":"750e8400-e29b-41d4-a716-446655440000"},"contributors":[]}}'
92
92
  ```
93
93
 
94
- Do not add `localized` or field-level `i18n` to reference fields. The reference value belongs to the entry variant that stores it.
94
+ Do not add `localized` or field-level `i18n` to reference fields. The reference value belongs to the entry variant containing it.
95
95
 
96
96
  ## Localized and shared content
97
97
 
98
- Schemas are localized by default. Omit `--locale` for default-locale entries. Pass `--locale <locale>` when creating, listing, reading, or updating a specific non-default locale.
98
+ Schemas are localized by default. Omit `--locale` to use the project default. Pass it explicitly when reading, updating, or deleting a non-default variant: public identity is stable `id` plus `locale`.
99
99
 
100
100
  When creating a localized variant of an existing entry, pass the source entry's `documentId` with `--document-id`. Keep projects single-locale unless the user asks for multilingual or localized output; when multiple locales are configured, create and update every locale explicitly.
101
101
 
102
- For content shared across locales, define `"i18n": { "enabled": false }` at the schema root and create each entry once. Do not pass `--document-id` to manufacture locale variants. Shared entry responses omit `locale`; a read locale is only used to resolve populated references that target localized schemas. No other `i18n` options and no field-level localization settings are supported.
102
+ For content shared across locales, define `"i18n": { "enabled": false }` at the schema root and create each entry once. Do not pass `--document-id` to manufacture locale variants. Raw API and CLI responses omit `locale`; generated collection models add the effective active/default locale without creating per-locale variants. No other `i18n` options and no field-level localization settings are supported.
103
103
 
104
104
  Use `"isSingleton": true` for a type with exactly one logical document. Generated singleton clients expose `get`/`update` and their React Query hooks, not collection create/remove APIs.
105
105
 
106
106
  ## "Add a blog" recipe
107
107
 
108
108
  1. Load `fimo/references/content.md` for the JSON shape and field types.
109
- 2. Write `src/schemas/BlogPost.json` with fields `title`, `body` (richtext), `coverImage` (media), etc. Do NOT declare a `slug` field — `slug` (and `id`, `documentId`, `locale`, `createdAt`, `updatedAt`, `__fimo`) is reserved entry metadata and will be rejected; every entry gets a `slug` for free.
109
+ 2. Write `src/schemas/BlogPost.json` with fields `title`, `body` (richtext), `coverImage` (media), etc. Do NOT declare a `slug` field — `slug` (and `id`, `entryId`, `documentId`, `locale`, `createdAt`, `updatedAt`, `__fimo`) is reserved entry metadata and will be rejected; every entry gets a `slug` for free.
110
110
  3. Run `fimo schemas push BlogPost`.
111
111
  4. Generate cover images: `fimo generate pruna/p-image --prompt "..."` (per-model flags come from `fimo models show <model>`; see `media.md`).
112
112
  5. Seed entries: `fimo entries create BlogPost --body '{"data": {"slug": "first-post", "title": "...", "coverImage": {...}}}'`.
@@ -132,7 +132,7 @@ Propagation: typically 15–30 minutes, up to 48 hours rarely.
132
132
 
133
133
  ## Web app equivalent
134
134
 
135
- For users on the dashboard, the same workflow lives at **Settings → Domains** (custom domain) and **Organization → Domains** (buy/owned). See `fimo-studio/references/custom-domains.md` and the [public docs](https://docs.fimo.ai/publish/custom-domains).
135
+ For users on the dashboard, the same workflow lives at **Settings → Domains** (custom domain) and **Organization → Domains** (buy/owned). See `fimo-studio/references/custom-domains.md` and the [public docs](https://fimo.ai/docs/publish/custom-domains).
136
136
 
137
137
  ## Anti-patterns
138
138
 
@@ -1,6 +1,6 @@
1
1
  ## Also available via CLI
2
2
 
3
- If you're driving Fimo from the terminal, the equivalent commands are `fimo domains add`, `fimo domains refresh`, and `fimo domains remove`. The default URL (the `<slug>.fimo.site` subdomain) is managed with `fimo domains rename-default` and `fimo domains suggest-default` — both free, no Premium needed. See the `fimo-cli` skill's `references/domains.md`. Full docs: https://docs.fimo.ai/publish/custom-domains.
3
+ If you're driving Fimo from the terminal, the equivalent commands are `fimo domains add`, `fimo domains refresh`, and `fimo domains remove`. The default URL (the `<slug>.fimo.site` subdomain) is managed with `fimo domains rename-default` and `fimo domains suggest-default` — both free, no Premium needed. See the `fimo-cli` skill's `references/domains.md`. Full docs: https://fimo.ai/docs/publish/custom-domains.
4
4
 
5
5
  ## Before You Start
6
6
 
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "bundled": true,
3
3
  "bundler": "esbuild",
4
- "bundledAt": "2026-07-14T21:14:24.078Z",
4
+ "bundledAt": "2026-07-16T16:23:22.068Z",
5
5
  "cliVersion": "0.6.0",
6
6
  "external": [
7
7
  "oxc-parser",
8
- "fsevents"
8
+ "fsevents",
9
+ "zod"
9
10
  ]
10
11
  }