> For AI agents: the complete documentation index is available at https://docs.halo.run/llms.txt, the full documentation bundle is available at https://docs.halo.run/llms-full.txt.

# 在插件中提供主题模板

当你在插件中创建了自己的自定义模型后，你可能需要在主题端提供一个模板来展示这些数据，这一般有两种方式：

1. 插件规定模板名称，由主题选择性适配，如瞬间插件提供了 `/moments` 的路由渲染 `moments.html` 模板，主题可以选择性地提供 `moments.html` 模板来展示瞬间数据。
2. 插件提供默认模板，当主题没有提供对应的模板时，使用默认模板，主题提供了对应的模板时，使用主题提供的模板。

## 创建一个模板

首先，你需要在插件的 `resources` 目录下创建一个 `templates` 目录，然后在 `templates` 目录下提供你的模板，例如：

```tree
├── templates
│   ├── moments.html
```

然后提供一个路由用于渲染这个模板，例如：

```java
import run.halo.app.theme.TemplateNameResolver;
import run.halo.app.theme.router.ModelConst;

@RequiredArgsConstructor
@Configuration(proxyBeanMethods = false)
public class MomentRouter {
   private final TemplateNameResolver templateNameResolver;

   @Bean
   RouterFunction<ServerResponse> momentRouterFunction() {
      return route(GET("/moments"), this::renderMomentPage).build();
   }

    Mono<ServerResponse> renderMomentPage(ServerRequest request) {
        // 或许你需要准备你需要提供给模板的默认数据，非必须
        var model = new HashMap<String, Object>();
        model.put("moments", List.of());
        model.put(ModelConst.TEMPLATE_ID, "plugin:plugin-moment:moments");
        return templateNameResolver.resolveTemplateNameOrDefault(request.exchange(), "moments")
            .flatMap(templateName -> ServerResponse.ok().render(templateName, model));
    }
}
```

使用 `TemplateNameResolver` 来解析模板名称，如果主题提供了对应的模板，那么就使用主题提供的模板，否则使用插件提供的模板，如果直接返回模板名称，那么只会使用主题提供的模板，如果主题没有提供对应的模板，那么会抛出异常。

`ModelConst.TEMPLATE_ID` 对应模板变量 `_templateId`。建议使用 `plugin:<plugin.yaml metadata.name>:<page>` 形式的稳定值，便于 Head 处理器、SEO 插件和其他渲染扩展识别插件页面。模板名称、模型字段和 `_templateId` 都属于主题集成契约，不应在兼容版本中随意修改。

## 复用当前主题页面布局

从 Halo 2.26.0 开始，如果插件提供的前台页面希望复用当前主题的页头、页脚和整体页面外壳，可以在插件模板中调用 `layout :: html(...)`：

```html title="src/main/resources/templates/moments.html"
<!DOCTYPE html>
<html
  xmlns:th="https://www.thymeleaf.org"
  th:replace="~{layout :: html(head = ~{::head}, content = ~{::content})}"
>
  <th:block th:fragment="head">
    <title>瞬间 - [[${site.title}]]</title>
  </th:block>
  <th:block th:fragment="content">
    <section>
      <!-- 插件页面正文 -->
    </section>
  </th:block>
</html>
```

`layout` 是 Halo 保留的集成模板名。当当前主题提供符合契约的 `templates/layout.html` 时，插件页面会使用主题布局；否则会使用 Halo 内置的 fallback 布局。完整契约可参考[页面布局契约](https://docs.halo.run/developer-guide/theme/page-layout.md)。

## 模板片段

如果你的默认模板不止一个，你可能需要通过模板片段来抽取一些公共的部分，例如，你的插件提供了一个 `moments.html` 模板，你可能需要抽取一些公共的部分，例如头部、尾部等，你可以这样做：

```tree
├── templates
│   ├── moments.html
│   ├── fragments
│   │   ├── layout.html
```

然后定义一个 `layout.html` 模板，例如：

```html
<!DOCTYPE html th:fragment="layoutHtml(content)">
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title th:text="${title}">Moment</title>
  </head>
  <body>
    <div class="container">
      <th:block th:replace="${content}" />
    </div>
  </body>
</html>
```

那么使用 `layout.html` 模板中提供的 `fragment` 时，你需要这样做：

```html
<div
  th:replace="~{plugin:plugin-moment:fragments/layout :: layoutHtml(content = ~{::content})}"
>
  <th:block th:fragment="content"> Hello World </th:block>
</div>
```

`plugin:plugin-moment:fragments/layout` 即为使用 `layout.html` 模板的路径，必须以 `plugin:<your-plugin-name>:`前缀作为开头，`fragments/layout` 为模板相对于 `resources/templates` 的路径，`<your-plugin-name>` 即为你的插件名称。

**总结：**

1. 定义模板片段时与主题端定义模板片段时一样
2. 使用模板片段时，必须以 `plugin:<your-plugin-name>:` 前缀作为开头，后跟模板相对于 `resources/templates` 的路径，例如 `plugin:plugin-moment:fragments/layout`，`plugin-moment` 即为你的插件名称，`fragments/layout` 为模板相对于 `resources/templates` 的路径。

参考：[Thymeleaf 模板片段](https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#including-template-fragments)
