fimo 0.4.0-experimental.2 → 0.4.0-experimental.4

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,12 +92,25 @@ 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
  ```
92
100
 
101
+ ## Schema transitions
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:
104
+
105
+ - Adding fields and changing labels preserve entries.
106
+ - Removing a field removes only that field's values.
107
+ - A one-to-many relation change wraps each existing reference in an array.
108
+ - A many-to-one change, relation target change, scalar type change, or scalar/relation change clears only the changed field.
109
+ - Collection-to-singleton removes that type's entries and cleans references to those documents.
110
+ - Singleton-to-collection preserves the existing row.
111
+
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
+
93
114
  **Default to `richtext`** for fields named `body`, `content`, `description`, `summary`, `bio`, `intro`, `details`, `excerpt` — only use `string` when the field is explicitly short (e.g. a 160-char meta description).
94
115
 
95
116
  ## Tiptap JSONContent (richtext)
@@ -159,7 +180,12 @@ For `media` fields, prefer `source: "local"` with assets uploaded via `fimo asse
159
180
 
160
181
  ## Generated React Query hooks
161
182
 
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.
183
+ After pre-build (or `fimo deploy`), each schema generates two files:
184
+
185
+ - `src/schemas/{Uid}.types.ts` — pure TypeScript types such as `BlogPost`, `BlogPostInput`, `BlogPostQuery`, and `BlogPostResult`.
186
+ - `src/schemas/{Uid}.ts` — runtime helpers, React Query hooks, default export, and re-exported public types.
187
+
188
+ 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
189
 
164
190
  **Entity shape — fields live at the top level of the entry**, not under a nested `data` key. The generated `BlogPost` interface looks like:
165
191
 
@@ -168,7 +194,7 @@ interface BlogPost {
168
194
  id: string;
169
195
  documentId: string;
170
196
  slug: FimoString;
171
- locale: string;
197
+ locale: string; // localized schemas only; omitted when i18n.enabled is false
172
198
  createdAt: string;
173
199
  updatedAt: string;
174
200
  __fimo: {
@@ -185,35 +211,56 @@ interface BlogPost {
185
211
 
186
212
  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`.
187
213
 
188
- ## Localized content
214
+ ## Localized and shared content
189
215
 
190
- Content entries are stored per locale. Generated hooks read the active Fimo locale automatically.
216
+ Localized content is stored per locale. Generated hooks read the active Fimo locale automatically.
191
217
 
192
218
  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.
193
219
 
220
+ For shared content, set `i18n: { "enabled": false }` on the schema and create each entry once. Shared entries:
221
+
222
+ - have no public `locale` property;
223
+ - are returned in every locale context without duplication;
224
+ - ignore locale on owner reads and writes;
225
+ - still use the active/requested locale when a populated relation targets a localized schema.
226
+
227
+ Do not duplicate shared entries across locales, add field-level localization settings, or implement locale fallbacks.
228
+
194
229
  **Generated API:**
195
230
 
196
231
  ```ts
197
232
  // low-level fetch helpers
198
- getById(id: string): Promise<BlogPost>
199
- getBySlug(slug: string): Promise<BlogPost | null>
200
- getByField(fieldName: string, value: string): Promise<BlogPost | null>
201
- get<TFields extends BlogPostFields | undefined>(params?: BlogPostQuery<TFields>): Promise<BlogPostResult<TFields>[]>
202
- create(payload: Partial<BlogPost>): Promise<BlogPost>
203
- update(id: string, payload: Partial<BlogPost>): Promise<BlogPost>
233
+ getById<TPopulate extends BlogPostPopulate | undefined>(id: string, options?: { populate?: TPopulate }): Promise<BlogPostResult<undefined, TPopulate>>
234
+ getBySlug<TPopulate extends BlogPostPopulate | undefined>(slug: string, options?: { populate?: TPopulate }): Promise<BlogPostResult<undefined, TPopulate> | null>
235
+ getByField<TPopulate extends BlogPostPopulate | undefined>(fieldName: string, value: string, options?: { populate?: TPopulate }): Promise<BlogPostResult<undefined, TPopulate> | null>
236
+ get<TFields extends BlogPostFields | undefined, TPopulate extends BlogPostPopulate | undefined>(params?: BlogPostQuery<TFields, TPopulate>): Promise<BlogPostResult<TFields, TPopulate>[]>
237
+ create(payload: BlogPostInput): Promise<BlogPost>
238
+ update(id: string, payload: Partial<BlogPostInput>): Promise<BlogPost>
204
239
  remove(id: string): Promise<void>
205
240
 
206
241
  // react-query hooks
207
- useGetById(id) // → { data: BlogPost | undefined, isLoading, ... }
208
- useGetBySlug(slug) // → { data: BlogPost | null | undefined, ... } — ideal for /:slug detail routes
209
- useGetByField(fieldName, value) // → { data: BlogPost | null | undefined, ... }
210
- useGet<TFields>(params?) // → { data: BlogPostResult<TFields>[] | undefined, isLoading, ... }
242
+ useGetById(id, { populate? }) // → { data: BlogPostResult<undefined, TPopulate> | undefined, isLoading, ... }
243
+ useGetBySlug(slug, { populate? }) // → { data: BlogPostResult<undefined, TPopulate> | null | undefined, ... } — ideal for /:slug detail routes
244
+ useGetByField(fieldName, value, { populate? })
245
+ useGet<TFields, TPopulate>(params?) // → { data: BlogPostResult<TFields, TPopulate>[] | undefined, isLoading, ... }
211
246
  useCreate() / useUpdate() / useRemove()
212
247
 
213
248
  // Suspense variants (same signatures, throw to a <Suspense> boundary instead of returning isLoading)
214
249
  useSuspenseGetById(id) / useSuspenseGetBySlug(slug) / useSuspenseGetByField(fieldName, value) / useSuspenseGet(params?)
215
250
  ```
216
251
 
252
+ 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`.
253
+
254
+ For a schema with `"isSingleton": true`, codegen omits collection methods and generates this smaller API:
255
+
256
+ ```ts
257
+ get<TPopulate extends SettingsPopulate | undefined>(options?: { locale?: string; populate?: TPopulate }): Promise<SettingsResult<undefined, TPopulate>>
258
+ update(payload: Partial<SettingsInput>): Promise<Settings>
259
+ useGet(options?) / useSuspenseGet(options?) / useUpdate()
260
+ ```
261
+
262
+ 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.
263
+
217
264
  Access pattern — the hooks **unwrap** the API envelope, so `get()` / `useGet()` resolve to a `BlogPost[]` directly (not a `{ data, meta }` wrapper):
218
265
 
219
266
  ```tsx
@@ -223,6 +270,7 @@ posts.map((post) => /* ... */); // BlogPost[]
223
270
  ```
224
271
 
225
272
  Query params supported: `limit` (1-100, default 20), `offset` (default 0), `search`, `sort`, `where`, `fields`, and `populate`. Use these canonical names in new code.
273
+ `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.
226
274
 
227
275
  `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:
228
276
 
@@ -242,6 +290,8 @@ BlogPost.useGet({
242
290
  ```
243
291
 
244
292
  Operators: scalar equality, `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$contains`, `$startsWith`, `$null`.
293
+ 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".
294
+ 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.
245
295
 
246
296
  Use `fields` to keep list payloads small. The API always keeps `id`, and returns only the requested fields beyond that:
247
297
 
@@ -261,10 +311,26 @@ const { data: posts = [] } = BlogPost.useGet({
261
311
  fields: ['slug', 'title', 'author'] as const,
262
312
  populate: ['author'] as const,
263
313
  });
264
- // Without populate: post.author is a target documentId string.
314
+ // Without populate: post.author is { documentId: "..." }.
265
315
  // With populate: post.author is the target Author entry object, or null.
266
316
  ```
267
317
 
318
+ 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:
319
+
320
+ ```ts
321
+ import type { BlogPostResult } from '@/schemas/BlogPost';
322
+
323
+ type PostWithAuthor = BlogPostResult<undefined, 'author'>;
324
+ type PostCardItem = BlogPostResult<
325
+ readonly ['slug', 'title', 'author', 'contributors'],
326
+ readonly ['author', 'contributors']
327
+ >;
328
+ ```
329
+
330
+ 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.
331
+
332
+ `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.
333
+
268
334
  Populate is depth-one. It only works for relation fields; trying to populate a scalar field returns 400. To-many relation order is preserved.
269
335
 
270
336
  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.
@@ -273,16 +339,16 @@ The generated hooks do **not** surface pagination meta (`total`, `hasMore`, `lim
273
339
 
274
340
  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:
275
341
 
276
- | Schema field type | Field value you get | Notes |
277
- | ----------------- | --------------------------- | -------------------------------------------------------------------------------------------- |
278
- | `string` | `FimoString` | `extends String` — **not** a primitive `string` |
279
- | `richtext` | `FimoRichText` | Tiptap JSONContent + source metadata |
280
- | `boolean` | `FimoBoolean` | class; `.valueOf()` → `boolean` |
281
- | `media` | `FimoMedia` / `FimoMedia[]` | shape + rendering in `references/assets.md` |
282
- | `number` | `number` | plain — not wrapped |
283
- | `json` | `Record<string, any>` | plain — not wrapped |
284
- | `date` | `any` (raw ISO date string) | not wrapped (`FimoDate` is not emitted — see below) |
285
- | `relation` | `string` / `string[]` | target `documentId` value(s); use `populate` on raw API when embedded target rows are needed |
342
+ | Schema field type | Field value you get | Notes |
343
+ | ----------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------- |
344
+ | `string` | `FimoString` | `extends String` — **not** a primitive `string` |
345
+ | `richtext` | `FimoRichText` | Tiptap JSONContent + source metadata |
346
+ | `boolean` | `FimoBoolean` | class; `.valueOf()` → `boolean` |
347
+ | `media` | `FimoMedia` / `FimoMedia[]` | shape + rendering in `references/assets.md` |
348
+ | `number` | `number` | plain — not wrapped |
349
+ | `json` | `Record<string, any>` | plain — not wrapped |
350
+ | `date` | `any` (raw ISO date string) | not wrapped (`FimoDate` is not emitted — see below) |
351
+ | `relation` | `FimoReference` / `FimoReference[]` | reference object(s) with target `documentId`; use `populate` when embedded target rows are needed |
286
352
 
287
353
  Rules of thumb:
288
354
 
@@ -346,7 +412,7 @@ const { data, meta } = await res.json();
346
412
  // meta: { limit, offset, total, hasMore }
347
413
  ```
348
414
 
349
- 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.
415
+ 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.
350
416
 
351
417
  ## Forms vs. schemas
352
418
 
@@ -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
 
@@ -9,8 +9,8 @@ This file covers the **CLI workflow** for content. Use it when the user wants to
9
9
  ## Flow
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
- 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.
12
+ 2. Run `fimo schemas push {Uid}`. It generates `src/schemas/{Uid}.types.ts` and `src/schemas/{Uid}.ts`, then registers the schema with Fimo. It does not run whole-project validation.
13
+ 3. App code imports the generated client 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
 
@@ -29,6 +29,8 @@ The command is `fimo schemas` (plural). There is no singular `fimo schema` alias
29
29
 
30
30
  All commands accept `-C, --cwd <dir>` to point at a different project directory.
31
31
 
32
+ Schema transitions are automatic. Adding fields and changing labels preserve content. Removing a field removes only that field's stored values. One-to-many relation changes wrap existing references in arrays. Field type changes, scalar/relation changes, relation 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.
33
+
32
34
  ## Entries CLI commands
33
35
 
34
36
  Entries are NOT JSON-file-driven. Create/update them via CLI.
@@ -57,11 +59,13 @@ fimo entries list BlogPost --fields slug,title,excerpt,coverImage --sort -publis
57
59
  fimo entries list BlogPost --fields slug,title,author --populate author
58
60
  ```
59
61
 
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.
62
+ Use the canonical read query flags: `--search`, `--sort`, `--where`, `--limit`, `--offset`, `--fields`, and `--populate`.
63
+ `--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
64
  `--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
65
 
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.
66
+ `--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.
67
+
68
+ 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
69
 
66
70
  For relation schema JSON and entry value shape, load `fimo/references/content.md` § Relation fields. The short version:
67
71
 
@@ -74,18 +78,24 @@ For relation schema JSON and entry value shape, load `fimo/references/content.md
74
78
  }
75
79
  ```
76
80
 
77
- Entry bodies store target `documentId` values:
81
+ Entry bodies store reference objects with target `documentId` values:
78
82
 
79
83
  ```bash
80
- fimo entries create BlogPost --body '{"data":{"slug":"hello","title":"Hello","author":"750e8400-e29b-41d4-a716-446655440000","contributors":[]}}'
84
+ fimo entries create BlogPost --body '{"data":{"slug":"hello","title":"Hello","author":{"documentId":"750e8400-e29b-41d4-a716-446655440000"},"contributors":[]}}'
81
85
  ```
82
86
 
83
- ## Localized content
87
+ Do not add `localized` or field-level `i18n` to relation fields. The relation value belongs to the entry variant that stores it.
88
+
89
+ ## Localized and shared content
84
90
 
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.
91
+ 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
92
 
87
93
  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
94
 
95
+ 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.
96
+
97
+ 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.
98
+
89
99
  ## "Add a blog" recipe
90
100
 
91
101
  1. Load `fimo/references/content.md` for the JSON shape and field types.
@@ -99,7 +109,7 @@ When creating a localized variant of an existing entry, pass the source entry's
99
109
 
100
110
  ## When to run sync manually
101
111
 
102
- The `fimo/vite` plugin auto-regenerates `src/schemas/*.ts` on `vite dev` / `vite build`. If you need to regen outside vite (e.g. CI, scripting), use:
112
+ `fimo schemas push`, `fimo validate`, and the `fimo/vite` plugin all reuse the same schema generator. `schemas push` does not run route, label, agent, form, or project-wide checks. For a full local validation and codegen pass without updating the remote schema, use:
103
113
 
104
114
  ```bash
105
115
  fimo validate # runs all phases: codegen, lint, route validation, label checks
@@ -108,7 +118,7 @@ fimo validate # runs all phases: codegen, lint, route validatio
108
118
  Advanced/internal (hidden from `fimo --help`) — for the bare regen phase:
109
119
 
110
120
  ```bash
111
- fimo scripts sync-schemas # regenerate src/schemas/*.ts only
121
+ fimo scripts sync-schemas # regenerate src/schemas/*.types.ts and src/schemas/*.ts
112
122
  ```
113
123
 
114
124
  ## 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-09T07:06:21.467Z",
5
- "cliVersion": "0.3.5",
4
+ "bundledAt": "2026-07-11T09:31:12.817Z",
5
+ "cliVersion": "0.3.6",
6
6
  "external": [
7
7
  "oxc-parser",
8
8
  "fsevents"