Andromeda

Content and schemas

View as Markdown

Frontmatter, attribute types, validation and queries.

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

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
TypeAcceptsBecomes
:string :integer :float :booleanthe matching YAML typeitself
:date :datetime2026-09-01, 'Jul 08 2022', ISO stringsDate / Time
:arraya list, with of: for the element typeArray
:enumone of values:the value
:hasha mappingHash
:imagea path relative to the entryan image, published through the asset pipeline
:referencean id in another collectionthe target entry, resolved lazily

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

References

A :reference holds the id of an entry in another collection, named by collection:. For a list of references, use :array with of: :reference and put collection: on the same attribute:

attribute :author, :reference, collection: :authors
attribute :categories, :array, of: :reference, collection: :categories, default: []
author: jane
categories: [exam-prep, abroad]

The readers return entries: post.author is the author, and post.categories the categories in the order written. They are resolved lazily — the target collection is loaded the first time a reader asks — and an id with no entry raises Andromeda::EntryNotFound. post.data[:author] and post.data[:categories] keep the raw Andromeda::Reference pointers (collection and id).

andromeda:build and andromeda:check look up every reference, so a mistyped id fails the build instead of the page that shows it.

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

Content::Post.all
Content::Post.published.order(pub_date: :desc).limit(10)
Content::Post.published.order(pub_date: :desc).offset(10).limit(10)   # second page of ten
Content::Post.where(draft: false, lang: "en")          # every key must match
Content::Post.where { |post| post.tags.include?("rails") }
Content::Post.where { |post| post.data[:categories].any? { |ref| ref.id == "abroad" } }
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. For an array of references, compare ids through data: post.categories returns entries, so it never matches a string. order is stable: entries that tie keep the order they were loaded in, as they do in Astro.

offset(count) skips the first count entries and returns a relation, so a listing can be paged with offset and limit and still take more scopes.

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:

FileId
hello-world.mdhello-world
Guides/Getting Started.mdguides/getting-started
posts/hello/index.mdxposts/hello
a slug: in the frontmatterthat value, verbatim

Last updated September 29, 2026