# Directory Structure & Configuration
## Directory Structure
```
my-theme/
├── templates/
│ ├── assets/ # Static assets (CSS/JS/images etc.) — must be placed here
│ │ ├── css/
│ │ │ └── style.css
│ │ └── js/
│ │ └── main.js
│ ├── index.html # Home page
│ ├── post.html # Post detail
│ ├── page.html # Single page detail
│ ├── archives.html # Post archives
│ ├── tags.html # Tag listing
│ ├── tag.html # Tag archive
│ ├── categories.html # Category listing
│ └── category.html # Category archive
├── screenshot.png # Optional Console preview image (Halo 2.25+)
├── theme.yaml # Theme configuration (required)
└── settings.yaml # Theme settings form definition (optional)
```
> **Important**: The theme folder name must match the `metadata.name` field in `theme.yaml`; otherwise some assets may fail to load.
Halo 2.25+ recognizes the first readable root preview image in this order:
`screenshot.png`, `screenshot.jpeg`, `screenshot.jpg`, `screenshot.webp`. The
resolved URL is exposed as `Theme.status.screenshot`.
## theme.yaml
The theme root directory must contain `theme.yaml`. Minimal runnable config:
```yaml
apiVersion: theme.halo.run/v1alpha1
kind: Theme
metadata:
name: theme-foo # must match the theme folder name
spec:
displayName: My Theme
version: 1.0.0
requires: ">=2.0.0"
```
Full field example:
```yaml
apiVersion: theme.halo.run/v1alpha1
kind: Theme
metadata:
name: theme-foo
spec:
displayName: My Theme
author:
name: Author Name
website: https://example.com
description: An example theme
logo: https://example.com/logo.png
homepage: https://github.com/example/theme-foo
repo: https://github.com/example/theme-foo.git
issues: https://github.com/example/theme-foo/issues
settingName: "theme-foo-setting" # must match metadata.name in settings.yaml
configMapName: "theme-foo-configMap"
customTemplates: # optional
post:
- name: Documentation
description: Article in documentation format
screenshot:
file: post_documentation.html
category:
- name: Knowledge Base
description: Knowledge base category
screenshot:
file: category_knowledge.html
page:
- name: About
description: About page
screenshot:
file: page_about.html
version: 1.0.0
requires: ">=2.0.0"
license:
- name: "GPL-3.0"
url: "https://github.com/example/theme-foo/blob/main/LICENSE"
```
### Key Fields
| Field | Description | Required |
| ---------------------- | ------------------------------------------------------------------- | ---------------------------------------- |
| `metadata.name` | Unique theme identifier — **must match the folder name** | Yes |
| `spec.displayName` | Display name | Yes |
| `spec.version` | Theme version | Yes |
| `spec.requires` | Minimum required Halo version | Yes |
| `spec.settingName` | Setting resource name — must match `metadata.name` in settings.yaml | No |
| `spec.configMapName` | ConfigMap name for persisting settings | No (configure together with settingName) |
| `spec.customTemplates` | Custom template configuration | No |
> After modifying `theme.yaml`, click "Reload Theme Configuration" on the theme page in Console for changes to take effect.
## settings.yaml
Defines a form that is auto-rendered on the theme settings page in Console, using [FormKit](https://github.com/formkit/formkit).
```yaml
apiVersion: v1alpha1
kind: Setting
metadata:
name: theme-foo-setting # must match spec.settingName in theme.yaml
spec:
forms:
- group: style # group name (accessed in templates as theme.config.style.xxx)
label: Style
formSchema:
- $formkit: radio
name: color_scheme # field name (accessed as theme.config.style.color_scheme)
label: Default color scheme
value: system
options:
- label: Follow system
value: system
- label: Dark
value: dark
- label: Light
value: light
- $formkit: color
name: background_color
label: Background color
value: "#f2f2f2"
- group: layout
label: Layout
formSchema:
- $formkit: radio
name: nav
label: Navigation layout
value: "single"
options:
- label: Single column
value: "single"
- label: Double column
value: "double"
```
### Using Settings Values in Templates
Pattern: `theme.config.[group].[name]`
```html
Double-column nav
```
### Native FormKit Input Types
Commonly used native input components in theme settings (see links for full docs):
| `$formkit` value | Purpose | Docs |
| ---------------- | --------------------------------- | -------------------------------------------- |
| `text` | Single-line text | https://formkit.com/inputs/text.md |
| `textarea` | Multi-line text | https://formkit.com/inputs/textarea.md |
| `number` | Number input | https://formkit.com/inputs/number.md |
| `password` | Password input | https://formkit.com/inputs/password.md |
| `radio` | Radio buttons (options list) | https://formkit.com/inputs/radio.md |
| `checkbox` | Checkbox (single or multi-select) | https://formkit.com/inputs/checkbox.md |
| `color` | Color picker (returns hex value) | https://formkit.com/inputs/color.md |
| `range` | Slider range | https://formkit.com/inputs/range.md |
| `date` | Date picker | https://formkit.com/inputs/date.md |
| `datetime-local` | Date-time picker | https://formkit.com/inputs/datetime-local.md |
| `button` | Button | https://formkit.com/inputs/button.md |
| `email` | Email input | https://formkit.com/inputs/email.md |
| `group` | Group (for grouping fields) | https://formkit.com/inputs/group.md |
| `url` | URL input | https://formkit.com/inputs/url.md |
FormKit Inputs overview: https://formkit.com/inputs
FormKit Schema (conditional rendering, loops, expressions, advanced usage): https://formkit.com/essentials/schema.md
> Note: FormKit Pro input components are not supported.
### Halo Extended Input Types
Additional input components provided by Halo on top of FormKit:
| `$formkit` value | Purpose |
| ------------------ | ------------------------------------------------------------------- |
| `select` | Enhanced dropdown with multi-select, search, and remote data source |
| `switch` | Toggle switch (boolean or custom on/off values) |
| `toggle` | Visual toggle supporting image/color/text options |
| `attachment` | Attachment picker (preview, direct upload, select from library) |
| `attachmentInput` | Attachment picker (library popup only) |
| `code` | Code editor (supports yaml/html/js/css/json) |
| `array` | Object array (add/remove/reorder — recommended over repeater) |
| `list` | Primitive array (strings, numbers, etc.) |
| `categorySelect` | Post category selector (returns `metadata.name`) |
| `categoryCheckbox` | Post category checkbox (returns array of `metadata.name`) |
| `tagSelect` | Post tag selector (returns `metadata.name`) |
| `tagCheckbox` | Post tag checkbox (returns array of `metadata.name`) |
| `postSelect` | Post selector |
| `singlePageSelect` | Single page selector |
| `menuSelect` | Menu selector (supports multi-select) |
| `menuCheckbox` | Menu checkbox |
| `menuRadio` | Menu radio |
| `iconify` | Icon picker (Iconify-based, supports svg/dataurl/url/name formats) |
| `secret` | Secret resource selector |
| `verificationForm` | Remote verification form group |
Full parameter reference: https://raw.githubusercontent.com/halo-dev/docs/refs/heads/main/docs/developer-guide/form-schema.md
Version-sensitive FormKit notes:
- Halo 2.25+: `select` option objects support `icon` and `description`; remote
select data can map those fields via `requestOption.iconField` and
`requestOption.descriptionField`.
- Halo 2.25+: `secret` supports `descriptionPreset`.
- Halo 2.23+: `iconify` supports optional `sizing`.
- Halo 2.22.8+: `toggle` is available.
- Halo 2.22.2+: `switch` is available.
- Halo 2.22+: prefer `array` over `repeater`; the newer `attachment` supports
preview/direct upload/library selection, while the older library-only picker
is `attachmentInput`.
### FormKit Schema Gotchas
**1. Inside `array` / `list` children, use `$value` — not `$get()`**
Within the `children` of an `array`, `list`, or `repeater`, access the current item's data via `$value`, not `$get(id).value`. `$get()` can only reference standalone named input nodes by `id`; it cannot reach the current item in a nested context.
```yaml
# ✅ Correct: use $value.[name] to reference a sibling field
- $formkit: array
name: socials
children:
- $formkit: text
name: platform
label: Platform
- $formkit: text
name: url
label: URL
if: "$value.platform !== ''" # references sibling field "platform"
# ❌ Wrong: $get(platform).value does not work in a nested context
- $formkit: text
name: url
if: "$get(platform).value !== ''"
```
**2. Nodes using `if` must also declare a `key`**
Any `$formkit`/`$el`/`$cmp` node with an `if` attribute must declare a unique `key`. Without it, Vue reuses DOM nodes when the condition toggles, causing stale form values or rendering glitches.
```yaml
# ✅ Correct: add key whenever if is present
- $formkit: text
key: url-field
name: url
label: URL
if: "$value.show_link === true"
# ❌ Wrong: missing key may cause value leakage on toggle
- $formkit: text
name: url
label: URL
if: "$value.show_link === true"
```