# Resource metadata

Root settings, system fields, resource rules and schema examples.

## .meta file

Defines the structure and behavior of a resource. All fields must be explicitly configured.

```json
{
  "fields": {
    "title": {
      "name": "Title",
      "type": "text",
      "required": true
    },
    "title_slug": {
      "name": "URL slug",
      "type": "slug",
      "source": "title"
    },
    "published": {
      "name": "Published",
      "type": "boolean"
    },
    "body": {
      "name": "Body",
      "type": "html",
      "buttons": "h2,h3,h4,bold,italic,orderedlist,unorderedlist,quote,anchor",
      "media": true,
      "media_sizes": "sm-90,md-70,lg-60,xl-50,xxl-40",
      "admin_col": false
    },
    "sort_order": {
      "name": "Sort order",
      "type": "text",
      "admin_col": false
    }
  },
  "sort": {
    "field": "sort_order",
    "flags": "numeric",
    "order": "asc"
  }
}
```

## Root-level .meta configuration

| Key | Description |
|---|---|
| `sort` | Default sort (`field`, `flags`: `string`/`numeric`, `order`: `asc`/`desc`) |
| `validate` | Validation rules (e.g. `natural-short-text`) |
| `languages` | Enabled languages for this resource |
| `ai_prompts` | Per-field AI translation instructions |
| `name` | Object with optional `singular` and `plural` labels used by `[#resource-name#]`, admin headings, and Nimbly-bar resource entries. Labels fall back to names derived from the resource ID. Example: `{"singular": "Member", "plural": "Members"}`. |
| `nimblybar` | Boolean controlling whether the resource appears in the Nimbly-bar Resources menu. Regular resources are visible by default; `users` and `roles` are hidden by default. Set `true` to include or `false` to hide explicitly. The current user still needs `view-<resource>`. |
| `actions` | Object with optional `view_url` and `url` keys. `view_url` overrides the generic admin View action (default `/nb-admin/{resource}/{uuid}?view=1`); `url` adds the separate “View on site” action. Both support `[#record.<field>#]` substitutions. Example: `{"view_url": "/review/?id=[#record.uuid#]", "url": "[#base-url#]/articles/[#record.title_slug#]"}`. |
| `events` | Resource lifecycle event declarations. Supported keys: `create`, `update`, `delete`. Values are arrays of named events or `job:<type>` queue entries. See below. |
| `splitdir` | Boolean. When `true`, records are stored in a two-level subdirectory tree by UUID prefix for filesystem performance at scale (> ~10,000 records). See §12 API - Scalability. |
| `index` | Array of field names to index. Creates fast lookup paths for those fields. See §4 Indexes below. |
| `upsert` | Boolean. When `true`, `data_update()` (and therefore `PUT /api/v1/{resource}/{uuid}`) creates a missing record instead of failing. Use only for key-value-style resources where "record may not exist yet" is the normal, expected state (e.g. core's `.config`, used for per-page settings) - not for regular content resources, where updating a non-existent record should stay an error. |
| `self_edit` | `users` only. Comma-separated fields a user may save on their own record through `PUT /api/v1/users/{uuid}`. Default: `name`. Anyone who may edit users (`edit-users`, `manage-users`, admin) saves every field. |
| `hidden` | Comma-separated fields the API never returns: not in a list, a single record, the answer to a create or update, or an export. They are still stored and saved as usual; read them in PHP with `data_read()`. Fields named in `encrypt` are always hidden. For `users`, `password`, `salt`, `api`, `password_reset_token` and `change_email_token` are hidden by core, whatever the `.meta` says. |
| `keep_shortcodes` | Boolean. By default a shortcode tag in a value that is written to a record is stored as text (`[#` and `#]` become HTML entities), whoever writes it: the API, an import, an agent, a form handler or `data_create()` / `data_update()` in a library. It then displays as typed and is never run when the value is printed inside another shortcode. Values a record already holds and the `.meta` itself are not touched. Set `true` only for a resource that no template prints and that must hold its text exactly as written; core does so for the job queue and the agent's own records. |
| `history` | Boolean. Whether changes to the resource's records are kept, see Record history below. Default: on for regular resources, off for `users` and for hidden resources other than `.content`, `.config` and `.navigation`. |
| `sitemap` | Lazy sitemap declaration. `url` is a string or language-keyed URL template; optional `published` names a publication field and optional `each` names one array field to expand. Rendering exposes `record`, `language`, and `sitemap_item`. |

## System fields (auto-managed)

Every record automatically has: `uuid`, `_created`, `_modified`, `_created_by`, `_modified_by`. Never define these in `.meta`.

## Resource conventions (required)

These rules are mandatory for all resources unless explicitly stated otherwise.

**Always include:**
- `"required": true` on all name/title fields
- `published` boolean field on content resources
- `sort_order` field + `sort` config on list resources (partners, logos, cards, team members, etc.)
- `"admin_col": false` on images, html, urls, large text, and technical fields like sort_order

**Always consider:**
- Does this resource need a `published` field?
- Does ordering matter? Add `sort_order` + `sort`
- Is this linked from a URL? Add a slug field and list it in `index`
- Does any field need to be unique? Add it to `unique`; a unique field is indexed by itself
- Does the admin overview need a specific column order? Use `admin_columns`, including `_modified` and `_created` when useful
- Does content need to be translated? Add `languages` and `i18n` per field
- Can this be smaller while still fully solving the current requirement?
- Can existing system metadata or a single field index solve this without extra custom fields?

**Admin visibility - fields to hide by default:**
- images
- html content
- urls
- large text fields
- sort_order and other technical fields

## Production-ready .meta examples

### Example A: Partners (list with sorting)

```json
{
  "fields": {
    "title": {
      "name": "Partner name",
      "type": "text",
      "required": true
    },
    "published": {
      "name": "Published",
      "type": "boolean"
    },
    "logo": {
      "name": "Logo",
      "type": "image",
      "admin_col": false
    },
    "website": {
      "name": "Website",
      "type": "url",
      "admin_col": false
    },
    "sort_order": {
      "name": "Sort order",
      "type": "text",
      "admin_col": false
    }
  },
  "sort": {
    "field": "sort_order",
    "flags": "numeric",
    "order": "asc"
  }
}
```

### Example B: Articles (content with date sorting)

```json
{
  "fields": {
    "title": {
      "name": "Title",
      "type": "text",
      "required": true
    },
    "published": {
      "name": "Published",
      "type": "boolean"
    },
    "featured": {
      "name": "Featured",
      "type": "boolean"
    },
    "date": {
      "name": "Date",
      "type": "date"
    },
    "main_img": {
      "name": "Image",
      "type": "image",
      "admin_col": false
    },
    "intro": {
      "name": "Intro",
      "type": "html",
      "buttons": "bold,italic",
      "admin_col": false
    },
    "main_text": {
      "name": "Main text",
      "type": "html",
      "buttons": "h2,h3,h4,bold,italic,orderedlist,unorderedlist,quote,anchor",
      "media": true,
      "media_sizes": "sm-90,md-70,lg-60,xl-50,xxl-40",
      "admin_col": false
    }
  },
  "sort": {
    "field": "date",
    "flags": "string",
    "order": "desc"
  }
}
```

### Example C: Admin overview columns

Use `admin_columns` to control the admin table column order. This can include built-in system columns `_modified` and `_created`.

```json
{
  "admin_columns": ["email", "_modified", "_created"],
  "sort": {
    "field": "_modified",
    "flags": "numeric",
    "order": "desc"
  }
}
```
