---
title: "Content and schemas"
description: "Frontmatter, attribute types, validation and queries."
url: "https://www.andromedacms.dev/docs/content-and-schemas"
updated_at: 2026-09-24
---

# Content and schemas

Every collection declares the frontmatter it expects. Files that do not match
fail the build with the file name and the problem, instead of surfacing as
`nil` in a view.

## Declaring attributes

```ruby
module Content
  class Post < Andromeda::Entry
    collection :posts, base: "app/content/posts", pattern: "**/*.{md,mdx}"

    attribute :title, :string, required: true
    attribute :pub_date, :date, required: true
    attribute :draft, :boolean, default: false
    attribute :tags, :array, of: :string, default: []
    attribute :hero_image, :image
    attribute :author, :reference, collection: :authors

    scope :published, -> { where(draft: false) }

    def reading_time = (body.length / 400.0).ceil
  end
end
```

| Type | Accepts | Becomes |
|------|---------|---------|
| `:string` `:integer` `:float` `:boolean` | the matching YAML type | itself |
| `:date` `:datetime` | `2026-09-01`, `'Jul 08 2022'`, ISO strings | `Date` / `Time` |
| `:array` | a list, with `of:` for the element type | `Array` |
| `:enum` | one of `values:` | the value |
| `:hash` | a mapping | `Hash` |
| `:image` | a path relative to the entry | an image, published through the asset pipeline |
| `:reference` | an id in another collection | the target entry, resolved lazily |

Options: `required:`, `default:` (a value or a callable), `of:`, `values:`,
`collection:`.

## Frontmatter

YAML (`---`) and TOML (`+++`) are both supported. YAML is read with 1.2
semantics, matching Astro: `yes` and `no` stay strings, while `true` and
`false` are booleans. Dates and timestamps become `Date` and `Time`.

Keys are `snake_case`. Content copied from an Astro project usually has
`camelCase` keys; `bin/rails andromeda:fix` rewrites them, and
`bin/rails andromeda:check` reports them.

## Querying

```ruby
Content::Post.all
Content::Post.published.order(pub_date: :desc).limit(10)
Content::Post.where(draft: false, lang: "en")          # every key must match
Content::Post.where { |post| post.tags.include?("rails") }
Content::Post.find("hello-world")   # raises Andromeda::EntryNotFound
```

`where` with a hash compares values for equality, so use the block form to
look inside an array. `order` is stable: entries that tie keep the order they
were loaded in, as they do in Astro.

Entries expose `id`, `data`, `body`, `file_path`, `digest`, `html` and
`headings`, plus a reader per declared attribute (`post.title`).

## Entry ids

Ids follow Astro's `glob()` loader rules, so URLs from an Astro project keep
working:

| File | Id |
|------|-----|
| `hello-world.md` | `hello-world` |
| `Guides/Getting Started.md` | `guides/getting-started` |
| `posts/hello/index.mdx` | `posts/hello` |
| a `slug:` in the frontmatter | that value, verbatim |
