fimo 0.12.0 → 0.12.1
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.
- package/assets/agent-templates/broken-link-sweeper/capabilities.yaml +0 -4
- package/assets/agent-templates/content-translator/capabilities.yaml +0 -2
- package/assets/agent-templates/seo-audit/capabilities.yaml +0 -1
- package/assets/content-templates/types-template.eta +13 -7
- package/assets/skills/fimo/SKILL.md +2 -2
- package/assets/skills/fimo-cli/SKILL.md +8 -8
- package/assets/skills/fimo-cli/references/agent-setup.md +1 -1
- package/assets/skills/fimo-cli/references/agents.md +5 -12
- package/assets/skills/fimo-cli/references/assets.md +2 -2
- package/assets/skills/fimo-cli/references/content.md +16 -30
- package/assets/skills/fimo-cli/references/describe.md +1 -1
- package/assets/skills/fimo-cli/references/forms.md +8 -8
- package/assets/skills/fimo-cli/references/init.md +5 -4
- package/assets/skills/fimo-cli/references/labels.md +4 -4
- package/assets/skills/fimo-cli/references/locales.md +3 -3
- package/assets/skills/fimo-cli/references/project-vars-and-secrets.md +1 -1
- package/assets/skills/fimo-code/SKILL.md +40 -50
- package/assets/skills/fimo-code/references/assets.md +6 -4
- package/assets/skills/fimo-code/references/content/clients.md +130 -0
- package/assets/skills/fimo-code/references/content/primitives.md +5 -0
- package/assets/skills/fimo-code/references/content/richtext.md +48 -0
- package/assets/skills/fimo-code/references/content/schema.md +122 -0
- package/assets/skills/fimo-code/references/content.md +12 -597
- package/assets/skills/fimo-code/references/design.md +1 -1
- package/assets/skills/fimo-code/references/frameworks/next/app-router.md +116 -0
- package/assets/skills/fimo-code/references/frameworks/next/locales.md +59 -0
- package/assets/skills/fimo-code/references/frameworks/next/pages-router.md +99 -0
- package/assets/skills/fimo-code/references/frameworks/next.md +116 -0
- package/assets/skills/fimo-code/references/frameworks/react-router.md +295 -0
- package/assets/skills/fimo-code/references/frameworks/vite-react.md +225 -0
- package/assets/skills/fimo-code/references/labels.md +24 -83
- package/assets/skills/fimo-code/references/locales.md +22 -111
- package/assets/skills/fimo-code/references/setup-plain-vite.md +3 -227
- package/assets/skills/fimo-code/references/setup-react-router.md +3 -276
- package/assets/skills/fimo-code/references/ui.md +32 -78
- package/dist/cli/bundle.json +2 -2
- package/dist/cli/index.js +16 -16
- package/dist/scripts/extract-translations.js +8 -1
- package/package.json +1 -1
- package/templates/react-router/AGENTS.md +10 -10
- package/templates/react-router/package.json +1 -1
- package/templates/react-router/react-router.config.ts +1 -1
|
@@ -1,602 +1,17 @@
|
|
|
1
|
-
# Content
|
|
1
|
+
# Content reference map
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Fimo schemas are framework-neutral JSON contracts with framework-specific public modules under `src/schemas/`.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Load only what the task needs:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- `references/content/schema.md` — schema JSON, field types, blocks, references, and safe transitions.
|
|
8
|
+
- `references/content/richtext.md` — Tiptap JSONContent, entry payloads, and slug rules.
|
|
9
|
+
- `references/content/clients.md` — generated collection/singleton clients, queries, projections, population, and locale behavior.
|
|
10
|
+
- `references/ui.md` — source-tracked values and their React renderers.
|
|
11
|
+
- `references/frameworks/next.md` — Next App/Pages Router module boundaries.
|
|
12
|
+
- `references/frameworks/react-router.md` — React Router framework-mode setup and imports.
|
|
13
|
+
- `references/frameworks/vite-react.md` — Vite + React setup and imports.
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
{
|
|
11
|
-
"uid": "BlogPost",
|
|
12
|
-
"name": "Blog Posts",
|
|
13
|
-
"i18n": { "enabled": true },
|
|
14
|
-
"fields": {
|
|
15
|
-
"title": { "type": "string" },
|
|
16
|
-
"summary": { "type": "text" },
|
|
17
|
-
"body": { "type": "richtext" },
|
|
18
|
-
"publishDate": { "type": "string" },
|
|
19
|
-
"featured": { "type": "boolean" },
|
|
20
|
-
"coverImage": { "type": "media" },
|
|
21
|
-
"metadata": { "type": "json" },
|
|
22
|
-
"author": { "type": "reference", "target": "Author" },
|
|
23
|
-
"contributors": { "type": "reference", "target": "Author", "many": true }
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
```
|
|
15
|
+
For schema and content writes, load `fimo-cli/references/content.md`.
|
|
27
16
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
`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 schema-root `i18n` keys are supported.
|
|
31
|
-
|
|
32
|
-
Inside a localized type, an individual top-level scalar or reference field may carry `"i18n": false`. That field then holds one value for the whole document: writing it through any locale updates all of them. Use it for values that are not translated, such as a price, a SKU, a product photo, or a reference that should point at the same target everywhere. Fields inside `blocks` follow the locale of the entry containing them and reject the flag.
|
|
33
|
-
|
|
34
|
-
```json
|
|
35
|
-
{
|
|
36
|
-
"uid": "Product",
|
|
37
|
-
"name": "Products",
|
|
38
|
-
"fields": {
|
|
39
|
-
"title": { "type": "string" },
|
|
40
|
-
"sku": { "type": "string", "i18n": false },
|
|
41
|
-
"price": { "type": "number", "i18n": false }
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
### Reserved field names — do NOT declare these in `fields`
|
|
47
|
-
|
|
48
|
-
Every entry already carries this metadata automatically. Declaring any of them as a
|
|
49
|
-
schema field is rejected by `fimo schemas push` with a 400 (e.g. `fields.slug: "slug" is
|
|
50
|
-
reserved entry metadata and cannot be used as a content field.`):
|
|
51
|
-
|
|
52
|
-
`id`, `entryId`, `documentId`, `slug`, `locale`, `createdAt`, `updatedAt`, `__fimo`.
|
|
53
|
-
|
|
54
|
-
Collection entries expose the effective `locale`. It remains reserved for both localized and shared schemas.
|
|
55
|
-
|
|
56
|
-
In particular **do not add a `slug` field** — every entry gets a `slug` for free. You still
|
|
57
|
-
pass `slug` inside `data` when _creating an entry_ (see "Entry body shape" below); it just
|
|
58
|
-
must not appear in the schema's `fields`. If you need an additional URL-ish string, name it
|
|
59
|
-
something else (e.g. `handle`, `permalink`).
|
|
60
|
-
|
|
61
|
-
## Field types
|
|
62
|
-
|
|
63
|
-
| Type | `data` JSON shape | Use for |
|
|
64
|
-
| ----------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
|
65
|
-
| `string` | `"..."` | One-line text: titles, names, slugs |
|
|
66
|
-
| `text` | `"First line\nSecond line"` | Multiline plain text without formatting |
|
|
67
|
-
| `richtext` | Tiptap JSONContent (see below) | Formatted content with headings, links, lists, or media |
|
|
68
|
-
| `number` | `42` | Counts, quantities |
|
|
69
|
-
| `boolean` | `true` / `false` | Flags |
|
|
70
|
-
| `date` | ISO date string, e.g. `"2026-06-03"` | Publish dates, timestamps |
|
|
71
|
-
| `media` | `{ "source": "local"\|"external", "url": "https://...", "alt": "..." }` | Images, video |
|
|
72
|
-
| `json` | any JSON | Structured metadata |
|
|
73
|
-
| `reference` | `{ "id": "..." }`, `null`, or an array of reference objects when `many: true` | References to another schema |
|
|
74
|
-
| `blocks` | ordered array of objects | Fixed groups, repeated sections, or selectable page blocks |
|
|
75
|
-
|
|
76
|
-
A `date` field generates as `string` in the TS client and wraps in `FimoString` at read time — the same source-tracked shape as `string`/`text` — so `<Date value={post.someDate} />` and plain string methods (`.slice()`, template literals, …) both work directly.
|
|
77
|
-
|
|
78
|
-
## Enumerated strings
|
|
79
|
-
|
|
80
|
-
A `string` field may declare `options` to close its value set. The CMS renders a select, the API validates the value as an enum, and the generated client types the field as a literal union instead of `string`.
|
|
81
|
-
|
|
82
|
-
```json
|
|
83
|
-
"layout": { "type": "string", "options": ["article", "video"], "default": "article" }
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
- Values must be unique. Set a `default` that is one of them.
|
|
87
|
-
- `options` cannot be combined with `regex`, `minLength`, or `maxLength`, and is rejected on every field type other than `string`.
|
|
88
|
-
- Writes are validated against the set: a value outside it is rejected with a 400. An optional field also accepts `""` and `null` so the editor can clear the choice; a `required` one only accepts a declared value.
|
|
89
|
-
- Removing an option that entries still hold fails the push, the same way an unsafe type change does. Migrate those entries to a remaining value first.
|
|
90
|
-
|
|
91
|
-
## Conditional fields
|
|
92
|
-
|
|
93
|
-
A field may declare `showIf` (every clause must match) or `showIfAny` (one clause is enough) to apply only in some entries. A clause names a sibling field and a value.
|
|
94
|
-
|
|
95
|
-
```json
|
|
96
|
-
{
|
|
97
|
-
"layout": { "type": "string", "options": ["article", "video"], "default": "article" },
|
|
98
|
-
"body": { "type": "richtext", "showIf": { "field": "layout", "equals": "article" } },
|
|
99
|
-
"videoUrl": { "type": "string", "required": true, "showIf": { "field": "layout", "equals": "video" } },
|
|
100
|
-
"transcript": {
|
|
101
|
-
"type": "text",
|
|
102
|
-
"showIfAny": [
|
|
103
|
-
{ "field": "layout", "equals": "video" },
|
|
104
|
-
{ "field": "hasAudio", "equals": true }
|
|
105
|
-
]
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
- A clause is `{ "field": "<sibling>", "equals": <value> }` or `{ "field": "<sibling>", "in": [<values>] }`. Use `in` for several values of one field.
|
|
111
|
-
- `showIf` accepts one clause or an array of clauses, and every clause must match. `showIfAny` takes an array and one match is enough. A field declares one key or the other, never both.
|
|
112
|
-
- **A hidden field is not rendered and not validated, so `required` does not apply to it.** An entry whose condition never held can be saved without ever filling the field.
|
|
113
|
-
- **Hiding a field changes nothing about storage or reads.** The value stays in the entry and the API keeps returning it, so an editor who flips the trigger back gets their work back. Narrow on the trigger in site code rather than trusting the value.
|
|
114
|
-
- Because a conditional field can be absent, it is always optional in the generated types even when `required` is set.
|
|
115
|
-
- Conditions resolve against siblings in the same object. A field inside a block variant may only reference fields in that same block item.
|
|
116
|
-
- A clause may reference a `boolean`, `number`, or `string` field. A `string` trigger must declare `options`, which is what lets `fimo validate` reject a clause value that could never match.
|
|
117
|
-
- A trigger cannot itself be conditional — conditions are one level deep, so a chain like A shows B shows C is rejected.
|
|
118
|
-
- `fimo validate` and `fimo schemas push` reject an unknown trigger name, a trigger of the wrong type, a value outside the trigger's `options`, and a trigger that is itself conditional.
|
|
119
|
-
|
|
120
|
-
## Blocks
|
|
121
|
-
|
|
122
|
-
`blocks` is the only nested structured-content type and always stores an array. Use `schema.fields` for one homogeneous block shape. Set `max: 1` when the field is a single optional group. Use `schema.oneOf` when editors choose a variant for each item.
|
|
123
|
-
|
|
124
|
-
```json
|
|
125
|
-
{
|
|
126
|
-
"seo": {
|
|
127
|
-
"type": "blocks",
|
|
128
|
-
"max": 1,
|
|
129
|
-
"schema": {
|
|
130
|
-
"fields": {
|
|
131
|
-
"title": { "type": "string", "required": true },
|
|
132
|
-
"description": { "type": "text" }
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
"sections": {
|
|
137
|
-
"type": "blocks",
|
|
138
|
-
"min": 1,
|
|
139
|
-
"max": 20,
|
|
140
|
-
"schema": {
|
|
141
|
-
"oneOf": [
|
|
142
|
-
{
|
|
143
|
-
"uid": "hero",
|
|
144
|
-
"label": "Hero",
|
|
145
|
-
"fields": {
|
|
146
|
-
"heading": { "type": "string", "required": true },
|
|
147
|
-
"introduction": { "type": "text" }
|
|
148
|
-
}
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
"uid": "quote",
|
|
152
|
-
"label": "Quote",
|
|
153
|
-
"fields": {
|
|
154
|
-
"quote": { "type": "text", "required": true },
|
|
155
|
-
"author": { "type": "reference", "target": "Author" }
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
]
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
- `blocks.schema.fields` repeats one homogeneous shape.
|
|
165
|
-
- `blocks.schema.oneOf` lets the editor choose exactly one variant per item.
|
|
166
|
-
- Blocks schemas support at most 7 nested `blocks` levels, 40 variants in one `schema.oneOf`, and `min` or `max` values up to 200 items per blocks field.
|
|
167
|
-
- Blocks always read and write arrays. A field with `max: 1` is `[]` or `[item]`, never a bare object.
|
|
168
|
-
- `min` and `max` constrain the total number of blocks. Use `min: 1` to require at least one block; `required: true` has the same minimum-one behavior.
|
|
169
|
-
- Fimo creates a stable `_key` for each item. A `oneOf` item also carries `_type`, matching the variant `uid`.
|
|
170
|
-
- Nested fields support the same defaults and validators as top-level fields.
|
|
171
|
-
- **Each block item's own scalar fields wrap the same way their field type wraps at the top level** — a nested `string`/`text` field is a `FimoString`, a nested `richtext` field is a `FimoRichText`, a nested `media` field is a `FimoMedia`, a nested `boolean` is a `FimoBoolean`, a nested `date` is a `FimoString`, a nested `json` field is a shallow-cloned, source-tagged object, a nested `number` stays a plain `number`, and a nested `blocks` field recurses the same way. Pass those nested values to the matching `fimo/ui` component exactly like a top-level field — `<Text value={section.heading} />` or `<Json value={section.metadata} />` inside a `.map()` over `page.sections` stays inline-editable.
|
|
172
|
-
- Blocks inherit the owning entry's locale.
|
|
173
|
-
- Nested references use one fixed `target`. Unpopulated reads return `{ id, documentId }`, like top-level references.
|
|
174
|
-
- Nested population and filtering are not supported yet.
|
|
175
|
-
- `anyOf` is not supported. One block never merges multiple variant schemas.
|
|
176
|
-
|
|
177
|
-
## Reference fields
|
|
178
|
-
|
|
179
|
-
Use `reference` when one content entry references another content entry. Do not store entry IDs in a `json` field as a workaround.
|
|
180
|
-
|
|
181
|
-
```json
|
|
182
|
-
{
|
|
183
|
-
"uid": "BlogPost",
|
|
184
|
-
"name": "Blog Posts",
|
|
185
|
-
"fields": {
|
|
186
|
-
"title": { "type": "string" },
|
|
187
|
-
"author": { "type": "reference", "target": "Author" },
|
|
188
|
-
"contributors": { "type": "reference", "target": "Author", "many": true }
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
Rules:
|
|
194
|
-
|
|
195
|
-
- `target` is required and must be the PascalCase schema `uid` of the referenced content type.
|
|
196
|
-
- Omit `many` for a to-one reference. Use `"many": true` for a to-many reference.
|
|
197
|
-
- Self-references are allowed, e.g. a `Category` schema can have `parent: { "type": "reference", "target": "Category" }`.
|
|
198
|
-
- Entry values use the target's stable `id`, never the variant `entryId`: `{ "id": "..." }`.
|
|
199
|
-
- Do not include `contentType` for normal fixed-target references; the schema's `target` already defines it.
|
|
200
|
-
- `localized` is not a supported spelling. A reference may carry `"i18n": false` so every locale of the document points at the same target; without it the reference belongs to the entry variant that stores it.
|
|
201
|
-
- Use `null` for an empty to-one reference and `[]` for an empty to-many reference.
|
|
202
|
-
- The API validates that referenced target documents exist and rejects duplicate values in to-many reference arrays.
|
|
203
|
-
|
|
204
|
-
Entry payload example:
|
|
205
|
-
|
|
206
|
-
```json
|
|
207
|
-
{
|
|
208
|
-
"data": {
|
|
209
|
-
"slug": "reference-example",
|
|
210
|
-
"title": "Reference example",
|
|
211
|
-
"author": { "id": "750e8400-e29b-41d4-a716-446655440000" },
|
|
212
|
-
"contributors": [{ "id": "750e8400-e29b-41d4-a716-446655440000" }]
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
## Schema transitions
|
|
218
|
-
|
|
219
|
-
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:
|
|
220
|
-
|
|
221
|
-
- Adding optional fields and changing labels preserve entries.
|
|
222
|
-
- Adding a required field backfills its `default` only in existing objects that need it. The push fails safely if those objects exist and no valid default is provided.
|
|
223
|
-
- Removing a field or block variant removes only those nested values.
|
|
224
|
-
- A one-to-many reference change wraps each existing reference in an array.
|
|
225
|
-
- Changing between `string` and `text` preserves values.
|
|
226
|
-
- Other primitive type changes convert safe values, such as `42` to `"42"`, and reject unsafe values without changing content or schema.
|
|
227
|
-
- Lowering blocks `max` trims items from the end. Raising `min` appends blocks from the field's non-empty `default` array.
|
|
228
|
-
- Changing a reference target clears those connections. Renaming a relation field with the same target preserves them.
|
|
229
|
-
- Preserve top-level or nested renames with repeatable one-time operations: `fimo schemas push Page --rename sections.hero.heading=sections.hero.title`. Renames are command intent, not schema JSON.
|
|
230
|
-
- A many-to-one change or scalar/reference change clears only the changed field.
|
|
231
|
-
- Collection-to-Single removes that type's entries and cleans references to them.
|
|
232
|
-
- Singleton-to-collection preserves the existing entry.
|
|
233
|
-
- Blocks migrations are surgical: unaffected nested fields, block keys, and block order survive every supported change.
|
|
234
|
-
|
|
235
|
-
There are no reset or data-loss flags. A failed migration is atomic: neither schema nor entries change.
|
|
236
|
-
|
|
237
|
-
Choose by editing intent:
|
|
238
|
-
|
|
239
|
-
- Use `string` for one-line values such as titles, names, labels, slugs, URLs, and short metadata.
|
|
240
|
-
- Use `text` for plain multiline copy such as summaries, descriptions, bios, excerpts, intros, and details when formatting is unnecessary.
|
|
241
|
-
- Use `richtext` for content that needs formatting or structured blocks, especially article bodies and page content.
|
|
242
|
-
|
|
243
|
-
## Tiptap JSONContent (richtext)
|
|
244
|
-
|
|
245
|
-
Richtext values MUST be Tiptap JSONContent — never raw HTML strings:
|
|
246
|
-
|
|
247
|
-
```json
|
|
248
|
-
{
|
|
249
|
-
"type": "doc",
|
|
250
|
-
"content": [
|
|
251
|
-
{ "type": "heading", "attrs": { "level": 1 }, "content": [{ "type": "text", "text": "Title" }] },
|
|
252
|
-
{
|
|
253
|
-
"type": "paragraph",
|
|
254
|
-
"content": [
|
|
255
|
-
{ "type": "text", "text": "Hello " },
|
|
256
|
-
{ "type": "text", "marks": [{ "type": "bold" }], "text": "world" }
|
|
257
|
-
]
|
|
258
|
-
}
|
|
259
|
-
]
|
|
260
|
-
}
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
Available nodes: `doc`, `paragraph`, `heading` (levels 1-6; prefer 1-3 for hierarchy), `bulletList`, `orderedList`, `listItem`, `blockquote`, `codeBlock`, `horizontalRule`, `hardBreak` (renders as `<br/>`), `table`, `tableRow`, `tableHeader`, `tableCell`, `image` (`attrs.src`, `attrs.alt`), `iframe` (`attrs.src` — use embed URLs like `https://www.youtube.com/embed/<ID>`).
|
|
264
|
-
|
|
265
|
-
Marks: `bold`, `italic`, `underline`, `strike`, `code`, `link`.
|
|
266
|
-
|
|
267
|
-
**Use the full range of block types** when generating richtext — `<RichText>` styles them all out of the box:
|
|
268
|
-
|
|
269
|
-
- `codeBlock` for code snippets (bordered monospace container)
|
|
270
|
-
- `blockquote` for quotes and callouts (left-border accent)
|
|
271
|
-
- `horizontalRule` to visually separate sections
|
|
272
|
-
- `table` / `tableRow` / `tableHeader` / `tableCell` for structured data
|
|
273
|
-
- `bulletList` / `orderedList` for lists
|
|
274
|
-
- `heading` (levels 1-6; prefer 1-3) to build hierarchy — don't render long-form content as a single wall of `paragraph` nodes.
|
|
275
|
-
|
|
276
|
-
## Entry body shape
|
|
277
|
-
|
|
278
|
-
Entries are CLI-driven (see `fimo-cli/content.md`). The body shape passed to `fimo entries create --body '...'` is:
|
|
279
|
-
|
|
280
|
-
```json
|
|
281
|
-
{
|
|
282
|
-
"data": {
|
|
283
|
-
"slug": "my-first-post",
|
|
284
|
-
"title": "My First Post",
|
|
285
|
-
"body": { "type": "doc", "content": [...] },
|
|
286
|
-
"coverImage": { "source": "local", "url": "...", "alt": "..." }
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
**`slug` lives INSIDE `data`**, alongside every other field.
|
|
292
|
-
|
|
293
|
-
> The API validator strips any top-level keys other than `data` — if you put `slug` at the top level it is silently dropped and the backend generates a random one. Always nest it inside `data`.
|
|
294
|
-
|
|
295
|
-
## Slug rules
|
|
296
|
-
|
|
297
|
-
Always provide a meaningful `slug` when creating an entry — it appears in URLs. The server will `slugify()` whatever you send (lowercase, hyphen separators, strip special chars), so you can pass a clean candidate straight through.
|
|
298
|
-
|
|
299
|
-
- **Format**: lowercase, hyphen-separated, ASCII, no special characters (e.g. `my-first-blog-post`).
|
|
300
|
-
- **Derive from** the entry's main human-readable field: `title` for posts/pages, `name` for team members/products.
|
|
301
|
-
- **Concise and descriptive** — max 255 chars; aim for under 60 for readability.
|
|
302
|
-
- **Unique per content type** — two `BlogPost` entries can't share a slug. If seeding duplicates, suffix (`my-post`, `my-post-2`).
|
|
303
|
-
- **Stable** — once an entry is live, changing the slug breaks inbound links. Only update intentionally.
|
|
304
|
-
- If you omit `slug`, the backend generates a random one — never rely on this for content you want to link to.
|
|
305
|
-
|
|
306
|
-
For `media` fields, prefer `source: "local"` with assets uploaded via `fimo assets upload` or generated with a Fimo media task such as `fimo image generate` (see `fimo-cli/references/media.md`).
|
|
307
|
-
|
|
308
|
-
## Generated React Query hooks
|
|
309
|
-
|
|
310
|
-
After pre-build (or `fimo deploy`), each schema generates two files:
|
|
311
|
-
|
|
312
|
-
- `src/schemas/{Uid}.types.ts` — pure TypeScript types such as `BlogPost`, `BlogPostInput`, `BlogPostQuery`, and `BlogPostResult`.
|
|
313
|
-
- `src/schemas/{Uid}.ts` — exposes runtime helpers, React Query hooks, and public types.
|
|
314
|
-
|
|
315
|
-
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.
|
|
316
|
-
|
|
317
|
-
**Entity shape — fields live at the top level of the entry**, not under a nested `data` key. The generated `BlogPost` interface looks like:
|
|
318
|
-
|
|
319
|
-
```ts
|
|
320
|
-
interface BlogPost {
|
|
321
|
-
id: string;
|
|
322
|
-
entryId: string;
|
|
323
|
-
documentId: string;
|
|
324
|
-
slug: FimoString;
|
|
325
|
-
locale: string;
|
|
326
|
-
createdAt: string;
|
|
327
|
-
updatedAt: string;
|
|
328
|
-
__fimo: {
|
|
329
|
-
contentType: string;
|
|
330
|
-
};
|
|
331
|
-
title: FimoString;
|
|
332
|
-
summary: FimoString;
|
|
333
|
-
body?: FimoRichText | null;
|
|
334
|
-
coverImage: FimoMedia;
|
|
335
|
-
publishDate: FimoString;
|
|
336
|
-
featured: FimoBoolean;
|
|
337
|
-
// ...
|
|
338
|
-
}
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
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`.
|
|
342
|
-
|
|
343
|
-
Optional richtext fields are nullable in generated read and write types. `<RichText>` accepts the generated value directly and renders nothing when it is null or undefined. A non-conditional required richtext field stays non-null; conditional fields remain optional because their condition may not apply.
|
|
344
|
-
|
|
345
|
-
## Localized and shared content
|
|
346
|
-
|
|
347
|
-
Localized content has one variant per locale. Generated hooks read the active Fimo locale automatically.
|
|
348
|
-
|
|
349
|
-
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.
|
|
350
|
-
|
|
351
|
-
For shared content, set `i18n: { "enabled": false }` on the schema and create each entry once. Shared entries:
|
|
352
|
-
|
|
353
|
-
- send and expose the effective active/default locale in generated collection queries and models;
|
|
354
|
-
- omit `locale` from raw API responses because there is no localized variant;
|
|
355
|
-
- are returned in every locale context without duplication;
|
|
356
|
-
- ignore locale on owner reads and writes;
|
|
357
|
-
- still use the active/requested locale when a populated reference targets a localized schema.
|
|
358
|
-
|
|
359
|
-
Do not duplicate shared entries across locales. This section is about a whole type being shared; a single shared field inside a localized type is the separate `"i18n": false` field flag described above.
|
|
360
|
-
|
|
361
|
-
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.
|
|
362
|
-
|
|
363
|
-
**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:
|
|
364
|
-
|
|
365
|
-
```ts
|
|
366
|
-
for (const item of page.featured) {
|
|
367
|
-
switch (item.$type) {
|
|
368
|
-
case 'Post':
|
|
369
|
-
return item.title; // typed as Post here
|
|
370
|
-
case 'Video':
|
|
371
|
-
return item.clips; // typed as Video here
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
Filtering on polymorphic fields is not supported yet.
|
|
377
|
-
|
|
378
|
-
**Generated API:**
|
|
379
|
-
|
|
380
|
-
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.
|
|
381
|
-
|
|
382
|
-
```ts
|
|
383
|
-
// low-level fetch helpers
|
|
384
|
-
getById<TPopulate extends BlogPostPopulate | undefined>(id: string, options?: { locale?: string; populate?: TPopulate }): Promise<BlogPostResult<undefined, TPopulate>>
|
|
385
|
-
getBySlug<TPopulate extends BlogPostPopulate | undefined>(slug: string, options?: { populate?: TPopulate }): Promise<BlogPostResult<undefined, TPopulate> | null>
|
|
386
|
-
getByField<TPopulate extends BlogPostPopulate | undefined>(fieldName: string, value: string, options?: { populate?: TPopulate }): Promise<BlogPostResult<undefined, TPopulate> | null>
|
|
387
|
-
list<TFields extends BlogPostFields | undefined, TPopulate extends BlogPostPopulate | undefined>(params?: BlogPostQuery<TFields, TPopulate>): Promise<CollectionPage<BlogPostResult<TFields, TPopulate>>>
|
|
388
|
-
get<TFields extends BlogPostFields | undefined, TPopulate extends BlogPostPopulate | undefined>(params?: BlogPostQuery<TFields, TPopulate>): Promise<BlogPostResult<TFields, TPopulate>[]>
|
|
389
|
-
create(payload: BlogPostInput): Promise<BlogPost>
|
|
390
|
-
update(id: string, payload: Partial<BlogPostInput>): Promise<BlogPost>
|
|
391
|
-
remove(id: string, options?: { locale?: string }): Promise<void>
|
|
392
|
-
|
|
393
|
-
// react-query hooks
|
|
394
|
-
useGetById(id, { locale?, populate? }) // → { data: BlogPostResult<undefined, TPopulate> | undefined, isLoading, ... }
|
|
395
|
-
useGetBySlug(slug, { populate? }) // → { data: BlogPostResult<undefined, TPopulate> | null | undefined, ... } — ideal for /:slug detail routes
|
|
396
|
-
useGetByField(fieldName, value, { populate? })
|
|
397
|
-
useList<TFields, TPopulate>(params?) // → { data: CollectionPage<BlogPostResult<TFields, TPopulate>> | undefined, ... }
|
|
398
|
-
useInfiniteList<TFields, TPopulate>(params?) // → { items, total, hasMore, loadMore, isLoadingMore, ... }
|
|
399
|
-
useGet<TFields, TPopulate>(params?) // → legacy array-shaped { data: BlogPostResult<TFields, TPopulate>[] | undefined, ... }
|
|
400
|
-
useCreate() / useUpdate() / useRemove()
|
|
401
|
-
|
|
402
|
-
// Suspense variants (same signatures, throw to a <Suspense> boundary instead of returning isLoading)
|
|
403
|
-
useSuspenseGetById(id) / useSuspenseGetBySlug(slug) / useSuspenseGetByField(fieldName, value) / useSuspenseGet(params?)
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
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`.
|
|
407
|
-
|
|
408
|
-
For a schema with `"isSingleton": true`, codegen omits collection methods and generates this smaller API:
|
|
409
|
-
|
|
410
|
-
```ts
|
|
411
|
-
get<TPopulate extends SettingsPopulate | undefined>(options?: { locale?: string; populate?: TPopulate }): Promise<SettingsResult<undefined, TPopulate>>
|
|
412
|
-
update(payload: Partial<SettingsInput>): Promise<Settings>
|
|
413
|
-
useGet(options?) / useSuspenseGet(options?) / useUpdate()
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
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.
|
|
417
|
-
|
|
418
|
-
Choose the collection interface from the UI behavior. Use `useList` when the UI controls a page, and read pagination state from `pageInfo`:
|
|
419
|
-
|
|
420
|
-
```tsx
|
|
421
|
-
const { data: page, isLoading } = BlogPost.useList({ sort: '-publishedAt', limit: 12, offset: 24 });
|
|
422
|
-
if (isLoading || !page) return null;
|
|
423
|
-
|
|
424
|
-
page.items.map((post) => /* ... */);
|
|
425
|
-
page.pageInfo; // { limit, offset, total, hasMore, nextOffset }
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
Use `useInfiniteList` for load-more and infinite-scroll views. It accumulates pages and exposes one typed `items` array, so UI code does not manage offsets or flatten pages:
|
|
429
|
-
|
|
430
|
-
```tsx
|
|
431
|
-
const {
|
|
432
|
-
items: posts,
|
|
433
|
-
hasMore,
|
|
434
|
-
loadMore,
|
|
435
|
-
isLoadingMore,
|
|
436
|
-
} = BlogPost.useInfiniteList({
|
|
437
|
-
sort: '-publishedAt',
|
|
438
|
-
limit: 12,
|
|
439
|
-
});
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
`get()` and `useGet()` remain available as legacy array-shaped helpers for small, deliberately bounded subsets:
|
|
443
|
-
|
|
444
|
-
```tsx
|
|
445
|
-
const { data: featured = [] } = BlogPost.useGet({
|
|
446
|
-
where: { featured: true },
|
|
447
|
-
limit: 3,
|
|
448
|
-
});
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
Query params supported: `limit` (1-100, default 20), `offset` (default 0), `search`, `sort`, `where`, `fields`, and `populate`. Use these canonical names in new code.
|
|
452
|
-
`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.
|
|
453
|
-
|
|
454
|
-
`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:
|
|
455
|
-
|
|
456
|
-
```ts
|
|
457
|
-
BlogPost.useGet({
|
|
458
|
-
where: {
|
|
459
|
-
featured: true,
|
|
460
|
-
category: 'tech',
|
|
461
|
-
views: { $gte: 100 },
|
|
462
|
-
title: { $contains: 'React' },
|
|
463
|
-
coverImage: { $null: false },
|
|
464
|
-
__fimo: { contentType: { $eq: 'BlogPost' } },
|
|
465
|
-
},
|
|
466
|
-
sort: ['-publishedAt', 'title'],
|
|
467
|
-
limit: 12,
|
|
468
|
-
});
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
Operators: scalar equality, `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$contains`, `$startsWith`, `$null`.
|
|
472
|
-
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".
|
|
473
|
-
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.
|
|
474
|
-
|
|
475
|
-
Use `fields` to keep list payloads small. The API always keeps `id`, `entryId`, `documentId`, and `locale`, and returns only the requested fields beyond that:
|
|
476
|
-
|
|
477
|
-
```tsx
|
|
478
|
-
const { data: summaries = [] } = BlogPost.useGet({
|
|
479
|
-
fields: ['slug', 'title', 'excerpt', 'coverImage'] as const,
|
|
480
|
-
sort: '-publishedAt',
|
|
481
|
-
limit: 12,
|
|
482
|
-
});
|
|
483
|
-
// summaries: Array<Pick<BlogPost, 'id' | 'entryId' | 'documentId' | 'locale' | 'slug' | 'title' | 'excerpt' | 'coverImage'>>
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
Use `populate` to embed reference targets in read results:
|
|
487
|
-
|
|
488
|
-
```tsx
|
|
489
|
-
const { data: posts = [] } = BlogPost.useGet({
|
|
490
|
-
fields: ['slug', 'title', 'author'] as const,
|
|
491
|
-
populate: ['author'] as const,
|
|
492
|
-
});
|
|
493
|
-
// Without populate: post.author is { id: "..." }.
|
|
494
|
-
// With populate: post.author is the target Author entry object, or null.
|
|
495
|
-
```
|
|
496
|
-
|
|
497
|
-
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:
|
|
498
|
-
|
|
499
|
-
```ts
|
|
500
|
-
import type { BlogPostResult } from '@/schemas/BlogPost';
|
|
501
|
-
|
|
502
|
-
type PostWithAuthor = BlogPostResult<undefined, 'author'>;
|
|
503
|
-
type PostCardItem = BlogPostResult<
|
|
504
|
-
readonly ['slug', 'title', 'author', 'contributors'],
|
|
505
|
-
readonly ['author', 'contributors']
|
|
506
|
-
>;
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
Do not type populated hook data as `BlogPost[]`: plain `BlogPost` keeps reference fields as `FimoReference` objects, while `BlogPostResult<TFields, TPopulate>` models the projected/populated read result.
|
|
510
|
-
|
|
511
|
-
`BlogPostPopulate` is the generated input type for allowed populate values. It comes from the schema's reference 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.
|
|
512
|
-
|
|
513
|
-
Populate is depth-one. It only works for reference fields; trying to populate a scalar field returns 400. To-many reference order is preserved.
|
|
514
|
-
|
|
515
|
-
`list()` and `useList()` surface pagination metadata as `pageInfo`. `useInfiniteList()` consumes `nextOffset` internally and exposes `hasMore` plus `loadMore()`.
|
|
516
|
-
|
|
517
|
-
## Fimo primitives (what the field values are)
|
|
518
|
-
|
|
519
|
-
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:
|
|
520
|
-
|
|
521
|
-
| Schema field type | Field value you get | Notes |
|
|
522
|
-
| ----------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
523
|
-
| `string` | `FimoString` | `extends String` — **not** a primitive `string` |
|
|
524
|
-
| `text` | `FimoString` | Multiline plain text with the same source tracking as `string` |
|
|
525
|
-
| `richtext` | `FimoRichText` | Tiptap JSONContent + source metadata |
|
|
526
|
-
| `boolean` | `FimoBoolean` | class; `.valueOf()` → `boolean` |
|
|
527
|
-
| `media` | `FimoMedia` / `FimoMedia[]` | shape + rendering in `references/assets.md` |
|
|
528
|
-
| `number` | `number` | plain — not wrapped. A wrapper would break `<span>{post.views}</span>` (React renders a boxed `String` directly but throws on a boxed `Number`), so it stays untracked and unclickable in the editing surfaces |
|
|
529
|
-
| `json` | `Record<string, any>` | the same shape you wrote, plus a hidden (non-enumerable, symbol-keyed) source tag — render with `<Json>`, see below |
|
|
530
|
-
| `date` | `FimoString` | same wrapper as `string`/`text`, **not** `FimoDate` — see below |
|
|
531
|
-
| `blocks` | array of plain objects | the array/objects themselves are plain; each item's own scalar fields are wrapped by their own field type (a nested `string` is still `FimoString`, a nested `number` stays a plain `number`, …) — see "Blocks" above |
|
|
532
|
-
| `reference` | `FimoReference` / `FimoReference[]` | reference object(s) with target `id`; use `populate` when embedded targets are needed |
|
|
533
|
-
|
|
534
|
-
Rules of thumb:
|
|
535
|
-
|
|
536
|
-
- **Pass the whole primitive to the matching `fimo/ui` component** — `<Text value={post.title} />`, `<Image value={post.cover} />`, `<RichText value={post.body} />`, `<Boolean value={p.inStock} />`, `<Date value={post.publishDate} />`, `<Json value={post.metadata} />`. Don't unwrap first; the component reads the source metadata so the field stays inline-editable in the dashboard.
|
|
537
|
-
- **`FimoString extends String`**, so `typeof post.title === 'object'` and `post.title === 'Hi'` is `false`. It stringifies fine inside JSX and template literals; call `.toString()` only when something genuinely needs a primitive `string`.
|
|
538
|
-
- **`number` fields are the one scalar type left unwrapped** — `post.views` is a plain `number`, so `{post.views}` renders directly in JSX, `typeof`/`===`/arithmetic all behave normally, and there is no `<Number>` component. This is deliberate, not a gap: wrapping it would break direct JSX rendering in every existing site.
|
|
539
|
-
- **`json` fields are a shallow clone of what you wrote**, tagged with a hidden source symbol — not the original object, and not a class instance. Reading it behaves exactly like plain JSON (`JSON.stringify` round-trips it unchanged; `for...in`/`Object.keys` see only your own keys). Render it with `<Json value={post.metadata} />` (default renders as a `<span>`; pass `as="pre"` for a formatted block, or a children render function for specific keys) so it stays inline-editable and live-updates in the dashboard preview like every other field.
|
|
540
|
-
- **Never hand-construct** a `FimoString` / `FimoMedia` / etc. — that bypasses source tracking. For a literal string use `t()`; for a literal image use `<StaticImage>`.
|
|
541
|
-
- `t()` also returns a `FimoString` (see `references/labels.md`).
|
|
542
|
-
|
|
543
|
-
`FimoMedia` and `FimoBoolean` shapes:
|
|
544
|
-
|
|
545
|
-
```ts
|
|
546
|
-
class FimoMedia {
|
|
547
|
-
readonly url: string;
|
|
548
|
-
readonly alt: string;
|
|
549
|
-
readonly name: string;
|
|
550
|
-
readonly mime: string;
|
|
551
|
-
readonly width?: number;
|
|
552
|
-
readonly height?: number;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
class FimoBoolean {
|
|
556
|
-
readonly value: boolean;
|
|
557
|
-
valueOf(): boolean;
|
|
558
|
-
}
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
> **`FimoDate`:** exported from `fimo/ui` (it's one of the `<Date>` component's accepted value types), but the generated schema client does **not** wrap `date` fields in it — a `date` field wraps as `FimoString` instead (see the table above), because the generated TypeScript type for a `date` field is `string`, and `FimoString` is what keeps that contract's string methods (`.slice()`, `.includes()`, …) working. `<Date value={post.someDate} />` accepts a `FimoString`, a `FimoDate`, a raw `Date`, or a raw string — all four render correctly.
|
|
562
|
-
|
|
563
|
-
## UI code pattern
|
|
564
|
-
|
|
565
|
-
```tsx
|
|
566
|
-
import { useList } from '@/schemas/BlogPost';
|
|
567
|
-
import { Text, RichText, Image, Date } from 'fimo/ui';
|
|
568
|
-
|
|
569
|
-
export function BlogList() {
|
|
570
|
-
const { data: page, isLoading } = useList({ limit: 10 });
|
|
571
|
-
if (isLoading || !page) return null;
|
|
572
|
-
return (
|
|
573
|
-
<ul>
|
|
574
|
-
{page.items.map((post) => (
|
|
575
|
-
<li key={`${post.id}:${post.locale}`}>
|
|
576
|
-
<Text value={post.title} as="h2" />
|
|
577
|
-
<Image value={post.coverImage} width={800} height={400} fit="cover" />
|
|
578
|
-
<RichText value={post.body} className="prose" />
|
|
579
|
-
<Date value={post.publishDate} />
|
|
580
|
-
</li>
|
|
581
|
-
))}
|
|
582
|
-
</ul>
|
|
583
|
-
);
|
|
584
|
-
}
|
|
585
|
-
```
|
|
586
|
-
|
|
587
|
-
For the `<Text>` / `<RichText>` / `<Image>` / `<Date>` / `<Json>` components and the rules of thumb, see `references/ui.md`.
|
|
588
|
-
|
|
589
|
-
## Read path (published app, no auth)
|
|
590
|
-
|
|
591
|
-
```ts
|
|
592
|
-
const res = await fetch(`${import.meta.env.VITE_API_URL}/entries/<typeName>?locale=en&fields=slug,title&limit=12`);
|
|
593
|
-
const { data, meta } = await res.json();
|
|
594
|
-
// data: Entry[] — fields flattened at top level (id, entryId, documentId, locale, slug, title, ...)
|
|
595
|
-
// meta: { limit, offset, total, hasMore }
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
Prefer the generated `useList` or `useInfiniteList` hooks from `@/schemas/{Uid}` over hand-rolled fetches. They preserve projected/populated item types, cache results, and expose the pagination state regular list UIs need. Use the raw endpoint only for lower-level integrations outside the generated client.
|
|
599
|
-
|
|
600
|
-
## Forms vs. schemas
|
|
601
|
-
|
|
602
|
-
Schemas = **editor-managed content** (blog posts, pages). Forms = **end-user input** (contact, signup). See `references/forms.md` for forms.
|
|
17
|
+
Never edit generated TypeScript. Follow `references/content/schema.md` for schema changes and load the detected framework reference before choosing generated module imports.
|
|
@@ -4,7 +4,7 @@ Distinctive, production-grade frontend interfaces with high design quality. Avoi
|
|
|
4
4
|
|
|
5
5
|
Skip for: bug fixes, refactors, copy changes, or tasks where the user has already specified visual details (exact fonts, colors, layout).
|
|
6
6
|
|
|
7
|
-
For React
|
|
7
|
+
For source-tracked React primitives, see `references/ui.md` plus the detected framework reference. Preserve the application's existing styling, routing, and component conventions. This file is about aesthetic and creative direction.
|
|
8
8
|
|
|
9
9
|
## Brief first, code second
|
|
10
10
|
|