Configuration reference

Everything pullpress.config.yml understands — the one file that turns your repo into a CMS.

The config file lives in the root of your repository and is committed like everything else, so changes to your content model go through review too. It has a handful of top-level keys, a list of collections, and a small set of field types.

Top-level keys

KeyDescription
versionConfig schema version. Currently always 1.
media.folderRepository path where uploaded images are committed, e.g. public/uploads.
media.public_pathURL prefix the published site serves that folder from, e.g. /uploads. Used to write correct image links into your Markdown.
collectionsThe list of content collections editors can work with. Each collection has a name, a label, a type and fields.
componentsOptional. A map of reusable named field groups, referenced from fields with type: component. Define a group once, reuse it across collections.

Collection types

A collection describes one kind of content. Its type decides how entries map to files in the repository.

folder — one file per entry

Every entry is its own Markdown file inside the configured folder. Editors can create, edit and delete entries — ideal for blog posts, news items or team members.

collections:
  - name: blog
    label: Blog posts
    type: folder
    folder: content/blog
    fields:
      - { name: title, label: Title, type: string, required: true }
      - { name: date,  label: Date,  type: date, default: now }
      - { name: body,  label: Body,  type: markdown }

Sites that use one folder per entry (Astro content collections, Hugo leaf bundles: content/my-post/index.md) set structure: bundle. Add media: colocatedto store an entry's images inside its own folder, referenced relatively (./photo.jpg) so your generator's image pipeline picks them up:

collections:
  - name: projects
    label: Projects
    type: folder
    folder: redesign/content
    structure: bundle      # one folder per entry: content/my-post/index.md
    media: colocated       # images live inside the entry folder (./photo.jpg)
    slug: "{{date}}-{{slug:title}}"
    fields:
      - { name: title, label: Title, type: string, required: true }
      - { name: date,  label: Date,  type: date, default: now }
      - { name: body,  label: Body,  type: markdown }

files — fixed pages

A fixed set of files you list explicitly. Editors can edit them but not add or remove any — right for one-off pages like About or Contact.

collections:
  - name: pages
    label: Pages
    type: files
    files:
      - { name: about,   label: About page,   file: content/about.md }
      - { name: contact, label: Contact page, file: content/contact.md }
    fields:
      - { name: title, label: Title, type: string, required: true }
      - { name: body,  label: Body,  type: markdown }

data — a single structured data file

One JSON or YAML file edited entirely through a form, with no Markdown body. Use it for structured data your templates read directly: opening hours, navigation menus, site settings.

collections:
  - name: opening_hours
    label: Opening hours
    type: data
    file: data/opening-hours.yml
    fields:
      - name: hours
        label: Weekly hours
        type: list
        of: object
        fields:
          - { name: day,    label: Day,    type: select, options: [mon, tue, wed, thu, fri, sat, sun] }
          - { name: open,   label: Opens,  type: string }
          - { name: close,  label: Closes, type: string }
          - { name: closed, label: Closed all day, type: boolean, default: false }

Field types

Each field in a collection becomes a form control in the editor. The common options label, required and default work on every type. Two more help your editors: a help string shows a hint under the field, and a placeholdershows example text inside an empty input. Required fields get a red asterisk and a friendly “what's missing” summary on submit.

TypeDescriptionExample
stringA single line of text.{ name: title, type: string, required: true }
textMulti-line plain text, shown as a textarea.{ name: summary, type: text }
markdownRich text in the clean editor, stored as Markdown.{ name: body, type: markdown }
numberAn integer or decimal number.{ name: price, type: number }
booleanA yes/no toggle.{ name: featured, type: boolean, default: false }
dateA calendar date. default: now fills in today.{ name: date, type: date, default: now }
datetimeA date with a time of day.{ name: published_at, type: datetime }
selectOne choice from a fixed list, via options.{ name: category, type: select, options: [news, recipes] }
multiselectMultiple choices from a fixed list, shown as checkboxes.{ name: categories, type: multiselect, options: [news, events] }
urlA web address, validated as https://…{ name: website, type: url }
emailAn email address, validated.{ name: contact, type: email }
colorA color picker, stored as #rrggbb.{ name: accent, type: color }
imageAn image upload, committed to the media folder.{ name: cover, type: image }
listA repeatable list of items; of sets the item type.{ name: tags, type: list, of: string }
objectA group of nested fields, defined with fields.{ name: cta, type: object, fields: [...] }
relationA reference to an entry in another folder collection (stores its slug).{ name: author, type: relation, collection: team }
blocksStackable sections of different shapes (each item carries a _type key).{ name: sections, type: blocks, blocks: [...] }
hiddenNot shown to editors; written along with a fixed value (e.g. a layout key).{ name: layout, type: hidden, default: post }
codeA monospace code block, with an optional language hint.{ name: snippet, type: code, language: js }
mapA location, edited as latitude/longitude (stored as 'lat,lng').{ name: location, type: map }
computeA read-only value derived from a template over sibling fields.{ name: ref, type: compute, template: '{{year}}-{{slug}}' }
keyvalueArbitrary key/value pairs, e.g. extra metadata.{ name: meta, type: keyvalue }
seoSEO panel: title + description (with length hints), canonical URL, OG image and noindex, with a search-snippet and JSON-LD preview.{ name: seo, type: seo }
componentExpands a reusable field group from the top-level components map.{ name: cta, type: component, component: callToAction }

list and object compose: a list of object gives you repeatable groups of fields, like the weekly opening hours in the example above.

Conditional fields (showIf)

Any field can declare showIfto appear only when a sibling field matches. Hidden fields are cleared (so they never land in the frontmatter), and a conditional field's required rule applies only while it is visible.

fields:
  - { name: hasCta, label: Add a call to action?, type: boolean }
  - name: ctaUrl
    label: Button URL
    type: url
    required: true
    showIf: { field: hasCta, truthy: true }   # equals / oneOf also work

Nested collections

Set nested: true on a folder collection to also gather entries from subfolders (up to five levels deep) and show them grouped per folder — handy for documentation trees. Search and relation pickers pick the nested entries up automatically.

collections:
  - name: docs
    label: Documentation
    type: folder
    folder: content/docs
    nested: true            # gather entries from subfolders too
    fields:
      - { name: title, type: string, required: true }
      - { name: body,  type: markdown }

Reusable field components

Define a named field group once under the top-level components map, then drop it into any collection with a component field. It expands to a regular object group when loaded, so a change to the group updates every collection that uses it.

components:
  callToAction:
    - { name: label, type: string }
    - { name: url,   type: url }
collections:
  - name: pages
    type: folder
    folder: content/pages
    fields:
      - { name: title, type: string, required: true }
      - { name: cta,   type: component, component: callToAction }

Where media lives (and why not S3)

Uploads are committed to your repository, next to the content that references them. That is a deliberate choice, not a missing feature: the repo stays the single source of truth, images travel with their posts through branches, review and history, and cloning the repo gives you the complete site — no second system to back up or pay for.

Need a CDN or image resizing? Let your generator or host handle it at build time (Astro's and Next.js's image pipelines, Netlify image CDN, Cloudinary fetch URLs). PullPress keeps the original in Git; your build makes it fast. Uploads are compressed client-side and capped at a few megabytes, which in practice keeps repos comfortably small. External object storage (S3 and friends) would break the "your repo is everything" guarantee, so we don't plan it.