Files
halo-dark-mode-plugin/.agents/skills/halo-theme-dev/references/vite-plugin.md
T
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

8.0 KiB

Vite Integration (Recommended)

Strongly recommended for building theme projects. Compared to plain Thymeleaf, this approach provides a modern frontend development experience (TypeScript, Sass, PostCSS, TailwindCSS) plus lightweight HTML component reuse (include/slot) — significantly reducing the verbosity of Thymeleaf's native fragment system.


vite-plugin-halo-theme

Key Features

  1. HTML template reuse: supports <include>, <slot> (default and named), and <props> syntax, preprocessed at Vite build time — fully independent of Thymeleaf.
  2. Automatic multi-page entry scanning: automatically discovers all .html files under src/ (except src/partials/) as Vite build entries, with output going to templates/.
  3. Static asset handling: CSS/JS in src/ is bundled by Vite into templates/assets/; files in public/ are copied directly to templates/assets/ without processing.

Directory Convention

my-theme/
├── src/
│   ├── css/
│   │   └── main.css          # CSS source
│   ├── js/
│   │   └── main.ts           # TypeScript source
│   ├── partials/             # Not treated as page entries; referenced via include
│   │   ├── layout.html       # Shared layout
│   │   └── post-card.html    # Reusable post card component
│   ├── index.html            # Home page entry → templates/index.html
│   ├── post.html             # → templates/post.html
│   ├── page.html
│   ├── archives.html
│   ├── tags.html
│   ├── tag.html
│   ├── categories.html
│   └── category.html
├── public/                   # Copied directly to templates/assets/ (no Vite processing)
├── templates/                # Build output directory (add to .gitignore)
├── vite.config.ts
├── package.json
├── .gitignore
├── theme.yaml
└── settings.yaml

Quick Setup

package.json

{
  "type": "module",
  "scripts": {
    "dev": "vite build --watch",
    "build": "vite build"
  },
  "devDependencies": {
    "@halo-dev/vite-plugin-halo-theme": "latest",
    "vite": "^6.0.0"
  }
}

vite.config.ts

import { defineConfig } from "vite";
import { haloThemePlugin } from "@halo-dev/vite-plugin-halo-theme";

export default defineConfig({
  plugins: [haloThemePlugin()],
});

.gitignore

node_modules/
templates/
!templates/.gitkeep

Template Syntax (build-time only — independent of Thymeleaf)

Important

: include/slot syntax is processed at Vite build time; Thymeleaf syntax is processed at server runtime. The two systems are fully isolated and can be mixed freely.

<include> — Import a partial

<include src="layout.html">
  <main>Page content</main>
</include>

Named Slots

Declare slots in a partial (src/partials/layout.html):

<head>
  <slot name="head">
    <title th:text="${site.title}">Default title</title>
  </slot>
</head>
<body>
  <slot />
</body>

Use in a page:

<include src="layout.html">
  <template name="head">
    <title th:text="${post.spec.title}">Post title</title>
  </template>
  <article th:utext="${post.content.content}"></article>
</include>

Include Path Resolution

Syntax Resolves to
foo.html src/partials/foo.html (preferred)
partials/foo.html src/partials/foo.html
./foo.html Relative to current file
/foo.html Relative to src/ root

Static Asset Paths

All HTML files — both src/*.html and src/partials/*.html — resolve static asset references relative to src/, regardless of the file's actual location.

<!-- src/partials/layout.html: referencing src/css/main.css -->
<link rel="stylesheet" href="./css/main.css" />
<!-- ✅ correct (relative to src/) -->
<link rel="stylesheet" href="../css/main.css" />
<!-- ❌ wrong (relative to file location) -->

<!-- Same rule in src/index.html -->
<script type="module" src="./js/main.ts"></script>
<!-- ✅ correct -->

Always write asset paths as if the file is in src/, even when it is inside src/partials/.


Full Example: Shared Layout

src/partials/layout.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <slot name="head">
      <title th:text="${site.title}">Site title</title>
    </slot>
    <link rel="stylesheet" href="./css/main.css" />
  </head>
  <body>
    <header>
      <a th:href="@{/}" th:text="${site.title}"></a>
      <nav th:with="menu = ${menuFinder.getPrimary()}">
        <ul>
          <li th:each="item : ${menu.menuItems}">
            <a
              th:href="${item.status.href}"
              th:text="${item.status.displayName}"
              th:target="${item.spec.target?.value}"
            ></a>
          </li>
        </ul>
      </nav>
    </header>

    <main>
      <slot />
    </main>

    <footer>
      <halo:footer />
    </footer>
  </body>
</html>

src/index.html

<include src="layout.html">
  <template name="head">
    <title th:text="${site.title}">Home</title>
  </template>

  <section>
    <ul>
      <li th:each="post : ${posts.items}">
        <a th:href="${post.status.permalink}" th:text="${post.spec.title}"></a>
        <time th:text="${#temporals.format(post.spec.publishTime, 'yyyy-MM-dd')}"></time>
        <p th:text="${post.status.excerpt}"></p>
      </li>
    </ul>
    <nav th:if="${posts.hasPrevious() || posts.hasNext()}">
      <a th:if="${posts.hasPrevious()}" th:href="${posts.prevUrl}">Previous</a>
      <span th:text="${posts.page} + '/' + ${posts.totalPages}"></span>
      <a th:if="${posts.hasNext()}" th:href="${posts.nextUrl}">Next</a>
    </nav>
  </section>
</include>

src/post.html

<include src="layout.html">
  <template name="head">
    <title th:text="${post.spec.title}">Post title</title>
  </template>

  <article>
    <h1 th:text="${post.spec.title}"></h1>
    <time th:text="${#temporals.format(post.spec.publishTime, 'yyyy-MM-dd')}"></time>
    <div th:utext="${post.content.content}"></div>

    <!-- Prev / next navigation -->
    <div th:with="cursor = ${postFinder.cursor(post.metadata.name)}">
      <a
        th:if="${cursor.hasPrevious()}"
        th:href="${cursor.previous.status.permalink}"
        th:text="${cursor.previous.spec.title}"
        >Previous</a
      >
      <a
        th:if="${cursor.hasNext()}"
        th:href="${cursor.next.status.permalink}"
        th:text="${cursor.next.spec.title}"
        >Next</a
      >
    </div>

    <!-- Comments -->
    <div th:if="${haloCommentEnabled}">
      <halo:comment group="content.halo.run" kind="Post" th:attr="name=${post.metadata.name}" />
    </div>
  </article>
</include>

TailwindCSS Integration

pnpm add -D tailwindcss @tailwindcss/vite
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
import { haloThemePlugin } from "@halo-dev/vite-plugin-halo-theme";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [tailwindcss(), haloThemePlugin()],
});
/* src/css/main.css */
@import "tailwindcss";

Ready-to-use Templates

The skill's assets/ directory provides two ready-to-use templates:

  • assets/theme-minimal/ — Zero-build-tool minimal theme; good for quick prototyping or plain HTML/CSS themes
  • assets/theme-vite/ — Full Vite project with vite-plugin-halo-theme; recommended as the starting point for new themes

Usage: copy the directory into themes/ in your Halo working directory, ensure the folder name matches metadata.name in theme.yaml, then install and activate in Console.