Building a theme
Foundry themes own presentation. A theme defines layouts, partials, theme assets, and a small manifest that describes the package.
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.endbody.startbody.endpage.before_mainpage.after_mainpage.before_contentpage.after_contentpost.before_headerpost.after_headerpost.before_contentpost.after_contentpost.sidebar.toppost.sidebar.overviewpost.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
-
layoutsandsupported_layoutsdefine the expected template surface. Ifsupported_layoutsis present, validation treats it as the authoritative layout contract. -
config_schemadescribes theme-level configuration fields using the same schema field shapes used elsewhere in Foundry. -
field_contractsis 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 incontent/custom-fields.yaml. -
securityis 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.
-
screenshotsis metadata for admin/docs/reporting and should point at paths inside the theme package. -
sdk_versionis the official Foundry JavaScript SDK contract version the theme expects. The shipped default themes targetv1, and validation now rejects themes that declare an unsupported SDK contract. -
compatibility_versionis 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_configandallow_admin_stateare 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 whenallow_shared_fieldsis enabled.
End-to-end custom fields example
A complete theme-driven custom field flow has four parts:
- the theme declares contracts in
theme.yaml - page-specific values live in document frontmatter under
fields: - shared/global values live in
content/custom-fields.yaml - 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 Fieldsadmin section shows shared contracts likeShared Marketing Copy. -
Users with
config.managecan edit and save shared values there. -
Users without
config.managecan 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
- Run
foundry theme validate <name> --security --csp. - Inspect the generated CSP and remote asset summary with
foundry theme security <name>. - Preview with
foundry serve,foundry serve-preview, orfoundry serve-standalone. - Confirm page, post, and taxonomy archive templates all render cleanly.
- 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.
