Foundry logo Foundry Advanced custom fields

Core model

  • The theme owns the schema. Contracts are declared in theme.yaml under field_contracts.
  • The document owns page-specific values. Those stay in frontmatter under fields:.
  • Shared/global values live in one file. Foundry stores them in content/custom-fields.yaml.
  • The admin resolves fields from the active theme. The editor shows the matching document contract for the current page. The Custom Fields screen shows shared contracts.
  • Plugins must keep derived metadata out of persisted fields. Runtime-only values like TOC or reading time belong in render-time context data such as ctx.Data.

Theme contract example

field_contracts:
  - key: marketing-page
    title: Marketing Page
    description: Standard hero fields for marketing pages.
    target:
      scope: document
      types: [page]
      layouts: [page]
      slugs: [pricing, about, contact]
    fields:
      - name: hero_eyebrow
        type: text
        label: Hero Eyebrow
      - name: hero_title
        type: text
        label: Hero Title
        required: true
      - name: hero_body
        type: textarea
        label: Hero Body
      - name: cta_label
        type: text
        label: CTA Label
      - name: cta_url
        type: text
        label: CTA URL

  - key: site_marketing
    title: Shared Marketing Copy
    description: Shared CTA defaults for the whole site.
    target:
      scope: shared
      key: site_marketing
    fields:
      - name: primary_cta_label
        type: text
        label: Primary CTA Label
      - name: primary_cta_url
        type: text
        label: Primary CTA URL

Document values

---
title: Pricing
slug: pricing
layout: page
fields:
  hero_eyebrow: Clear pricing
  hero_title: Pricing that keeps infrastructure calm
  hero_body: Foundry keeps teams on a predictable operational surface.
  cta_label: Start with Foundry
  cta_url: /contact/
---

Page body content here.

Shared values

values:
  site_marketing:
    primary_cta_label: Launch with Foundry
    primary_cta_url: /contact/

Template access

Document values are read with field .Page "...". Shared values are exposed through .Site.CustomFields by contract key.

<section class="hero">
  <div class="eyebrow">{{ with field .Page "hero_eyebrow" }}{{ . }}{{ else }}Marketing{{ end }}</div>
  <h1>{{ with field .Page "hero_title" }}{{ . }}{{ else }}{{ .Page.Title }}{{ end }}</h1>
  <p>{{ with field .Page "hero_body" }}{{ . }}{{ else }}{{ .Page.Summary }}{{ end }}</p>

  {{ $shared := index .Site.CustomFields "site_marketing" }}
  <a class="button" href="{{ with field .Page "cta_url" }}{{ . }}{{ else }}{{ index $shared "primary_cta_url" }}{{ end }}">
    {{ with field .Page "cta_label" }}{{ . }}{{ else }}{{ index $shared "primary_cta_label" }}{{ end }}
  </a>
</section>

How the admin behaves

  • When a user opens a page in the editor, Foundry matches the active theme contracts by document type, layout, and slug.
  • The editor shows the matching contract title so the user knows why those fields are available on that page.
  • The Custom Fields admin section shows shared contracts declared by the active theme.
  • Users with config.manage can edit and save shared/global field values.
  • Users without config.manage can still inspect shared field groups, but the save controls remain disabled.

Migration from the old model

Advanced field schemas no longer belong in content/config/site.yaml. If you still have an older site using config-owned field schemas, migrate them with:

foundry theme migrate field-contracts

Related docs