# Page themes
Public shows use the default React SPA feed unless you pick a **page theme** under **Page Customizations**. Packaged themes are Liquid templates with CSS, optional images/fonts, and HarborFM mount points for interactive blocks (episodes, player, cast, reviews, and so on).
For show-level toggles (accent, which blocks appear), see [Podcasts: Page Customizations](Podcasts.md#page-customizations). For listener-facing feed behavior, see [Public feeds and embed](Public-feeds-and-embed.md).
## Choosing a theme
1. Open the show and click **Page Customizations**.
2. Pick a **Page Theme**:
- **Default HarborFM** - built-in React feed layout.
- **Server-wide themes** - Fluid, Folio, and any theme an admin has promoted.
- **Imported themes** - packages you imported under **Themes** (on podcasts you own).
3. Optionally set **Primary Color** and visibility toggles (author, descriptions, funding, podroll, cast, reviews, and related blocks).
4. Optionally set a **Meta Pixel ID**. Public podcast and episode pages load the pixel; when the site consent banner is enabled, it loads only after analytics consent.
The picker shows package preview images when the theme provides one. When a theme sets `homepage` in `theme.json`, **Preview** opens that live URL.
Visibility toggles and accent color apply to both Default and packaged themes. Packaged themes can read accent colors and `show.*` flags in template context.
## Themes studio
Users with **Can Import Theme** can open **Themes** to:
- Browse **Server Themes** (preview thumbnails, description, live Preview when `homepage` is set)
- **Download** a server theme as a zip
- **Import** a theme zip (max 10 MB; rate limited to 2 per minute per user)
- Edit an imported copy in the near-fullscreen theme editor (name, version, home template, page routes, Liquid/CSS/images/fonts)
Re-importing the same `theme.json` `id` updates that theme in place (including personal copies of server-wide themes). Theme files count toward the account storage limit.
Assigning a **server-wide** theme id uses the data-backed server package. To use your fork, pick your imported copy’s row in Page Customizations.
### Admins
Admins can **promote** a personal theme to a server-wide theme (available to every show), **demote** it back, delete server themes, and edit server theme files. Admin edits and promote set `allowOverride: false` so customized server themes are not overwritten on upgrade.
## Built-ins and storage
**Fluid** and **Folio** ship with HarborFM and are seeded into `{DATA_DIR}/themes/server` on first boot. Folio is multi-page (home plus about, crew, support, connect, episodes). Both include preview images, homepage demo links, and optional `not_found` templates for unknown theme `.html` routes.
On upgrade, HarborFM replaces a data copy only when its `theme.json` still allows override (default) and the shipped `version` differs. Packages under `{DATA_DIR}/themes/server` persist across Docker image updates.
## Gallery and authoring docs
Community/gallery themes (with previews) are published from [`harborfm-themes`](https://github.com/LoganRickert/harborfm-themes) and listed on the docs [Themes](https://harborfm.com/themes/) page.
Authoring guides:
- Docs: [Theme Authoring Guide](https://harborfm.com/theme-guide/)
- Repo / AI skill: [theme-SKILL.md](../blob/main/theme-SKILL.md) (also downloadable as **SKILL.md** from the Themes page)
Local developers can clone `harborfm-themes/` next to the HarborFM tree and run `pnpm themes:sync` to copy gallery packages into `{DATA_DIR}/themes/server`. See the main [README](../blob/main/README.md#page-themes).
## Package layout
```
theme.json required (id, name, version; optional fields below)
templates/podcast.liquid required
templates/episode.liquid required
templates/*.liquid optional extra pages and _partials
css/ optional (*.css; all auto-linked)
images/ optional (png, jpg, jpeg, gif, webp)
fonts/ optional (.woff2, .ttf)
```
Optional `theme.json` fields:
| Field | Purpose |
|-------|---------|
| `description` | Short blurb for Themes cards and the gallery |
| `preview` | Path under `images/` for picker / gallery cards |
| `homepage` | https URL for live Preview |
| `index` | Home template basename (default `podcast`) |
| `pages` | Map template basename → public `.html` filename |
| `not_found` | Template for unknown theme pages (HTTP 404; not a public page) |
| `allowOverride` | When `false`, shipped upgrades do not overwrite this server theme |
Extra templates become `/feed/{slug}/{page}.html` (or `/{page}.html` on a custom domain). Theme pages are included in the podcast sitemap.
### Mounts and actions
Interactive HarborFM blocks use Liquid mounts:
```liquid
{% render 'harborfm/episodes' %}
{% render 'harborfm/player' %}
{% render 'harborfm/search' %}
{% render 'harborfm/reviews' %}
{% render 'harborfm/cast' %}
{% render 'harborfm/funding' %}
{% render 'harborfm/links' %}
{% render 'harborfm/podroll' %}
{% render 'harborfm/site_header' %}
{% render 'harborfm/show_header' %}
{% render 'harborfm/breadcrumbs' %}
```
Put `harborfm/episodes` on exactly one page. Themes can also wire native controls:
```html
RSS
```
Unavailable actions are hidden automatically.
Script tags, event handlers, `javascript:` URLs, and the Liquid `| raw` filter are rejected on import.
### Liquid context and styling
Useful context includes `podcast`, `episode` / `episodes`, `cast`, `funding_links`, `links`, `podroll`, `reviews`, `theme` (id/name/version/homepage), `accent` (including `accent.fg` for text on accent), `show` (visibility booleans), `urls` (`home`, `pages`, `theme_asset_base`), `site`, and `page`.
Accent CSS variables include `--accent`, `--accent-dim`, `--accent-glow`, and `--accent-fg`.
Dialogs (Message, Get Alerts, Share, Subscribe, Write Review) render under themed chrome so theme CSS can style them, for example:
```css
[data-harborfm-dialog="feedback"] { /* Message */ }
[data-harborfm-dialog="alerts"] { /* Get Alerts */ }
[data-harborfm-dialog="share"] { /* Share */ }
[data-harborfm-dialog-overlay="feedback"],
[data-harborfm-dialog-overlay="alerts"] { /* backdrop */ }
```
Theme partials (`templates/_nav.liquid`, etc.) use `{% render %}` with an isolated scope. Pass context explicitly:
```liquid
{% render '_nav', urls: urls, podcast: podcast, site: site, page: page %}
```
## Who can import themes
- **`defaultCanImportTheme`** (Settings > Default Limits) - whether newly registered users can import and edit themes. Default is on.
- **`canImportTheme`** (Users admin) - per-user flag. When off, that user cannot import, download, or edit theme files (403). Selecting a built-in or server theme on a show they manage still works when the theme is available.
See [Admin and settings](Admin-and-settings.md).
## See also
- [Podcasts: Page Customizations](Podcasts.md#page-customizations)
- [Public feeds and embed](Public-feeds-and-embed.md)
- [Admin and settings](Admin-and-settings.md)
- Main [README: Page themes](../blob/main/README.md#page-themes)
- [Theme Authoring Guide](https://harborfm.com/theme-guide/)
- [Themes gallery](https://harborfm.com/themes/)