> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fimo.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Content & CMS

> Model content with schemas, seed and query entries, and render it with generated hooks.

Fimo's whole point is that content lives in the CMS, not hardcoded in your JSX — so editors can change it in Studio without touching code. You declare **schemas** (content models), create **entries** against them, and render them with generated hooks and `fimo/ui` components.

## The flow

<Steps>
  <Step title="Write the schema JSON">
    Create `src/schemas/{Uid}.json` — PascalCase filename, e.g. `BlogPost.json`. Add fields like `title`, `body` (richtext), and `coverImage` (media). Don't declare a `slug` field — every entry gets one for free.
  </Step>

  <Step title="Push it">
    ```bash theme={null}
    fimo schemas push BlogPost
    ```

    This generates `src/schemas/BlogPost.types.ts` and `src/schemas/BlogPost.ts` (the typed client) and registers the schema with Fimo.
  </Step>

  <Step title="Seed entries">
    ```bash theme={null}
    fimo entries create BlogPost --body '{"data":{"slug":"first-post","title":"Hello"}}'
    ```
  </Step>

  <Step title="Render it">
    Import the generated client from `@/schemas/BlogPost` and render with `fimo/ui` components (`<Text>`, `<Image>`, `<RichText>`). Never write `const posts = [...]` in JSX.
  </Step>
</Steps>

## Schema commands

```bash theme={null}
fimo schemas push [TypeName]     # create or update; omit the name to push all as one atomic batch
fimo schemas list                # list registered schemas
fimo schemas get <TypeName>      # inspect one schema
fimo schemas delete <TypeName>   # remove a schema
```

Schema transitions are automatic — adding fields and changing labels preserve content; changing the schema **is** the migration request (there are no reset or data-loss flags). Run `fimo validate` after pushing to regenerate the client and verify the whole project.

## Choose a text field

| Type       | Editing experience                                                  | Use it for                                                            |
| ---------- | ------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `string`   | One compact line                                                    | Titles, names, labels, slugs, URLs, and short metadata                |
| `text`     | Plain multiline input that starts taller and grows with the content | Summaries, descriptions, bios, excerpts, intros, and details          |
| `richtext` | Structured editor with formatting and content blocks                | Article bodies and page content with headings, links, lists, or media |

`string` and `text` both store plain strings and support the same length, regex, filter, and sort behavior. Changing a field between them preserves its values. Use `richtext` only when the content needs formatting or structured blocks.

## Structure pages with blocks

Use `blocks` for every nested content group. A blocks field is always an array. Set `max: 1` for one fixed group, such as SEO metadata. Use `schema.fields` when every item has the same shape, or `schema.oneOf` when editors choose a variant for each item.

```json theme={null}
{
  "seo": {
    "type": "blocks",
    "max": 1,
    "schema": {
      "fields": {
        "title": { "type": "string", "required": true },
        "description": { "type": "text" }
      }
    }
  },
  "sections": {
    "type": "blocks",
    "min": 1,
    "max": 20,
    "schema": {
      "oneOf": [
        {
          "uid": "hero",
          "label": "Hero",
          "fields": {
            "heading": { "type": "string", "required": true }
          }
        },
        {
          "uid": "quote",
          "label": "Quote",
          "fields": {
            "quote": { "type": "text", "required": true },
            "author": { "type": "reference", "target": "Author" }
          }
        }
      ]
    }
  }
}
```

Entries send fixed groups as arrays too: `"seo": [{ "title": "Home" }]`. Every block has a stable `_key`; a `oneOf` block also carries `_type`, matching its variant `uid`. Nested references return `{ id, documentId }` until populated. Nested population and filtering are not supported yet.

## Entry commands

Use the singular commands for one entry. Bulk create and update also accept JSON files or stdin.

```bash theme={null}
fimo entries create <TypeName> --body '<json>'
fimo entries list <TypeName> [--where JSON] [--sort F] [--fields FIELDS] [--populate FIELDS] [--limit N]
fimo entries get <TypeName> <id>
fimo entries update <TypeName> <id> --body '<json>'
fimo entries delete <TypeName> <id>
fimo entries query --sql '<select>'
fimo entries bulk-create <TypeName> (--body '<json-array>' | --file <path> | --stdin)
fimo entries bulk-update <TypeName> (--ids <ids> | --where '<json>') --body '<json>'
fimo entries bulk-update <TypeName> (--file <path> | --stdin)
fimo entries bulk-delete <TypeName> (--ids <ids> | --where '<json>')
```

`entries list` accepts the same read query surface as the generated hooks — `--search`, `--sort`, `--where`, `--limit`, `--offset`, `--fields`, and `--populate`. `--where` supports operators like `$eq`, `$gte`, `$in`, and `$contains`:

```bash theme={null}
fimo entries list BlogPost --where '{"featured":true}' --sort -publishedAt
fimo entries list BlogPost --fields slug,title,author --populate author
```

### Query entries with SQL

`entries query` requires content view access and accepts one read-only `SELECT` over the `entries` table. Writes and access to other tables are rejected. Common aggregation, JSON, text, date, and window functions are supported. Queries time out after five seconds and return at most 1,000 rows.

<Warning>
  Never use SQL for content writes. It would bypass schema validation, locale and reference handling, content-change
  events, and current or future webhooks. Use the singular or bulk entry mutation commands instead.
</Warning>

```bash theme={null}
fimo entries query --sql "SELECT document_id AS id, data->>'title' AS title FROM entries WHERE content_type = 'BlogPost' LIMIT 20"
fimo entries query --file query.sql
cat query.sql | fimo entries query --stdin
```

<Note>
  SQL exposes the raw representation: `document_id` is the stable ID used by the CLI, content tools, references, and
  API. `id` identifies one exact locale variant. Select `document_id AS id` when results will feed another Fimo command,
  and use `locale` to select a variant.
</Note>

If a safe read is blocked because a SQL function is unavailable, report the missing function to Fimo support rather than working around the restriction with a write.

### Bulk create, update, and delete

Bulk create takes an array of the same bodies accepted by `entries create`:

```bash theme={null}
fimo entries bulk-create BlogPost --body '[{"data":{"title":"First"}},{"data":{"title":"Second"}}]'
fimo entries bulk-create BlogPost --file posts.json
cat posts.json | fimo entries bulk-create BlogPost --stdin
```

Bulk update can apply one regular update body to IDs or to entries matched by `--where`:

```bash theme={null}
fimo entries bulk-update BlogPost \
  --ids <id1>,<id2> \
  --body '{"data":{"status":"published"}}'

fimo entries bulk-update BlogPost \
  --where '{"status":"draft"}' \
  --body '{"data":{"status":"published"}}'
```

For different changes per entry, pass a JSON array through `--file` or `--stdin`:

```json theme={null}
[
  { "id": "<id1>", "body": { "data": { "status": "published" } } },
  { "id": "<id2>", "body": { "data": { "featured": true } } }
]
```

```bash theme={null}
fimo entries bulk-update BlogPost --file updates.json
fimo entries bulk-delete BlogPost --ids <id1>,<id2>
fimo entries bulk-delete BlogPost --where '{"archived":true}'
```

Each bulk request is limited to 1,000 entries. Missing requested IDs do not fail valid work: the CLI prints a warning, while `--json` includes a top-level `warnings` array and the exact IDs in `data.meta.missing`. Bulk writes apply the singular write path one entry at a time. Recoverable create and update errors report how many earlier entries were already changed.

<Note>
  The exact entry `--body` shape — where `slug` goes, how media fields look, and the Tiptap richtext format — is part of
  the project's `fimo` package surface. Load the project `fimo` skill before constructing an entry payload.
</Note>

## Localized, shared, and singleton content

Schemas are localized by default. Omit `--locale` for the default locale; pass `--locale <locale>` (with `--document-id` on `create`) for other locales. Use `"i18n": { "enabled": false }` for content shared across all locales, and `"isSingleton": true` for a type with exactly one document.

## Generated types & hooks

`fimo schemas push` (and `fimo validate`) generate a typed client per schema. App code imports from `@/schemas/{Uid}` — collection hooks like `useList()` / `useInfiniteList()`, `useGet()` for singletons and legacy fixed queries, plus `BlogPostResult` (reads) and `BlogPostInput` (writes) types. Never edit the generated `.ts` files — regenerate them instead.

An entry's `id` stays the same across locale variants. Pass `locale` to select a specific variant; `entryId` identifies only the exact variant returned. `documentId` remains available as a compatibility alias for `id`. Reference inputs use the stable identity, for example `{ "id": author.id }`.

Generated read and mutation hooks use the active Fimo locale when you omit `locale`. This includes `useCreate()`, `useUpdate()`, and `useRemove()`. Pass an explicit `locale` when a mutation must target a different variant.

## What's next

<Columns cols={3}>
  <Card title="Media & AI generation" icon="image" href="/docs/cli/media">
    Add cover images and video.
  </Card>

  <Card title="Labels, locales & i18n" icon="language" href="/docs/cli/labels">
    Wrap static copy in `t()`.
  </Card>

  <Card title="Framework support" icon="layer-group" href="/docs/cli/frameworks">
    Render content with `fimo/ui`.
  </Card>
</Columns>
