fimo 0.4.0-experimental.1 → 0.4.0-experimental.3

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.
@@ -10,6 +10,7 @@ This file covers the **package surface**: the JSON shape you write, the generate
10
10
  {
11
11
  "uid": "BlogPost",
12
12
  "name": "Blog Posts",
13
+ "i18n": { "enabled": true },
13
14
  "fields": {
14
15
  "title": { "type": "string" },
15
16
  "body": { "type": "richtext" },
@@ -25,6 +26,8 @@ This file covers the **package surface**: the JSON shape you write, the generate
25
26
 
26
27
  **Required**: `uid` (PascalCase, matches filename), `name`, `fields`.
27
28
 
29
+ `i18n` is an optional type-level policy. Omit it or use `{ "enabled": true }` for localized content. Use `{ "enabled": false }` for one shared entry that appears in every locale, such as an Author, Category, or Settings type. No other `i18n` keys and no field-level localization settings are supported.
30
+
28
31
  ### Reserved field names — do NOT declare these in `fields`
29
32
 
30
33
  Every entry already carries this metadata automatically. Declaring any of them as a
@@ -33,6 +36,8 @@ reserved entry metadata and cannot be used as a content field.`):
33
36
 
34
37
  `id`, `documentId`, `slug`, `locale`, `createdAt`, `updatedAt`, `__fimo`.
35
38
 
39
+ Localized entries expose `locale`; non-localized entries omit it. It remains reserved in both schemas.
40
+
36
41
  In particular **do not add a `slug` field** — every entry gets a `slug` for free. You still
37
42
  pass `slug` inside `data` when _creating an entry_ (see "Entry body shape" below); it just
38
43
  must not appear in the schema's `fields`. If you need an additional URL-ish string, name it
@@ -40,16 +45,16 @@ something else (e.g. `handle`, `permalink`).
40
45
 
41
46
  ## Field types
42
47
 
43
- | Type | `data` JSON shape | Use for |
44
- | ---------- | ----------------------------------------------------------------------- | --------------------------------- |
45
- | `string` | `"..."` | Titles, short text |
46
- | `richtext` | Tiptap JSONContent (see below) | Blog body, descriptions, articles |
47
- | `number` | `42` | Counts, quantities |
48
- | `boolean` | `true` / `false` | Flags |
49
- | `date` | ISO date string, e.g. `"2026-06-03"` | Publish dates, timestamps |
50
- | `media` | `{ "source": "local"\|"external", "url": "https://...", "alt": "..." }` | Images, video |
51
- | `json` | any JSON | Structured metadata |
52
- | `relation` | document ID string, `null`, or document ID array 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
+ | `relation` | `{ "documentId": "..." }`, `null`, or an array of reference objects when `many: true` | References to another schema |
53
58
 
54
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.
55
60
 
@@ -74,7 +79,10 @@ Rules:
74
79
  - `target` is required and must be the PascalCase schema `uid` of the referenced content type.
75
80
  - Omit `many` for a to-one relation. Use `"many": true` for a to-many relation.
76
81
  - Self-references are allowed, e.g. a `Category` schema can have `parent: { "type": "relation", "target": "Category" }`.
77
- - Entry values store target `documentId` values, not row `id` values. Use `null` for an empty to-one relation and `[]` for an empty to-many relation.
82
+ - Entry values store reference objects with the target `documentId`, not row `id` values: `{ "documentId": "..." }`.
83
+ - Do not include `contentType` for normal fixed-target relations; the schema's `target` already defines it.
84
+ - Do not add `localized` or field-level `i18n` to a relation. The relation value belongs to the entry variant that stores it.
85
+ - Use `null` for an empty to-one relation and `[]` for an empty to-many relation.
78
86
  - The API validates that referenced target documents exist and rejects duplicate values in to-many relation arrays.
79
87
 
80
88
  Entry payload example:
@@ -84,8 +92,8 @@ Entry payload example:
84
92
  "data": {
85
93
  "slug": "relation-example",
86
94
  "title": "Relation example",
87
- "author": "750e8400-e29b-41d4-a716-446655440000",
88
- "contributors": ["750e8400-e29b-41d4-a716-446655440000"]
95
+ "author": { "documentId": "750e8400-e29b-41d4-a716-446655440000" },
96
+ "contributors": [{ "documentId": "750e8400-e29b-41d4-a716-446655440000" }]
89
97
  }
90
98
  }
91
99
  ```
@@ -159,7 +167,12 @@ For `media` fields, prefer `source: "local"` with assets uploaded via `fimo asse
159
167
 
160
168
  ## Generated React Query hooks
161
169
 
162
- After pre-build (or `fimo deploy`), `src/schemas/{Uid}.ts` exports typed helpers. Import each schema from its own module, e.g. `import { useGet } from '@/schemas/BlogPost'` — there is no generated `src/schemas/index.ts` barrel.
170
+ After pre-build (or `fimo deploy`), each schema generates two files:
171
+
172
+ - `src/schemas/{Uid}.types.ts` — pure TypeScript types such as `BlogPost`, `BlogPostInput`, `BlogPostQuery`, and `BlogPostResult`.
173
+ - `src/schemas/{Uid}.ts` — runtime helpers, React Query hooks, default export, and re-exported public types.
174
+
175
+ Import from the public schema module, e.g. `import BlogPost from '@/schemas/BlogPost'` and `import type { BlogPostResult } from '@/schemas/BlogPost'`. Do not import from `.types.ts` directly in app code. There is no generated `src/schemas/index.ts` barrel.
163
176
 
164
177
  **Entity shape — fields live at the top level of the entry**, not under a nested `data` key. The generated `BlogPost` interface looks like:
165
178
 
@@ -168,12 +181,11 @@ interface BlogPost {
168
181
  id: string;
169
182
  documentId: string;
170
183
  slug: FimoString;
171
- locale: string;
184
+ locale: string; // localized schemas only; omitted when i18n.enabled is false
172
185
  createdAt: string;
173
186
  updatedAt: string;
174
187
  __fimo: {
175
188
  contentType: string;
176
- translationProgress: { state: 'queued' | 'translating'; fields?: string[] } | null;
177
189
  };
178
190
  title: FimoString;
179
191
  body: FimoRichText;
@@ -184,37 +196,58 @@ interface BlogPost {
184
196
  }
185
197
  ```
186
198
 
187
- Content fields live at the top level. Fimo-owned metadata that is not part of the user schema lives under `__fimo`, so a schema can safely define fields named `contentType` or `translationProgress`.
199
+ Content fields live at the top level. Fimo-owned metadata that is not part of the user schema lives under `__fimo`, so a schema can safely define fields named `contentType`.
188
200
 
189
- ## Localized content
201
+ ## Localized and shared content
190
202
 
191
- Content entries are stored per locale. Generated hooks read the active Fimo locale automatically.
203
+ Localized content is stored per locale. Generated hooks read the active Fimo locale automatically.
192
204
 
193
205
  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.
194
206
 
207
+ For shared content, set `i18n: { "enabled": false }` on the schema and create each entry once. Shared entries:
208
+
209
+ - have no public `locale` property;
210
+ - are returned in every locale context without duplication;
211
+ - ignore locale on owner reads and writes;
212
+ - still use the active/requested locale when a populated relation targets a localized schema.
213
+
214
+ Do not duplicate shared entries across locales, add field-level localization settings, or implement locale fallbacks.
215
+
195
216
  **Generated API:**
196
217
 
197
218
  ```ts
198
219
  // low-level fetch helpers
199
- getById(id: string): Promise<BlogPost>
200
- getBySlug(slug: string): Promise<BlogPost | null>
201
- getByField(fieldName: string, value: string): Promise<BlogPost | null>
202
- get<TFields extends BlogPostFields | undefined>(params?: BlogPostQuery<TFields>): Promise<BlogPostResult<TFields>[]>
203
- create(payload: Partial<BlogPost>): Promise<BlogPost>
204
- update(id: string, payload: Partial<BlogPost>): Promise<BlogPost>
220
+ getById<TPopulate extends BlogPostPopulate | undefined>(id: string, options?: { populate?: TPopulate }): Promise<BlogPostResult<undefined, TPopulate>>
221
+ getBySlug<TPopulate extends BlogPostPopulate | undefined>(slug: string, options?: { populate?: TPopulate }): Promise<BlogPostResult<undefined, TPopulate> | null>
222
+ getByField<TPopulate extends BlogPostPopulate | undefined>(fieldName: string, value: string, options?: { populate?: TPopulate }): Promise<BlogPostResult<undefined, TPopulate> | null>
223
+ get<TFields extends BlogPostFields | undefined, TPopulate extends BlogPostPopulate | undefined>(params?: BlogPostQuery<TFields, TPopulate>): Promise<BlogPostResult<TFields, TPopulate>[]>
224
+ create(payload: BlogPostInput): Promise<BlogPost>
225
+ update(id: string, payload: Partial<BlogPostInput>): Promise<BlogPost>
205
226
  remove(id: string): Promise<void>
206
227
 
207
228
  // react-query hooks
208
- useGetById(id) // → { data: BlogPost | undefined, isLoading, ... }
209
- useGetBySlug(slug) // → { data: BlogPost | null | undefined, ... } — ideal for /:slug detail routes
210
- useGetByField(fieldName, value) // → { data: BlogPost | null | undefined, ... }
211
- useGet<TFields>(params?) // → { data: BlogPostResult<TFields>[] | undefined, isLoading, ... }
229
+ useGetById(id, { populate? }) // → { data: BlogPostResult<undefined, TPopulate> | undefined, isLoading, ... }
230
+ useGetBySlug(slug, { populate? }) // → { data: BlogPostResult<undefined, TPopulate> | null | undefined, ... } — ideal for /:slug detail routes
231
+ useGetByField(fieldName, value, { populate? })
232
+ useGet<TFields, TPopulate>(params?) // → { data: BlogPostResult<TFields, TPopulate>[] | undefined, isLoading, ... }
212
233
  useCreate() / useUpdate() / useRemove()
213
234
 
214
235
  // Suspense variants (same signatures, throw to a <Suspense> boundary instead of returning isLoading)
215
236
  useSuspenseGetById(id) / useSuspenseGetBySlug(slug) / useSuspenseGetByField(fieldName, value) / useSuspenseGet(params?)
216
237
  ```
217
238
 
239
+ Write inputs contain plain JSON values. Read models contain Fimo wrapper values for source tracking, so use `BlogPostInput` rather than sending a `BlogPost` back to `create` or `update`.
240
+
241
+ For a schema with `"isSingleton": true`, codegen omits collection methods and generates this smaller API:
242
+
243
+ ```ts
244
+ get<TPopulate extends SettingsPopulate | undefined>(options?: { locale?: string; populate?: TPopulate }): Promise<SettingsResult<undefined, TPopulate>>
245
+ update(payload: Partial<SettingsInput>): Promise<Settings>
246
+ useGet(options?) / useSuspenseGet(options?) / useUpdate()
247
+ ```
248
+
249
+ 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.
250
+
218
251
  Access pattern — the hooks **unwrap** the API envelope, so `get()` / `useGet()` resolve to a `BlogPost[]` directly (not a `{ data, meta }` wrapper):
219
252
 
220
253
  ```tsx
@@ -224,6 +257,7 @@ posts.map((post) => /* ... */); // BlogPost[]
224
257
  ```
225
258
 
226
259
  Query params supported: `limit` (1-100, default 20), `offset` (default 0), `search`, `sort`, `where`, `fields`, and `populate`. Use these canonical names in new code.
260
+ `fields` and `populate` change the returned item type. `where`, `search`, `sort`, `limit`, and `offset` only change selection or ordering. `locale` selects a localized owner variant, or the localized target variant when shared content is populated; raw shared reads omit it.
227
261
 
228
262
  `where` filters on the server. Use scalar values for equality and operator objects for ranges, lists, text matches, or null checks. Keep boolean, string, and number values typed in the object:
229
263
 
@@ -243,6 +277,8 @@ BlogPost.useGet({
243
277
  ```
244
278
 
245
279
  Operators: scalar equality, `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$contains`, `$startsWith`, `$null`.
280
+ Relation field values are reference objects, but relation filters use the target `documentId` string, for example `where: { author: "750e..." }`; for to-many relations that means "contains this target document".
281
+ For to-many relations, `$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. Relation fields cannot be sorted.
246
282
 
247
283
  Use `fields` to keep list payloads small. The API always keeps `id`, and returns only the requested fields beyond that:
248
284
 
@@ -262,10 +298,26 @@ const { data: posts = [] } = BlogPost.useGet({
262
298
  fields: ['slug', 'title', 'author'] as const,
263
299
  populate: ['author'] as const,
264
300
  });
265
- // Without populate: post.author is a target documentId string.
301
+ // Without populate: post.author is { documentId: "..." }.
266
302
  // With populate: post.author is the target Author entry object, or null.
267
303
  ```
268
304
 
305
+ Keep `fields` and `populate` arrays `as const` so TypeScript can infer the precise projected and populated shape. Prefer hook inference. If a helper/component needs a named type, use the generated result type:
306
+
307
+ ```ts
308
+ import type { BlogPostResult } from '@/schemas/BlogPost';
309
+
310
+ type PostWithAuthor = BlogPostResult<undefined, 'author'>;
311
+ type PostCardItem = BlogPostResult<
312
+ readonly ['slug', 'title', 'author', 'contributors'],
313
+ readonly ['author', 'contributors']
314
+ >;
315
+ ```
316
+
317
+ Do not type populated hook data as `BlogPost[]`: plain `BlogPost` keeps relation fields as `FimoReference` objects, while `BlogPostResult<TFields, TPopulate>` models the projected/populated read result.
318
+
319
+ `BlogPostPopulate` is the generated input type for allowed populate values. It comes from the schema's relation fields, so a post schema with `author` and `contributors` only accepts those names in `populate`. Most app code never imports it directly; it is mainly used by `BlogPostQuery` and hook generics.
320
+
269
321
  Populate is depth-one. It only works for relation fields; trying to populate a scalar field returns 400. To-many relation order is preserved.
270
322
 
271
323
  The generated hooks do **not** surface pagination meta (`total`, `hasMore`, `limit`, `offset`). If you need it, read it from a raw fetch to `/entries/<typeName>` (see the **Read path** section below), which returns the full `{ data, meta }` envelope.
@@ -274,16 +326,16 @@ The generated hooks do **not** surface pagination meta (`total`, `hasMore`, `lim
274
326
 
275
327
  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:
276
328
 
277
- | Schema field type | Field value you get | Notes |
278
- | ----------------- | --------------------------- | -------------------------------------------------------------------------------------------- |
279
- | `string` | `FimoString` | `extends String` — **not** a primitive `string` |
280
- | `richtext` | `FimoRichText` | Tiptap JSONContent + source metadata |
281
- | `boolean` | `FimoBoolean` | class; `.valueOf()` → `boolean` |
282
- | `media` | `FimoMedia` / `FimoMedia[]` | shape + rendering in `references/assets.md` |
283
- | `number` | `number` | plain — not wrapped |
284
- | `json` | `Record<string, any>` | plain — not wrapped |
285
- | `date` | `any` (raw ISO date string) | not wrapped (`FimoDate` is not emitted — see below) |
286
- | `relation` | `string` / `string[]` | target `documentId` value(s); use `populate` on raw API when embedded target rows are needed |
329
+ | Schema field type | Field value you get | Notes |
330
+ | ----------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------- |
331
+ | `string` | `FimoString` | `extends String` — **not** a primitive `string` |
332
+ | `richtext` | `FimoRichText` | Tiptap JSONContent + source metadata |
333
+ | `boolean` | `FimoBoolean` | class; `.valueOf()` → `boolean` |
334
+ | `media` | `FimoMedia` / `FimoMedia[]` | shape + rendering in `references/assets.md` |
335
+ | `number` | `number` | plain — not wrapped |
336
+ | `json` | `Record<string, any>` | plain — not wrapped |
337
+ | `date` | `any` (raw ISO date string) | not wrapped (`FimoDate` is not emitted — see below) |
338
+ | `relation` | `FimoReference` / `FimoReference[]` | reference object(s) with target `documentId`; use `populate` when embedded target rows are needed |
287
339
 
288
340
  Rules of thumb:
289
341
 
@@ -347,7 +399,7 @@ const { data, meta } = await res.json();
347
399
  // meta: { limit, offset, total, hasMore }
348
400
  ```
349
401
 
350
- Prefer the generated React Query hooks from `@/schemas/{Uid}` over hand-rolled fetch — they handle caching and types (returning a typed `BlogPost[]`). Reach for this raw fetch only when you need the pagination `meta`, which the hooks don't surface.
402
+ Prefer the generated React Query hooks from `@/schemas/{Uid}` over hand-rolled fetch — they handle caching and types (returning typed `BlogPostResult<TFields, TPopulate>[]` results). Reach for this raw fetch only when you need the pagination `meta`, which the hooks don't surface.
351
403
 
352
404
  ## Forms vs. schemas
353
405
 
@@ -22,7 +22,7 @@ Fimo provides locale utilities that follow the Fimo standard, but they are helpe
22
22
  - `defaultLocale` is the source/default locale.
23
23
  - `locales` lists enabled locales and must include `defaultLocale`. Do not add non-default locales unless the user asks for multiple languages, localized routes, translations, or a specific locale.
24
24
  - `routing.strategy` currently supports `path-prefix`.
25
- - If multiple locales are configured, create or update labels and content for every configured locale.
25
+ - If multiple locales are configured, create or update labels and localized content for every configured locale. Create schemas with `i18n: { "enabled": false }` only once; those entries are shared across locales.
26
26
 
27
27
  Locale tags should be structurally valid, for example `en`, `es`, `pt-BR`, or `zh-Hans`.
28
28
 
@@ -10,7 +10,7 @@ This file covers the **CLI workflow** for content. Use it when the user wants to
10
10
 
11
11
  1. Write `src/schemas/{Uid}.json` — PascalCase filename (e.g. `BlogPost.json`). For the JSON shape and field types, load `fimo/references/content.md`.
12
12
  2. Register the schema with `fimo schemas push {Uid}`.
13
- 3. Pre-build generates `src/schemas/{Uid}.ts` — typed React Query hooks. See `fimo/references/content.md` for the hook API.
13
+ 3. Pre-build generates `src/schemas/{Uid}.types.ts` and `src/schemas/{Uid}.ts` — type definitions plus typed React Query hooks. App code imports from `@/schemas/{Uid}`, not from `.types.ts` directly. See `fimo/references/content.md` for the hook API.
14
14
  4. Seed entries via `fimo entries create`. Generate cover images first with `fimo generate <model>` if needed (see `media.md` / `assets.md`).
15
15
  5. Write React pages that import from `@/schemas/{Uid}` and render with `fimo/ui` components.
16
16
 
@@ -57,11 +57,13 @@ fimo entries list BlogPost --fields slug,title,excerpt,coverImage --sort -publis
57
57
  fimo entries list BlogPost --fields slug,title,author --populate author
58
58
  ```
59
59
 
60
- Use the canonical read query flags: `--search`, `--sort`, `--where`, `--limit`, `--offset`, and `--fields`.
61
- `--where` is a JSON object keyed by schema field name. Simple values mean equality, so booleans, strings, and numbers should stay typed inside the JSON (`{"featured":true}`, `{"category":"react"}`, `{"rank":{"$gte":3}}`). Public Fimo-owned metadata lives under `__fimo`; currently `__fimo.contentType` is filterable. Operator objects support `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$contains`, `$startsWith`, and `$null`, subject to the field type. Use `--sort field` for ascending and `--sort -field` for descending.
60
+ Use the canonical read query flags: `--search`, `--sort`, `--where`, `--limit`, `--offset`, `--fields`, and `--populate`.
61
+ `--where` is a JSON object keyed by schema field name. Simple values mean equality, so booleans, strings, and numbers should stay typed inside the JSON (`{"featured":true}`, `{"category":"react"}`, `{"rank":{"$gte":3}}`). Relation field values are reference objects, but relation filters use the target `documentId` string, for example `{"author":"750e..."}`; for to-many relations that means "contains this target document". Public Fimo-owned metadata lives under `__fimo`; currently `__fimo.contentType` is filterable. Operator objects support `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$contains`, `$startsWith`, and `$null`, subject to the field type. For to-many relations, `$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. Relation fields cannot be sorted. Use `--sort field` for ascending and `--sort -field` for descending on scalar fields.
62
62
  `--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`.
63
63
 
64
- `--populate` is a comma-separated list of relation fields to embed. Without `--populate`, relation fields return target `documentId` strings. With `--populate author,contributors`, those fields return the target entry objects for the requested locale. Only relation fields can be populated; asking to populate a scalar field returns 400.
64
+ `--populate` is a comma-separated list of relation fields to embed. Without `--populate`, relation fields return reference objects like `{ "documentId": "..." }`. With `--populate author,contributors`, those fields return the target entry objects for the requested locale. Only relation fields can be populated; asking to populate a scalar field returns 400.
65
+
66
+ 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.
65
67
 
66
68
  For relation schema JSON and entry value shape, load `fimo/references/content.md` § Relation fields. The short version:
67
69
 
@@ -74,18 +76,24 @@ For relation schema JSON and entry value shape, load `fimo/references/content.md
74
76
  }
75
77
  ```
76
78
 
77
- Entry bodies store target `documentId` values:
79
+ Entry bodies store reference objects with target `documentId` values:
78
80
 
79
81
  ```bash
80
- fimo entries create BlogPost --body '{"data":{"slug":"hello","title":"Hello","author":"750e8400-e29b-41d4-a716-446655440000","contributors":[]}}'
82
+ fimo entries create BlogPost --body '{"data":{"slug":"hello","title":"Hello","author":{"documentId":"750e8400-e29b-41d4-a716-446655440000"},"contributors":[]}}'
81
83
  ```
82
84
 
83
- ## Localized content
85
+ Do not add `localized` or field-level `i18n` to relation fields. The relation value belongs to the entry variant that stores it.
86
+
87
+ ## Localized and shared content
84
88
 
85
- Content entries are stored per locale. Omit `--locale` for default-locale entries. Pass `--locale <locale>` when creating, listing, reading, or updating a specific non-default locale.
89
+ 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.
86
90
 
87
91
  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.
88
92
 
93
+ 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 relations that target localized schemas. No other `i18n` options and no field-level localization settings are supported.
94
+
95
+ 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.
96
+
89
97
  ## "Add a blog" recipe
90
98
 
91
99
  1. Load `fimo/references/content.md` for the JSON shape and field types.
@@ -108,7 +116,7 @@ fimo validate # runs all phases: codegen, lint, route validatio
108
116
  Advanced/internal (hidden from `fimo --help`) — for the bare regen phase:
109
117
 
110
118
  ```bash
111
- fimo scripts sync-schemas # regenerate src/schemas/*.ts only
119
+ fimo scripts sync-schemas # regenerate src/schemas/*.types.ts and src/schemas/*.ts
112
120
  ```
113
121
 
114
122
  ## Non-interactive use
@@ -25,7 +25,7 @@ Edit `fimo-config.json`. Keep one locale unless the user asks for multilingual o
25
25
  - `locales` must include `defaultLocale`. Do not add non-default locales unless the user asks for multiple languages, localized routes, translations, or a specific locale.
26
26
  - Locale tags should be structurally valid, for example `en`, `es`, `pt-BR`, or `zh-Hans`.
27
27
  - `routing.strategy` currently supports `path-prefix`.
28
- - If multiple locales are configured, create or update labels and content for every configured locale.
28
+ - If multiple locales are configured, create or update labels and localized content for every configured locale. Create schemas with `i18n: { "enabled": false }` only once.
29
29
 
30
30
  ## Validate
31
31
 
@@ -74,4 +74,4 @@ Labels and content are separate translated surfaces:
74
74
  - labels: static UI copy declared with `t('key')`, managed with `fimo labels`
75
75
  - content: schema entries, managed with `fimo entries`
76
76
 
77
- Omit `--locale` for default-locale writes. Pass `--locale <locale>` for each non-default locale you need to create or update.
77
+ Omit `--locale` for default-locale writes. Pass `--locale <locale>` for each non-default localized entry you need to create or update. Shared schemas (`i18n.enabled: false`) have one locale-free entry instead.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "bundled": true,
3
3
  "bundler": "esbuild",
4
- "bundledAt": "2026-07-08T14:52:17.138Z",
5
- "cliVersion": "0.3.5",
4
+ "bundledAt": "2026-07-10T13:57:48.782Z",
5
+ "cliVersion": "0.3.6",
6
6
  "external": [
7
7
  "oxc-parser",
8
8
  "fsevents"