Files
Serendipity edbf78f236 feat: 初始化 Halo 暗色模式插件
- Halo Plugin 后端(Java/Gradle),含 DarkModePlugin 主类和测试
- Vue 3 + TypeScript 前端 UI,包含主题切换组件和设置页面
- 暗色模式 CSS 变量和覆盖样式(布局/编辑器/表单/滚动条等)
- 设计文档和调查文档
- Halo 插件/主题开发 Agent Skills

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 21:03:00 +08:00

12 KiB

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:

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:

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.

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]

<!-- Use color_scheme from the style group -->
<body th:class="${theme.config.style.color_scheme}">
  <ul th:if="${theme.config.layout.nav == 'single'}">
    Single-column nav
  </ul>
  <div th:if="${theme.config.layout.nav == 'double'}">Double-column nav</div>
</body>

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.

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

# ✅ 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"