Foundry logo Foundry Theme authoring

Start with the scaffold

foundry theme scaffold <theme-name>
foundry theme validate <theme-name> --security --csp
foundry theme security <theme-name>
foundry theme switch <theme-name>

The scaffolded theme gives you the expected directory structure and the required base set of layouts and partials.

Theme structure

themes/editorial/
  theme.yaml
  assets/
    css/
      base.css
  layouts/
    base.html
    index.html
    page.html
    post.html
    list.html
    partials/
      head.html
      header.html
      footer.html

Manifest fields

theme.yaml describes the theme, the layouts it supports, the configuration it expects, the field contracts it exposes, and the slot surface it guarantees.

name: editorial
title: Editorial
version: 0.1.0
description: A Foundry theme.
license: MIT
min_foundry_version: 0.1.0
compatibility_version: v1
layouts:
  - base
  - index
  - page
  - post
  - list
supported_layouts:
  - base
  - index
  - page
  - post
  - list
screenshots:
  - screenshots/home.png
config_schema:
  - name: accent_color
    label: Accent Color
    type: text
    default: "#0c7c59"
field_contracts:
  - key: marketing-page
    title: Marketing Page
    target:
      scope: document
      types: [page]
      layouts: [page]
      slugs: [pricing, about]
    fields:
      - name: hero_title
        type: text
        label: Hero Title
      - name: hero_body
        type: textarea
        label: Hero Body
  - key: site_marketing
    title: Site Marketing
    target:
      scope: shared
      key: site_marketing
    fields:
      - name: primary_cta_label
        type: text
        label: Primary CTA Label
security:
  external_assets:
    allowed: false
  frontend_requests:
    allowed: false
  template_context:
    allow_site_params: true
    allow_content_fields: true
    allow_shared_fields: true
slots:
  - head.end
  - body.start
  - body.end
  - page.before_main
  - page.after_main
  - page.before_content
  - page.after_content
  - post.before_header
  - post.after_header
  - post.before_content
  - post.after_content
  - post.sidebar.top
  - post.sidebar.overview
  - post.sidebar.bottom

Layout responsibilities

  • base.html: wraps the page shell and usually includes shared head, header, footer, and plugin slots.
  • index.html: renders the home page or language root page.
  • page.html: renders page documents.
  • post.html: renders post documents.
  • list.html: renders list-style views such as taxonomy archives.

Required slots

For the launch version of Foundry, theme validation enforces a minimum slot contract. A valid theme must declare and render all of the following slots:

  • head.end
  • body.start
  • body.end
  • page.before_main
  • page.after_main
  • page.before_content
  • page.after_content
  • post.before_header
  • post.after_header
  • post.before_content
  • post.after_content
  • post.sidebar.top
  • post.sidebar.overview
  • post.sidebar.bottom

Validation behavior

Theme validation now reports a fuller diagnostic set:

  • the theme manifest declares the required slot names
  • the expected layouts actually render those slot calls
  • required layouts and required partials are present
  • templates do not reference missing layouts or partials
  • the layout set parses cleanly with the current Foundry template helpers

Manifest reference

  • layouts and supported_layouts define the expected template surface. If supported_layouts is present, validation treats it as the authoritative layout contract.
  • config_schema describes theme-level configuration fields using the same schema field shapes used elsewhere in Foundry.
  • field_contracts is the advanced custom-field contract. Themes declare which fields apply to which pages by type, layout, and slug, plus which shared/global field groups are editable in content/custom-fields.yaml.
  • security is the theme security contract. It declares whether the theme expects remote assets, whether browser-side requests may leave the current origin, and which template context surfaces are available.
  • Theme-declared fields are persisted content. Plugin-derived runtime metadata should not be written into document fields; it should stay in render-time context data.
  • screenshots is metadata for admin/docs/reporting and should point at paths inside the theme package.
  • sdk_version is the official Foundry JavaScript SDK contract version the theme expects. The shipped default themes target v1, and validation now rejects themes that declare an unsupported SDK contract.
  • compatibility_version is the theme-platform contract version. Use it to signal compatibility with future theme-platform changes.

Theme security

Foundry now validates frontend themes against a dedicated security contract. Themes do not receive the raw site config anymore; they receive a curated public-safe view of site settings plus the fields/data explicitly allowed by the theme contract.

security:
  external_assets:
    allowed: true
    scripts:
      - https://cdn.example.com
    styles:
      - https://fonts.example.com
  frontend_requests:
    allowed: true
    origins:
      - https://api.example.com
    methods:
      - GET
      - POST
  template_context:
    allow_site_params: true
    allow_content_fields: true
    allow_shared_fields: true
    allow_runtime_state: false
    allow_admin_state: false
    allow_raw_config: false
  • Remote assets are denied by default unless they are declared.
  • Public-page CSP headers are generated from the theme's declared security policy.
  • allow_raw_config and allow_admin_state are intentionally not supported; themes should never receive secrets, admin tokens, session stores, or raw filesystem paths.
  • Shared custom fields remain available through data "custom_fields" only when allow_shared_fields is enabled.

End-to-end custom fields example

A complete theme-driven custom field flow has four parts:

  1. the theme declares contracts in theme.yaml
  2. page-specific values live in document frontmatter under fields:
  3. shared/global values live in content/custom-fields.yaml
  4. templates read those values at render time

1. Theme contract

This example mirrors the shipped Foundry-Cloud-Landing theme and shows one document contract plus one shared contract:

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

2. Page-specific values

A matching page document keeps its own values in frontmatter:

---
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.

3. Shared/global values

Shared values live in content/custom-fields.yaml under the shared contract key:

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

4. Template access

Templates read document fields with field .Page "...". Shared values are exposed through .Site.CustomFields using the shared 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>

5. Admin behavior

  • When an editor opens the Pricing page, Foundry resolves the active theme contracts by page type, layout, and slug.
  • The document editor shows the contract title, such as Marketing Page, and renders the matching page-specific fields.
  • The Custom Fields admin section shows shared contracts like Shared Marketing Copy.
  • Users with config.manage can edit and save shared values there.
  • Users without config.manage can still view shared contracts, but the save controls stay disabled.

Theme and plugin interplay

Theme templates should leave room for plugin-provided markup. If you introduce a new slot, document it in the theme manifest and place it intentionally in the layout instead of treating slots as arbitrary append-only escape hatches.

Assets

Put theme CSS and other theme-owned assets under assets/. During build or preview, Foundry copies theme assets into public/theme/ and bundles discovered CSS into the main CSS output.

Validation checklist

  1. Run foundry theme validate <name> --security --csp.
  2. Inspect the generated CSP and remote asset summary with foundry theme security <name>.
  3. Preview with foundry serve, foundry serve-preview, or foundry serve-standalone.
  4. Confirm page, post, and taxonomy archive templates all render cleanly.
  5. Check that plugin slots still have sensible placement.

Reference theme

The default theme in the repository is the canonical reference for current layout expectations and slot placement.

Admin theme widget slots

Admin themes use a separate manifest, admin-theme.yaml. In addition to the component contract, admin themes now declare widget_slots so plugin-defined admin widgets can mount against a validated slot surface instead of guessing where the shell has room for them.

name: default
title: Default Admin
version: 0.1.0
admin_api: v1
sdk_version: v1
compatibility_version: v1
components:
  - shell
  - login
  - navigation
  - documents
  - media
  - users
  - config
  - plugins
  - themes
  - audit
widget_slots:
  - overview.after
  - documents.sidebar
  - media.sidebar
  - plugins.sidebar

For the current admin-platform contract, all four widget slots are required. The default admin theme auto-mounts plugin widgets declared for those slots, and alternate admin themes are validated against the same baseline.