docs: update documentation for Halo 2.10 (#262)

为 Halo 2.10.0 更新文档。

/kind documentation

```release-note
None 
```
wan92hen-patch-1
Ryan Wang 1 year ago committed by GitHub
parent f1e2d27c8a
commit ba0c62ef6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -28,7 +28,7 @@ git checkout v2.4.0
``` ```
:::tip :::tip
请务必按照以上要求切换到最新的 tag而不是直接使用 main 分支构建main 分支是我们的开发分支。此文档以 `2.9.0` 为例,查看最新的 tag 可使用 `git tag --column` 查看。 请务必按照以上要求切换到最新的 tag而不是直接使用 main 分支构建main 分支是我们的开发分支。此文档以 `2.10.0` 为例,查看最新的 tag 可使用 `git tag --column` 查看。
::: :::
:::warning :::warning
@ -63,7 +63,7 @@ pnpm build
## 构建 Fat Jar ## 构建 Fat Jar
构建之前需要修改 `gradle.properties` 中的 `version` 为当前 tag 的版本号,如:`version=2.9.0` 构建之前需要修改 `gradle.properties` 中的 `version` 为当前 tag 的版本号,如:`version=2.10.0`
```bash ```bash
cd path/to/halo cd path/to/halo
@ -89,18 +89,18 @@ cd path/to/halo
./gradlew clean build -x check ./gradlew clean build -x check
``` ```
构建完成之后,在 halo 项目下产生的 `application/build/libs/halo-2.9.0.jar` 即为构建完成的文件。 构建完成之后,在 halo 项目下产生的 `application/build/libs/halo-2.10.0.jar` 即为构建完成的文件。
## 构建 Docker 镜像 ## 构建 Docker 镜像
在进行之前,请确保已经完成上述操作,最终需要确认在 halo 项目的 `application/build/libs/` 目录已经包含了 `halo-2.9.0.jar` 文件。 在进行之前,请确保已经完成上述操作,最终需要确认在 halo 项目的 `application/build/libs/` 目录已经包含了 `halo-2.10.0.jar` 文件。
```bash ```bash
cd path/to/halo cd path/to/halo
``` ```
```bash ```bash
docker build -t halo-dev/halo:2.9.0 . docker build -t halo-dev/halo:2.10.0 .
``` ```
```bash ```bash

@ -20,18 +20,18 @@ import DockerArgs from "./slots/docker-args.md"
## 创建容器组 ## 创建容器组
可用的 Halo 2.9 的 Docker 镜像: 可用的 Halo 2.10 的 Docker 镜像:
- [halohub/halo](https://hub.docker.com/r/halohub/halo) - [halohub/halo](https://hub.docker.com/r/halohub/halo)
- [ghcr.io/halo-dev/halo](https://github.com/halo-dev/halo/pkgs/container/halo) - [ghcr.io/halo-dev/halo](https://github.com/halo-dev/halo/pkgs/container/halo)
:::info 注意 :::info 注意
目前 Halo 2 并未更新 Docker 的 latest 标签镜像,主要因为 Halo 2 不兼容 1.x 版本,防止使用者误操作。我们推荐使用固定版本的标签,比如 `halohub/halo:2.9` 或者 `halohub/halo:2.9.0`。 目前 Halo 2 并未更新 Docker 的 latest 标签镜像,主要因为 Halo 2 不兼容 1.x 版本,防止使用者误操作。我们推荐使用固定版本的标签,比如 `halohub/halo:2.10` 或者 `halohub/halo:2.10.0`。
- `halohub/halo:2.9`:表示最新的 2.9.x 版本,即每次发布 patch 版本都会同时更新 `halohub/halo:2.9` 镜像。 - `halohub/halo:2.10`:表示最新的 2.10.x 版本,即每次发布 patch 版本都会同时更新 `halohub/halo:2.10` 镜像。
- `halohub/halo:2.9.0`:表示一个具体的版本。 - `halohub/halo:2.10.0`:表示一个具体的版本。
后续文档以 `halohub/halo:2.9` 为例。 后续文档以 `halohub/halo:2.10` 为例。
::: :::
1. 在系统任意位置创建一个文件夹,此文档以 `~/halo` 为例。 1. 在系统任意位置创建一个文件夹,此文档以 `~/halo` 为例。
@ -59,7 +59,7 @@ import DockerArgs from "./slots/docker-args.md"
services: services:
halo: halo:
image: halohub/halo:2.9 image: halohub/halo:2.10
container_name: halo container_name: halo
restart: on-failure:3 restart: on-failure:3
depends_on: depends_on:
@ -117,7 +117,7 @@ import DockerArgs from "./slots/docker-args.md"
services: services:
halo: halo:
image: halohub/halo:2.9 image: halohub/halo:2.10
container_name: halo container_name: halo
restart: on-failure:3 restart: on-failure:3
depends_on: depends_on:
@ -181,7 +181,7 @@ import DockerArgs from "./slots/docker-args.md"
services: services:
halo: halo:
image: halohub/halo:2.9 image: halohub/halo:2.10
container_name: halo container_name: halo
restart: on-failure:3 restart: on-failure:3
volumes: volumes:
@ -205,7 +205,7 @@ import DockerArgs from "./slots/docker-args.md"
services: services:
halo: halo:
image: halohub/halo:2.9 image: halohub/halo:2.10
container_name: halo container_name: halo
restart: on-failure:3 restart: on-failure:3
network_mode: "host" network_mode: "host"
@ -268,7 +268,7 @@ import DockerArgs from "./slots/docker-args.md"
```yaml {3} ```yaml {3}
services: services:
halo: halo:
image: halohub/halo:2.9 image: halohub/halo:2.10
container_name: halo container_name: halo
``` ```
@ -338,7 +338,7 @@ networks:
services: services:
halo: halo:
image: halohub/halo:2.9 image: halohub/halo:2.10
container_name: halo container_name: halo
restart: on-failure:3 restart: on-failure:3
volumes: volumes:

@ -25,24 +25,24 @@ import DockerArgs from "./slots/docker-args.md"
## 使用 Docker 镜像 ## 使用 Docker 镜像
可用的 Halo 2.9 的 Docker 镜像: 可用的 Halo 2.10 的 Docker 镜像:
- [halohub/halo](https://hub.docker.com/r/halohub/halo) - [halohub/halo](https://hub.docker.com/r/halohub/halo)
- [ghcr.io/halo-dev/halo](https://github.com/halo-dev/halo/pkgs/container/halo) - [ghcr.io/halo-dev/halo](https://github.com/halo-dev/halo/pkgs/container/halo)
:::info 注意 :::info 注意
目前 Halo 2 并未更新 Docker 的 latest 标签镜像,主要因为 Halo 2 不兼容 1.x 版本,防止使用者误操作。我们推荐使用固定版本的标签,比如 `halohub/halo:2.9` 或者 `halohub/halo:2.9.0`。 目前 Halo 2 并未更新 Docker 的 latest 标签镜像,主要因为 Halo 2 不兼容 1.x 版本,防止使用者误操作。我们推荐使用固定版本的标签,比如 `halohub/halo:2.10` 或者 `halohub/halo:2.10.0`。
- `halohub/halo:2.9`:表示最新的 2.9.x 版本,即每次发布 patch 版本都会同时更新 `halohub/halo:2.9` 镜像。 - `halohub/halo:2.10`:表示最新的 2.10.x 版本,即每次发布 patch 版本都会同时更新 `halohub/halo:2.10` 镜像。
- `halohub/halo:2.9.0`:表示一个具体的版本。 - `halohub/halo:2.10.0`:表示一个具体的版本。
后续文档以 `halohub/halo:2.9` 为例。 后续文档以 `halohub/halo:2.10` 为例。
::: :::
1. 创建容器 1. 创建容器
```bash ```bash
docker run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.9 docker run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.10
``` ```
:::info :::info
@ -70,7 +70,7 @@ import DockerArgs from "./slots/docker-args.md"
1. 拉取新版本镜像 1. 拉取新版本镜像
```bash ```bash
docker pull halohub/halo:2.9 docker pull halohub/halo:2.10
``` ```
2. 停止运行中的容器 2. 停止运行中的容器
@ -93,5 +93,5 @@ import DockerArgs from "./slots/docker-args.md"
修改版本号后,按照最初安装的方式,重新创建容器即可。 修改版本号后,按照最初安装的方式,重新创建容器即可。
```bash ```bash
docker run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.9 docker run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.10
``` ```

@ -96,7 +96,7 @@ networks:
services: services:
halo: halo:
image: halohub/halo:2.9 image: halohub/halo:2.10
container_name: halo container_name: halo
restart: on-failure:3 restart: on-failure:3
volumes: volumes:

@ -50,25 +50,25 @@ Podman 采用无守护进程的包容性架构,因此可以更安全、更简
通过[前言](#前言)我们已经了解了 Podman ,其中提到 ***Podman 与 Docker 高度兼容*** ,正是因为 Podman 完全是为了替代 Docker 而诞生,所以原本的 Docker 生态中的镜像我们可以无需更改直接使用。 通过[前言](#前言)我们已经了解了 Podman ,其中提到 ***Podman 与 Docker 高度兼容*** ,正是因为 Podman 完全是为了替代 Docker 而诞生,所以原本的 Docker 生态中的镜像我们可以无需更改直接使用。
::: :::
可用的 Halo 2.9 的 Docker 镜像: 可用的 Halo 2.10 的 Docker 镜像:
- [halohub/halo](https://hub.docker.com/r/halohub/halo) - [halohub/halo](https://hub.docker.com/r/halohub/halo)
- [ghcr.io/halo-dev/halo](https://github.com/halo-dev/halo/pkgs/container/halo) - [ghcr.io/halo-dev/halo](https://github.com/halo-dev/halo/pkgs/container/halo)
:::info 注意 :::info 注意
目前 Halo 2 并未更新 Docker 的 latest 标签镜像,主要因为 Halo 2 不兼容 1.x 版本,防止使用者误操作。我们推荐使用固定版本的标签,比如 `halohub/halo:2.9` 或者 `halohub/halo:2.9.0`。 目前 Halo 2 并未更新 Docker 的 latest 标签镜像,主要因为 Halo 2 不兼容 1.x 版本,防止使用者误操作。我们推荐使用固定版本的标签,比如 `halohub/halo:2.10` 或者 `halohub/halo:2.10.0`。
- `halohub/halo:2.9`:表示最新的 2.9.x 版本,即每次发布 patch 版本都会同时更新 `halohub/halo:2.9` 镜像。 - `halohub/halo:2.10`:表示最新的 2.10.x 版本,即每次发布 patch 版本都会同时更新 `halohub/halo:2.10` 镜像。
- `halohub/halo:2.9.0`:表示一个具体的版本。 - `halohub/halo:2.10.0`:表示一个具体的版本。
后续文档以 `halohub/halo:2.9` 为例。 后续文档以 `halohub/halo:2.10` 为例。
::: :::
1. 创建容器 1. 创建容器
```bash ```bash
mkdir -p ~/.halo2 mkdir -p ~/.halo2
podman run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.9 podman run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.10
``` ```
:::info :::info
@ -96,7 +96,7 @@ Podman 采用无守护进程的包容性架构,因此可以更安全、更简
1. 拉取新版本镜像 1. 拉取新版本镜像
```bash ```bash
podman pull halohub/halo:2.9 podman pull halohub/halo:2.10
``` ```
2. 停止运行中的容器 2. 停止运行中的容器
@ -120,7 +120,7 @@ Podman 采用无守护进程的包容性架构,因此可以更安全、更简
```bash ```bash
mkdir -p ~/.halo2 mkdir -p ~/.halo2
podman run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.9 podman run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.10
``` ```
## 使用 [Podman Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html) ## 使用 [Podman Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html)
@ -156,7 +156,7 @@ Podman 没有和 Docker 类似的管理进程,在低配置的主机上更友
Environment=TZ=Asia/Shanghai Environment=TZ=Asia/Shanghai
Volume=/opt/podman-data/halo:/.halo Volume=/opt/podman-data/halo:/.halo
PublishPort=127.0.0.1:8090:8090 PublishPort=127.0.0.1:8090:8090
Image=ghcr.io/halo-dev/halo:2.9 Image=ghcr.io/halo-dev/halo:2.10
Exec=--halo.external-url=https://localhost:8090 --spring.sql.init.platform=postgresql --spring.r2dbc.url=r2dbc:pool:postgresql://127.0.0.1:5432/my-db --spring.r2dbc.username=my-user --spring.r2dbc.password=my-password --halo.cache.page.disabled=false Exec=--halo.external-url=https://localhost:8090 --spring.sql.init.platform=postgresql --spring.r2dbc.url=r2dbc:pool:postgresql://127.0.0.1:5432/my-db --spring.r2dbc.username=my-user --spring.r2dbc.password=my-password --halo.cache.page.disabled=false
[Service] [Service]
@ -185,7 +185,7 @@ Podman Quadlet 解析:
`[Container]` 部分: `[Container]` 部分:
- `AutoUpdate=registry`指定了自动拉取容器。假设后续Halo镜像支持了`latest`标签,你需要`systemctl enable --now podman-auto-update.timer`以启用容器自动更新。本文示例`ghcr.io/halo-dev/halo:2.9`,将会自动更新适用与`2.9`版本的patch例如您创建容器时是`2.9.1`,在官方发布`2.9.2`版本时,容器会自动更新到`2.9.2`。 - `AutoUpdate=registry`指定了自动拉取容器。假设后续Halo镜像支持了`latest`标签,你需要`systemctl enable --now podman-auto-update.timer`以启用容器自动更新。本文示例`ghcr.io/halo-dev/halo:2.10`,将会自动更新适用与`2.10`版本的patch例如您创建容器时是`2.10.1`,在官方发布`2.10.2`版本时,容器会自动更新到`2.10.2`。
- `ContainerName=`指定了 systemd 将生成的服务名称。 - `ContainerName=`指定了 systemd 将生成的服务名称。
- `User=60000 Group=60000 UserNS=keep-id:uid=60000,gid=60000` 限制容器以 id 60000 的用户运行提高安全性。注意这个id 60000请根据你实际想要运行的用户名来修改可通过`id user`获得你的用户的id. - `User=60000 Group=60000 UserNS=keep-id:uid=60000,gid=60000` 限制容器以 id 60000 的用户运行提高安全性。注意这个id 60000请根据你实际想要运行的用户名来修改可通过`id user`获得你的用户的id.
- `Environment=`字段指定了容器的环境变量,其中你需要注意的是`Environment=HALO_WORK_DIR="/.halo"` `Environment=SPRING_CONFIG_LOCATION="optional:classpath:/;optional:file:/.halo/"`这两个变量中的`/.halo`路径。 - `Environment=`字段指定了容器的环境变量,其中你需要注意的是`Environment=HALO_WORK_DIR="/.halo"` `Environment=SPRING_CONFIG_LOCATION="optional:classpath:/;optional:file:/.halo/"`这两个变量中的`/.halo`路径。
@ -228,7 +228,7 @@ Podman Quadlet 解析:
ContainerName=halo ContainerName=halo
Volume=/opt/podman-data/halo:/root/.halo Volume=/opt/podman-data/halo:/root/.halo
PublishPort=127.0.0.1:8090:8090 PublishPort=127.0.0.1:8090:8090
Image=ghcr.io/halo-dev/halo:2.9 Image=ghcr.io/halo-dev/halo:2.10
Exec=--halo.external-url=https://localhost:8090 --spring.sql.init.platform=postgresql --spring.r2dbc.url=r2dbc:pool:postgresql://127.0.0.1:5432/my-db --spring.r2dbc.username=my-user --spring.r2dbc.password=my-password --halo.cache.page.disabled=false Exec=--halo.external-url=https://localhost:8090 --spring.sql.init.platform=postgresql --spring.r2dbc.url=r2dbc:pool:postgresql://127.0.0.1:5432/my-db --spring.r2dbc.username=my-user --spring.r2dbc.password=my-password --halo.cache.page.disabled=false
[Service] [Service]

@ -34,7 +34,7 @@ slug: /
## 快速开始 ## 快速开始
```bash ```bash
docker run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.9 docker run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.10
``` ```
以上仅作为体验使用,详细部署文档请查阅:<https://docs.halo.run/getting-started/install/docker-compose> 以上仅作为体验使用,详细部署文档请查阅:<https://docs.halo.run/getting-started/install/docker-compose>

@ -3,7 +3,7 @@ title: 备份与恢复
description: 备份与恢复功能说明 description: 备份与恢复功能说明
--- ---
从 Halo 2.9 开始Halo 内置了备份和恢复的功能,可以在 Console 中一键备份和恢复完整的数据。 从 Halo 2.8 开始Halo 内置了备份和恢复的功能,可以在 Console 中一键备份和恢复完整的数据。
## 备份 ## 备份

@ -101,7 +101,7 @@ server {
--name halo-1 \ --name halo-1 \
-p 8090:8090 \ -p 8090:8090 \
-v ~/.halo2:/root/.halo2 \ -v ~/.halo2:/root/.halo2 \
halohub/halo:2.9 \ halohub/halo:2.10 \
# 第二个 Halo 容器 # 第二个 Halo 容器
docker run \ docker run \
@ -109,7 +109,7 @@ server {
--name halo-2 \ --name halo-2 \
-p 8091:8090 \ -p 8091:8090 \
-v ~/.halo2_2:/root/.halo2 \ -v ~/.halo2_2:/root/.halo2 \
halohub/halo:2.9 \ halohub/halo:2.10 \
``` ```
更多 Docker 相关的教程请参考:[使用 Docker 部署 Halo](../getting-started/install/docker.md) 更多 Docker 相关的教程请参考:[使用 Docker 部署 Halo](../getting-started/install/docker.md)

@ -34,11 +34,11 @@ const config = {
showLastUpdateAuthor: true, showLastUpdateAuthor: true,
remarkPlugins: [math, mermaid], remarkPlugins: [math, mermaid],
rehypePlugins: [katex], rehypePlugins: [katex],
lastVersion: "2.9", lastVersion: "2.10",
versions: { versions: {
current: { current: {
label: "2.10.0-SNAPSHOT", label: "2.11.0-SNAPSHOT",
path: "2.10.0-SNAPSHOT", path: "2.11.0-SNAPSHOT",
}, },
}, },
}, },
@ -284,18 +284,19 @@ const config = {
if (existingPath.startsWith("/1.4/")) { if (existingPath.startsWith("/1.4/")) {
return [existingPath.replace("/1.4/", "/1.4.17/")]; return [existingPath.replace("/1.4/", "/1.4.17/")];
} }
if (existingPath.startsWith("/2.10.0-SNAPSHOT/")) { if (existingPath.startsWith("/2.11.0-SNAPSHOT/")) {
return [ return [
existingPath.replace("/2.10.0-SNAPSHOT/", "/2.0.0-SNAPSHOT/"), existingPath.replace("/2.11.0-SNAPSHOT/", "/2.0.0-SNAPSHOT/"),
existingPath.replace("/2.10.0-SNAPSHOT/", "/2.1.0-SNAPSHOT/"), existingPath.replace("/2.11.0-SNAPSHOT/", "/2.1.0-SNAPSHOT/"),
existingPath.replace("/2.10.0-SNAPSHOT/", "/2.2.0-SNAPSHOT/"), existingPath.replace("/2.11.0-SNAPSHOT/", "/2.2.0-SNAPSHOT/"),
existingPath.replace("/2.10.0-SNAPSHOT/", "/2.3.0-SNAPSHOT/"), existingPath.replace("/2.11.0-SNAPSHOT/", "/2.3.0-SNAPSHOT/"),
existingPath.replace("/2.10.0-SNAPSHOT/", "/2.4.0-SNAPSHOT/"), existingPath.replace("/2.11.0-SNAPSHOT/", "/2.4.0-SNAPSHOT/"),
existingPath.replace("/2.10.0-SNAPSHOT/", "/2.5.0-SNAPSHOT/"), existingPath.replace("/2.11.0-SNAPSHOT/", "/2.5.0-SNAPSHOT/"),
existingPath.replace("/2.10.0-SNAPSHOT/", "/2.6.0-SNAPSHOT/"), existingPath.replace("/2.11.0-SNAPSHOT/", "/2.6.0-SNAPSHOT/"),
existingPath.replace("/2.10.0-SNAPSHOT/", "/2.7.0-SNAPSHOT/"), existingPath.replace("/2.11.0-SNAPSHOT/", "/2.7.0-SNAPSHOT/"),
existingPath.replace("/2.10.0-SNAPSHOT/", "/2.8.0-SNAPSHOT/"), existingPath.replace("/2.11.0-SNAPSHOT/", "/2.8.0-SNAPSHOT/"),
existingPath.replace("/2.10.0-SNAPSHOT/", "/2.9.0-SNAPSHOT/"), existingPath.replace("/2.11.0-SNAPSHOT/", "/2.9.0-SNAPSHOT/"),
existingPath.replace("/2.11.0-SNAPSHOT/", "/2.10.0-SNAPSHOT/"),
]; ];
} }
return undefined; return undefined;

@ -41,6 +41,18 @@
"message": "警告", "message": "警告",
"description": "The default label used for the Caution admonition (:::caution)" "description": "The default label used for the Caution admonition (:::caution)"
}, },
"theme.blog.archive.title": {
"message": "历史博文",
"description": "The page & hero title of the blog archive page"
},
"theme.blog.archive.description": {
"message": "历史博文",
"description": "The page & hero description of the blog archive page"
},
"theme.BackToTopButton.buttonAriaLabel": {
"message": "回到顶部",
"description": "The ARIA label for the back to top button"
},
"theme.blog.paginator.navAriaLabel": { "theme.blog.paginator.navAriaLabel": {
"message": "博文列表分页导航", "message": "博文列表分页导航",
"description": "The ARIA label for the blog pagination" "description": "The ARIA label for the blog pagination"
@ -53,17 +65,17 @@
"message": "较旧的博文", "message": "较旧的博文",
"description": "The label used to navigate to the older blog posts page (next page)" "description": "The label used to navigate to the older blog posts page (next page)"
}, },
"theme.BackToTopButton.buttonAriaLabel": { "theme.blog.post.paginator.navAriaLabel": {
"message": "回到顶部", "message": "博文分页导航",
"description": "The ARIA label for the back to top button" "description": "The ARIA label for the blog posts pagination"
}, },
"theme.blog.archive.title": { "theme.blog.post.paginator.newerPost": {
"message": "历史博文", "message": "较新一篇",
"description": "The page & hero title of the blog archive page" "description": "The blog post button label to navigate to the newer/previous post"
}, },
"theme.blog.archive.description": { "theme.blog.post.paginator.olderPost": {
"message": "历史博文", "message": "较旧一篇",
"description": "The page & hero description of the blog archive page" "description": "The blog post button label to navigate to the older/next post"
}, },
"theme.blog.post.plurals": { "theme.blog.post.plurals": {
"message": "{count} 篇博文", "message": "{count} 篇博文",
@ -77,18 +89,6 @@
"message": "查看所有标签", "message": "查看所有标签",
"description": "The label of the link targeting the tag list page" "description": "The label of the link targeting the tag list page"
}, },
"theme.blog.post.paginator.navAriaLabel": {
"message": "博文分页导航",
"description": "The ARIA label for the blog posts pagination"
},
"theme.blog.post.paginator.newerPost": {
"message": "较新一篇",
"description": "The blog post button label to navigate to the newer/previous post"
},
"theme.blog.post.paginator.olderPost": {
"message": "较旧一篇",
"description": "The blog post button label to navigate to the older/next post"
},
"theme.colorToggle.ariaLabel": { "theme.colorToggle.ariaLabel": {
"message": "切换浅色/暗黑模式(当前为{mode}", "message": "切换浅色/暗黑模式(当前为{mode}",
"description": "The ARIA label for the navbar color mode toggle" "description": "The ARIA label for the navbar color mode toggle"
@ -144,6 +144,10 @@
"message": "编辑此页", "message": "编辑此页",
"description": "The link label to edit the current page" "description": "The link label to edit the current page"
}, },
"theme.common.headingLinkTitle": {
"message": "{heading}的直接链接",
"description": "Title for link to heading"
},
"theme.lastUpdated.atDate": { "theme.lastUpdated.atDate": {
"message": "于 {date} ", "message": "于 {date} ",
"description": "The words used to describe on which date a page has been last updated" "description": "The words used to describe on which date a page has been last updated"
@ -156,10 +160,6 @@
"message": "最后{byUser}{atDate}更新", "message": "最后{byUser}{atDate}更新",
"description": "The sentence used to display when a page has been last updated, and by who" "description": "The sentence used to display when a page has been last updated, and by who"
}, },
"theme.common.headingLinkTitle": {
"message": "{heading}的直接链接",
"description": "Title for link to heading"
},
"theme.docs.tagDocListPageTitle.nDocsTagged": { "theme.docs.tagDocListPageTitle.nDocsTagged": {
"message": "{count} 篇文档带有标签", "message": "{count} 篇文档带有标签",
"description": "Pluralized label for \"{count} docs tagged\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" "description": "Pluralized label for \"{count} docs tagged\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
@ -228,6 +228,10 @@
"message": "阅读需 {readingTime} 分钟", "message": "阅读需 {readingTime} 分钟",
"description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" "description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
}, },
"theme.docs.sidebar.navAriaLabel": {
"message": "文档侧边栏",
"description": "The ARIA label for the sidebar navigation"
},
"theme.docs.sidebar.collapseButtonTitle": { "theme.docs.sidebar.collapseButtonTitle": {
"message": "收起侧边栏", "message": "收起侧边栏",
"description": "The title attribute for collapse button of doc sidebar" "description": "The title attribute for collapse button of doc sidebar"
@ -240,22 +244,18 @@
"message": "主页面", "message": "主页面",
"description": "The ARIA label for the home page in the breadcrumbs" "description": "The ARIA label for the home page in the breadcrumbs"
}, },
"theme.docs.sidebar.navAriaLabel": {
"message": "文档侧边栏",
"description": "The ARIA label for the sidebar navigation"
},
"theme.docs.sidebar.closeSidebarButtonAriaLabel": { "theme.docs.sidebar.closeSidebarButtonAriaLabel": {
"message": "关闭导航栏", "message": "关闭导航栏",
"description": "The ARIA label for close button of mobile sidebar" "description": "The ARIA label for close button of mobile sidebar"
}, },
"theme.docs.sidebar.toggleSidebarButtonAriaLabel": {
"message": "切换导航栏",
"description": "The ARIA label for hamburger menu button of mobile navigation"
},
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": { "theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": {
"message": "← 回到主菜单", "message": "← 回到主菜单",
"description": "The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)" "description": "The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)"
}, },
"theme.docs.sidebar.toggleSidebarButtonAriaLabel": {
"message": "切换导航栏",
"description": "The ARIA label for hamburger menu button of mobile navigation"
},
"theme.docs.sidebar.expandButtonTitle": { "theme.docs.sidebar.expandButtonTitle": {
"message": "展开侧边栏", "message": "展开侧边栏",
"description": "The ARIA label and title attribute for expand button of doc sidebar" "description": "The ARIA label and title attribute for expand button of doc sidebar"
@ -267,38 +267,6 @@
"theme.SearchBar.seeAll": { "theme.SearchBar.seeAll": {
"message": "查看全部 {count} 个结果" "message": "查看全部 {count} 个结果"
}, },
"theme.SearchPage.documentsFound.plurals": {
"message": "找到 {count} 份文件",
"description": "Pluralized label for \"{count} documents found\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
},
"theme.SearchPage.existingResultsTitle": {
"message": "「{query}」的搜索结果",
"description": "The search page title for non-empty query"
},
"theme.SearchPage.emptyResultsTitle": {
"message": "在文档中搜索",
"description": "The search page title for empty query"
},
"theme.SearchPage.inputPlaceholder": {
"message": "在此输入搜索字词",
"description": "The placeholder for search page input"
},
"theme.SearchPage.inputLabel": {
"message": "搜索",
"description": "The ARIA label for search page input"
},
"theme.SearchPage.algoliaLabel": {
"message": "通过 Algolia 搜索",
"description": "The ARIA label for Algolia mention"
},
"theme.SearchPage.noResultsText": {
"message": "未找到任何结果",
"description": "The paragraph for empty search result"
},
"theme.SearchPage.fetchingNewResults": {
"message": "正在获取新的搜索结果...",
"description": "The paragraph for fetching new search results"
},
"theme.SearchBar.label": { "theme.SearchBar.label": {
"message": "搜索", "message": "搜索",
"description": "The ARIA label and placeholder for search button" "description": "The ARIA label and placeholder for search button"
@ -395,6 +363,38 @@
"message": "搜索文档", "message": "搜索文档",
"description": "The placeholder of the input of the DocSearch pop-up modal" "description": "The placeholder of the input of the DocSearch pop-up modal"
}, },
"theme.SearchPage.documentsFound.plurals": {
"message": "找到 {count} 份文件",
"description": "Pluralized label for \"{count} documents found\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
},
"theme.SearchPage.existingResultsTitle": {
"message": "「{query}」的搜索结果",
"description": "The search page title for non-empty query"
},
"theme.SearchPage.emptyResultsTitle": {
"message": "在文档中搜索",
"description": "The search page title for empty query"
},
"theme.SearchPage.inputPlaceholder": {
"message": "在此输入搜索字词",
"description": "The placeholder for search page input"
},
"theme.SearchPage.inputLabel": {
"message": "搜索",
"description": "The ARIA label for search page input"
},
"theme.SearchPage.algoliaLabel": {
"message": "通过 Algolia 搜索",
"description": "The ARIA label for Algolia mention"
},
"theme.SearchPage.noResultsText": {
"message": "未找到任何结果",
"description": "The paragraph for empty search result"
},
"theme.SearchPage.fetchingNewResults": {
"message": "正在获取新的搜索结果...",
"description": "The paragraph for fetching new search results"
},
"theme.ErrorPageContent.tryAgain": { "theme.ErrorPageContent.tryAgain": {
"message": "重试", "message": "重试",
"description": "The label of the button to try again rendering when the React error boundary captures an error" "description": "The label of the button to try again rendering when the React error boundary captures an error"

@ -1,6 +1,6 @@
{ {
"version.label": { "version.label": {
"message": "2.10.0-SNAPSHOT", "message": "2.11.0-SNAPSHOT",
"description": "The label for version current" "description": "The label for version current"
}, },
"sidebar.tutorialSidebar.category.入门": { "sidebar.tutorialSidebar.category.入门": {

@ -0,0 +1,62 @@
{
"version.label": {
"message": "2.10",
"description": "The label for version 2.10"
},
"sidebar.tutorialSidebar.category.入门": {
"message": "入门",
"description": "The label for category 入门 in sidebar tutorialSidebar"
},
"sidebar.tutorialSidebar.category.安装指南": {
"message": "安装指南",
"description": "The label for category 安装指南 in sidebar tutorialSidebar"
},
"sidebar.tutorialSidebar.category.其他指南": {
"message": "其他指南",
"description": "The label for category 其他指南 in sidebar tutorialSidebar"
},
"sidebar.tutorialSidebar.category.用户指南": {
"message": "用户指南",
"description": "The label for category 用户指南 in sidebar tutorialSidebar"
},
"sidebar.tutorialSidebar.category.开发者指南": {
"message": "开发者指南",
"description": "The label for category 开发者指南 in sidebar tutorialSidebar"
},
"sidebar.tutorialSidebar.category.系统开发": {
"message": "系统开发",
"description": "The label for category 系统开发 in sidebar tutorialSidebar"
},
"sidebar.tutorialSidebar.category.插件开发": {
"message": "插件开发",
"description": "The label for category 插件开发 in sidebar tutorialSidebar"
},
"sidebar.tutorialSidebar.category.基础": {
"message": "基础",
"description": "The label for category 基础 in sidebar tutorialSidebar"
},
"sidebar.tutorialSidebar.category.示例": {
"message": "示例",
"description": "The label for category 示例 in sidebar tutorialSidebar"
},
"sidebar.tutorialSidebar.category.API 参考": {
"message": "API 参考",
"description": "The label for category API 参考 in sidebar tutorialSidebar"
},
"sidebar.tutorialSidebar.category.主题开发": {
"message": "主题开发",
"description": "The label for category 主题开发 in sidebar tutorialSidebar"
},
"sidebar.tutorialSidebar.category.模板变量": {
"message": "模板变量",
"description": "The label for category 模板变量 in sidebar tutorialSidebar"
},
"sidebar.tutorialSidebar.category.Finder API": {
"message": "Finder API",
"description": "The label for category Finder API in sidebar tutorialSidebar"
},
"sidebar.tutorialSidebar.category.参与贡献": {
"message": "参与贡献",
"description": "The label for category 参与贡献 in sidebar tutorialSidebar"
}
}

@ -0,0 +1,16 @@
---
title: 关于文档
description: 关于本文档站点的一些说明
---
:::note
此文档使用 [Docusaurus](https://docusaurus.io/) 搭建,感谢 [Docusaurus](https://github.com/facebook/docusaurus) 社区所做的贡献。
:::
## 参与贡献
:::tip
如果你发现文档中有不正确或者需要添加的内容,非常欢迎参与到文档编辑当中。
:::
当前文档的仓库地址为 [halo-dev/docs](https://github.com/halo-dev/docs) ,所以你可以 fork 此仓库,修改之后提交 `Pull request` 等待我们合并即可。

@ -0,0 +1,28 @@
---
title: 问题反馈
description: 问题反馈渠道及指南
---
:::info
如果您在使用过程中遇到了什么问题,您可以通过下面的方式反馈,但请尽量按照要求提出反馈。
:::
## GitHub Issues
链接:<https://github.com/halo-dev/halo/issues>
如果你在使用过程中,遇到了一些 bug 或者需要添加某些新特性,请尽量在 GitHub Issues 进行反馈,这非常有助于我们跟踪解决此问题,您也可以很方便的接收到处理状态。
建议步骤:
1. 在 [Issues 列表](https://github.com/halo-dev/halo/issues) 搜索相关问题,看看是否有其他人已经提到了此问题。
2. 如果当前还没有人遇到您类似的问题,那么请点击右上角的 `New issue` 按钮创建新的 issue。
3. 选择正确的反馈类型。
4. 请尽可能详细的按照模板填写内容。
5. 点击 `Submit new issue` 提交 issue。
## Halo 官方社区
链接:<https://bbs.halo.run>
此平台主要目的用于与其他 Halo 用户进行交流。但如果您对 GitHub 不是很熟悉或者没有账号,您也可以在此平台进行反馈。

@ -0,0 +1,110 @@
---
title: 代码贡献
description: 代码贡献指南
---
欢迎关注并有想法参与 Halo 的开发,以下是关于如何参与到 Halo 项目的指南,仅供参考。
## 发现 Issue
所有的代码尽可能都有依据Issue不是凭空产生。
### 寻找一个 Good First Issue
> 这个步骤非常适合首次贡献者。
在 [halo-dev](https://github.com/halo-dev) 和 [halo-sigs](https://github.com/halo-sigs) 组织下,有非常多的仓库。每个仓库下都有可能包含一些“首次贡献者”友好的 Issue主要是为了给贡献者提供一个友好的体验。 该类 Issue
一般会用 `good-first-issue` 标签标记。标签 `good-first-issue` 表示该 Issue 不需要对 Halo 有深入的理解也能够参与。
请点击:[good-first-issue](https://github.com/issues?q=org%3Ahalo-dev+is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22+no%3Aassignee+)
查看关于 Halo 的 Good First Issue。
### 认领 Issue
若对任何一个 Issue 感兴趣,请尝试在 Issue 进行回复,讨论解决 Issue 的思路。确定后可直接通过 `/assign` 或者 `/assign @GitHub 用户名` 认领这个
Issue。这样可避免两位贡献者在同一个问题上花时间。
## 代码贡献步骤
1. Fork 此仓库
点击 Halo 仓库主页右上角的 `Fork` 按钮即可。
2. Clone 仓库到本地
```bash
git clone https://github.com/{YOUR_USERNAME}/halo --recursive
# 或者 git clone git@github.com:{YOUR_USERNAME}/halo.git --recursive
```
3. 添加主仓库
添加主仓库方便未来同步主仓库最新的 commits 以及创建新的分支。
```bash
git remote add upstream https://github.com/halo-dev/halo.git
# 或者 git remote add upstream git@github.com:halo-dev/halo.git
git fetch upstream main
```
4. 创建新的开发分支
我们需要从主仓库的主分支创建一个新的开发分支。
```bash
git checkout upstream/main
git checkout -b {BRANCH_NAME}
```
5. 提交代码
```bash
git add .
git commit -s -m "Fix a bug king"
git push origin {BRANCH_NAME}
```
6. 合并主分支
在提交 Pull Request 之前,尽量保证当前分支和主分支的代码尽可能同步,这时需要我们手动操作。示例:
```bash
git fetch upstream/main
git merge upstream/main
git push origin {BRANCH_NAME}
```
## Pull Request
进入此阶段说明已经完成了代码的编写,测试和自测,并且准备好接受 Code Review。
### 创建 Pull Request
回到自己的仓库页面,选择 `New pull request` 按钮,创建 `Pull request` 到原仓库的 `main` 分支。
然后等待我们 Review 即可,如有 `Change Request`,再本地修改之后再次提交即可。
提交 Pull Request 的注意事项:
- 提交 Pull Request 请充分自测。
- 每个 Pull Request 尽量只解决一个 Issue特殊情况除外。
- 应尽可能多的添加单元测试,其他测试(集成测试和 E2E 测试)可看情况添加。
- 不论需要解决的 Issue 发生在哪个版本,提交 Pull Request 的时候,请将主仓库的主分支设置为 `main`。例如:即使某个 Bug 于 Halo 2.0.x 被发现,但是提交 Pull Request 仍只针对
`main` 分支,等待 Pull Request 合并之后,我们会通过 `/cherrypick release-2.0` 或者 `/cherry-pick release-2.1` 指令将此 Pull Request
的修改应用到 `release-2.0``release-2.1` 分支上。
### 更新 commits
Code Review 阶段可能需要 Pull Request 作者重新修改代码,请直接在当前分支 commit 并 push 即可,无需关闭并重新提交 Pull Request。示例
```bash
git add .
git commit -s -m "Refactor some code according code review"
git push origin bug/king
```
同时,若已经进入 Code Review 阶段,请不要强制推送 commits 到当前分支。否则 Reviewers 需要从头开始 Code Review。
### 开发规范
请参考 [https://docs.halo.run/developer-guide/core/code-style](https://docs.halo.run/developer-guide/core/code-style)
,请确保所有代码格式化之后再提交。

@ -0,0 +1,89 @@
---
title: 元数据表单定义
---
在 Halo 2.0,所有的模型都包含了 `metadata.annotations` 字段,用于存储元数据信息。元数据信息可以用于存储一些自定义的信息,可以等同于扩展字段。此文档主要介绍如何在 Halo 中为具体的模型定义元数据编辑表单,至于如何在插件或者主题模板中使用,请看插件或者主题的文档。
定义元数据编辑表单同样使用 `FormKit Schema`,但和主题或插件的定义方式稍有不同,其中输入组件类型可参考 [表单定义](./form-schema.md)。
:::info 提示
因为 `metadata.annotations` 是一个键值都为字符串类型的对象所以表单项的值必须为字符串类型。这就意味着FormKit 的 `number`、`group`、`repeater` 等类型的输入组件都不能使用。
:::
## AnnotationSetting 资源定义方式
```yaml title="annotation-setting.yaml"
apiVersion: v1alpha1
kind: AnnotationSetting
metadata:
generateName: annotation-setting-
spec:
targetRef:
group: content.halo.run
kind: Post
formSchema:
- $formkit: "text"
name: "download"
label: "下载地址"
- $formkit: "text"
name: "version"
label: "版本"
```
以上定义为文章模型添加了两个元数据字段,分别为 `download``version`,分别对应了下载地址和版本号,最终效果:
![Annotation Setting Preview](/img/annotation-setting/annotation-setting-preview.png)
字段说明:
1. `metadata.generateName`:可以不做修改,最终会以这个值当做前缀生成一个唯一的名称。
2. `spec.targetRef`:模型的关联,即为哪个模型添加元数据表单,目前支持的模型可查看下方的列表。
3. `spec.formSchema`:表单的定义,使用 FormKit Schema 来定义。虽然我们使用的 YAML但与 FormKit Schema 完全一致。
targetRef 支持列表:
| 对应模型 | group | kind |
| ---------- | ---------------- | ---------- |
| 文章 | content.halo.run | Post |
| 自定义页面 | content.halo.run | SinglePage |
| 文章分类 | content.halo.run | Category |
| 文章标签 | content.halo.run | Tag |
| 菜单项 | `""` | MenuItem |
| 用户 | `""` | User |
## 为多个模型定义表单
考虑到某些情况可能会同时为多个模型添加元数据表单,推荐在一个 `yaml` 文件中使用 `---` 来分割多个资源定义,如下:
```yaml title="annotation-setting.yaml"
apiVersion: v1alpha1
kind: AnnotationSetting
metadata:
generateName: annotation-setting-
spec:
targetRef:
group: content.halo.run
kind: Post
formSchema:
- $formkit: "text"
name: "download"
label: "下载地址"
- $formkit: "text"
name: "version"
label: "版本"
---
apiVersion: v1alpha1
kind: AnnotationSetting
metadata:
generateName: annotation-setting-
spec:
targetRef:
group: ""
kind: MenuItem
formSchema:
- $formkit: "text"
name: "icon"
label: "图标"
```

@ -0,0 +1,111 @@
---
title: 构建
description: 构建为可执行 JAR 和 Docker 镜像的文档
---
:::info
在此之前,我们推荐你先阅读[《准备工作》](./prepare),检查本地环境是否满足要求。
:::
一般情况下,为了保证版本一致性和可维护性,我们并不推荐自行构建和二次开发。但考虑到我们目前仅提供 Docker 镜像的发行版本,不再提供可执行 JAR 的发行版本,因此我们提供了构建的文档,以供用户自行构建。
## 克隆项目
如果你已经 fork 了相关仓库,请将以下命令中的 halo-dev 替换为你的 GitHub 用户名。
```bash
git clone https://github.com/halo-dev/halo
# 或者使用 ssh 的方式 clone推荐
git clone git@github.com:halo-dev/halo.git
# 切换到最新的 tag
cd halo
git checkout v2.4.0
```
:::tip
请务必按照以上要求切换到最新的 tag而不是直接使用 main 分支构建main 分支是我们的开发分支。此文档以 `2.10.0` 为例,查看最新的 tag 可使用 `git tag --column` 查看。
:::
:::warning
从 2.4.0 开始Console 项目已经合并到 Halo 主项目,所以不再需要单独克隆 Console 的项目仓库。
详情可查阅:<https://github.com/halo-dev/halo/issues/3393>
:::
## 构建 Console
```bash
cd path/to/halo
```
Linux / macOS 平台:
```bash
make -C console build
```
Windows 平台:
```bash
cd console
pnpm install
pnpm build:packages
pnpm build
```
## 构建 Fat Jar
构建之前需要修改 `gradle.properties` 中的 `version` 为当前 tag 的版本号,如:`version=2.10.0`
```bash
cd path/to/halo
```
下载预设插件:
```bash
# Windows
./gradlew.bat downloadPluginPresets
# macOS / Linux
./gradlew downloadPluginPresets
```
构建:
```bash
# Windows
./gradlew.bat clean build -x check
# macOS / Linux
./gradlew clean build -x check
```
构建完成之后,在 halo 项目下产生的 `application/build/libs/halo-2.10.0.jar` 即为构建完成的文件。
## 构建 Docker 镜像
在进行之前,请确保已经完成上述操作,最终需要确认在 halo 项目的 `application/build/libs/` 目录已经包含了 `halo-2.10.0.jar` 文件。
```bash
cd path/to/halo
```
```bash
docker build -t halo-dev/halo:2.10.0 .
```
```bash
# 插件构建完成的版本
docker images | grep halo
```
最终部署文档可参考:[使用 Docker Compose 部署](./docker-compose)

@ -0,0 +1,30 @@
---
title: 代码风格
description: 代码风格的相关配置说明
---
Halo 添加了 checkstyle 插件,来保证每位提交者代码的风格保持一致,减少无效代码的修改。本篇文章主要讲解如何在 IDEA 中添加 CheckStyle 插件,并引入项目所提供的 checkstyle.xml 配置。
## 安装 CheckStyle-IDEA
- 进入 IDEA 插件市场。
- 搜索 CheckStyle-IDEA点击安装即可。
## 配置 CheckStyle
- 进入 CheckStyle 配置File | Settings | Tools | Checkstyle
- 选择 Checkstyle 版本8.39。
- 在配置文件中点击添加按钮,配置描述可随便填写(推荐 Halo Checks选择 ./config/checkstyle/checkstyle.xml点击下一步和完成。
- 勾选刚刚创建的配置文件。
## 配置 Editor
- 进入编辑器配置File | Settings | Editor | Code Style
- 导入 checkstyle.xm 配置:
![image.png](https://halo.run/upload/2020/2/image-0c7a018e73f74634a534fa3ba8806628.png)
- 选择 `./config/checkstyle/checkstyle.xml` 配置文件,点击确定即可。
至此,有关代码风格检查工具和格式化配置已经完成。

@ -0,0 +1,25 @@
---
title: 准备工作
description: 开发环境的准备工作
---
## 环境要求
- [OpenJDK 17 LTS](https://github.com/openjdk/jdk)
- [Node.js 18 LTS](https://nodejs.org)
- [pnpm 7](https://pnpm.io/)
- [IntelliJ IDEA](https://www.jetbrains.com/idea/)
- [Git](https://git-scm.com/)
- [Docker](https://www.docker.com/)(可选)
## 名词解释
### 工作目录
指 Halo 所依赖的工作目录,在 Halo 运行的时候会在系统当前用户目录下产生一个 halo-next 的文件夹,绝对路径为 ~/halo-next。里面通常包含下列目录或文件
1. `db`:存放 H2 Database 的物理文件,如果你使用其他数据库,那么不会存在这个目录。
2. `themes`:里面包含用户所安装的主题。
2. `plugins`:里面包含用户所安装的插件。
5. `attachments`:附件目录。
4. `logs`:运行日志目录。

@ -0,0 +1,109 @@
---
title: 开发环境运行
description: 开发环境运行的指南
---
:::info
在此之前,我们推荐你先阅读[《准备工作》](./prepare),检查本地环境是否满足要求。
:::
## 项目结构说明
目前如果需要完整的运行 Halo总共需要三个部分
1. Halo 主项目([halo-dev/halo](https://github.com/halo-dev/halo)
2. Console 控制台(托管在 Halo 主项目)
3. 主题Halo 主项目内已包含默认主题)
:::info 说明
当前 Halo 主项目并不会将 Console 的构建资源托管到 Git 版本控制,所以在开发环境是需要同时运行 Console 项目的。当然,在我们的最终发布版本的时候会在 CI 中自动构建 Console 到 Halo 主项目。
:::
## 克隆项目
如果你已经 fork 了相关仓库,请将以下命令中的 halo-dev 替换为你的 GitHub 用户名。
```bash
git clone https://github.com/halo-dev/halo
# 或者使用 ssh 的方式 clone推荐
git clone git@github.com:halo-dev/halo.git
```
:::warning
从 2.4.0 开始Console 项目已经合并到 Halo 主项目,所以不再需要单独克隆 Console 的项目仓库。
详情可查阅:<https://github.com/halo-dev/halo/issues/3393>
:::
## 运行 Console
```bash
cd path/to/halo
```
Linux / macOS 平台:
```bash
make -C console dev
```
Windows 平台:
```bash
cd console
pnpm install
pnpm build:packages
pnpm dev
```
最终控制台打印了如下信息即代表运行正常:
```bash
VITE v3.1.6 ready in 638 ms
➜ Local: http://localhost:3000/console/
```
:::info 提示
请不要直接使用 Console 的运行端口3000访问会因为跨域问题导致无法正常登录建议按照后续的步骤以 dev 的配置文件运行 Halo在 dev 的配置文件中,我们默认代理了 Console 的访问地址,所以后续访问 Console 使用 `http://localhost:8090/console` 访问即可,代理的相关配置:
```yaml
halo:
console:
proxy:
endpoint: http://localhost:3000/
enabled: true
```
:::
## 运行 Halo
1. 在 IntelliJ IDEA 中打开 Halo 项目,等待 Gradle 初始化和依赖下载完成。
2. 修改 IntelliJ IDEA 的运行配置
1. macOS / Linux
将 Active Profiles 改为 `dev`,如图所示:
![IntelliJ IDEA Profiles](/img/developer-run/IntelliJ-IDEA-Profiles-macOS.png)
2. Windows
将 Active Profiles 改为 `dev,win`,如图所示:
![IntelliJ IDEA Profiles](/img/developer-run/IntelliJ-IDEA-Profiles-Win.png)
3. 点击 IntelliJ IDEA 的运行按钮,等待项目启动完成。
4. 或者使用 Gradle 运行
```bash
# macOS / Linux
./gradlew bootRun --args="--spring.profiles.active=dev"
# Windows
gradlew.bat bootRun --args="--spring.profiles.active=dev,win"
```
5. 最终访问 `http://localhost:8090/console` 即可进入控制台。访问 `http://localhost:8090` 即可进入站点首页。

@ -0,0 +1,36 @@
---
title: 系统结构
description: Halo 项目的构成
---
[Halo](https://github.com/halo-dev/halo) 博客系统分为以下四个部分:
| 项目名称 | 简介 |
| :------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------- |
| [halo](https://github.com/halo-dev/halo) | 提供整个系统的服务,采用 [Spring Boot](https://spring.io/) 开发 |
| [halo-admin](https://github.com/halo-dev/halo-admin) | 负责后台管理的渲染,采用 [Vue](https://vuejs.org/) 开发,已集成在 Halo 运行包内,无需独立部署。 |
| [halo-comment](https://github.com/halo-dev/halo-comment) | 评论插件,采用 [Vue](https://vuejs.org/) 开发,在主题中运行方式引入构建好的 `JavaScript` 文件即可 |
| [halo-theme-\*](https://github.com/halo-dev) | 主题项目集,采用 [FreeMarker](https://freemarker.apache.org/) 模板引擎编写,需要包含一些特殊的配置才能够被 halo 所使用 |
## 自定义配置
> 为什么要提前讲自定义配置呢?是因为在这里让大家了解到 `Halo` 的`配置方式`,以及`配置优先级`,不至于未来运行项目的时候不知道如何优雅地修改配置。
`Halo` 配置目录优先级如下(从上到下优先级越来越小,上层的配置将会覆盖下层):
- `Halo` 自定义配置
- file:~/.halo/
- file:~/.halo-dev/
- `Spring Boot` 默认配置
- file:./config/
- file:./
- classpath:/config/
- classpath:/
> 参考: [Application Property Files](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-application-property-files)
在开发的时候,希望大家能够在 `~/halo-dev/application.yml` 中进行添加自定义配置。当然后面也会讲到如何用`运行参数` 和 `VM options` 进行控制配置,届时可根据具体情况进行选择。
:::warning
开发的时候,我们不建议直接更改`项目源码`中的所包含的`配置文件`,包括 `application.yml`、`application-dev.yml`、`application-test.yml` 和 `application-user.yml`
:::

@ -0,0 +1,284 @@
---
title: 表单定义
---
在 Halo 2.0,在 Console 端的所有表单我们都使用了 [FormKit](https://github.com/formkit/formkit) 的方案。FormKit 不仅支持使用 Vue 组件的形式来构建表单,同时支持使用 Schema 的形式来构建。因此,我们的 [Setting](https://github.com/halo-dev/halo/blob/87ccd61ae5cd35a38324c30502d4e9c0ced41c6a/src/main/java/run/halo/app/core/extension/Setting.java#L20) 资源中的表单定义,都是使用 FormKit Schema 来定义的,最常用的场景即主题和插件的设置表单定义。当然,如果要在 Halo 2.0 的插件中使用,也可以参考 FormKit 的文档使用 Vue 组件的形式使用,但不需要在插件中引入 FormKit。
此文档将不会介绍 FormKit 的具体使用教程,因为我们已经很好的集成了 FormKit并且使用方式基本无异。此文章将介绍 Halo 2.0 中表单定义的一些规范,以及额外的一些输入组件。
FormKit 相关文档:
- Form Schema: <https://formkit.com/essentials/schema>
- FormKit Inputs: <https://formkit.com/inputs>
:::tip
目前不支持 FormKit Pro 中的输入组件,但 Halo 额外提供了部分输入组件,将在下面文档列出。
:::
## Setting 资源定义方式
```yaml title="settings.yaml"
apiVersion: v1alpha1
kind: Setting
metadata:
name: foo-setting
spec:
forms:
- group: group_1
label: 分组 1
formSchema:
- $formkit: radio
name: color_scheme
label: 默认配色
value: system
options:
- label: 跟随系统
value: system
- label: 深色
value: dark
- label: 浅色
value: light
- group: group_2
label: 分组 2
formSchema:
- $formkit: text
name: username
label: 用户名
value: ""
- $formkit: password
name: password
label: 密码
value: ""
```
:::tip
需要注意的是FormKit Schema 本身应该是 JSON 格式的,但目前我们定义一个表单所使用的是 YAML可能在参考 FormKit 写法时需要手动转换一下。
:::
字段说明:
1. `metadata.name`:设置资源的名称,建议以 `-setting` 结尾。
2. `spec.forms`:表单定义,可以定义多个表单,每个表单都有一个 `group` 字段,用于区分不同的表单。
3. `spec.forms[].label`:表单的标题。
4. `spec.forms[].formSchema`:表单的定义,使用 FormKit Schema 来定义。虽然我们使用的 YAML但与 FormKit Schema 完全一致。
## 组件类型
除了 FormKit 官方提供的常用输入组件之外Halo 还额外提供了一些输入组件,这些输入组件可以在 Form Schema 中使用。
### repeater
#### 描述
一组重复的输入组件,可以用于定义一组数据,最终得到的数据为一个对象的数组,可以方便地让使用者对其进行增加、移除、排序等操作。
#### 参数
- `min`:数组最小要求数量,默认为 `0`
- `max`:数组最大容量,默认为 `Infinity`,即无限制。
#### 示例
```yaml
- $formkit: repeater
name: socials
label: 社交账号
value: []
max: 5
min: 1
children:
- $formkit: text
name: name
label: 名称
value: ""
- $formkit: text
name: url
label: 地址
value: ""
```
:::tip
使用 `repeater` 类型时,一定要设置默认值,如果不需要默认有任何元素,可以设置为 `[]`
:::
其中 `name``url` 即数组对象的属性,最终保存表单之后得到的值为以下形式:
```json
{
"socials": [
{
"name": "GitHub",
"url": "https://github.com/halo-dev"
}
]
}
```
UI 效果:
<img src="/img/formkit/formkit-repeater.png" width="50%" />
### attachment
#### 描述
附件类型的输入框,支持直接调用附件库弹框选择附件。
#### 参数
- `accepts`:文件类型,数据类型为 `string[]`
#### 示例
```yaml
- $formkit: attachment
name: logo
label: Logo
accepts:
- "image/png"
- "video/mp4"
- "audio/*"
value: ""
```
### code
#### 描述
代码编辑器的输入组件,集成了 [Codemirror](https://codemirror.net/)。
#### 参数
- `language`:代码语言,目前支持 `yaml` `html` `javascript` `css` `json`
- `height`:代码编辑器的高度。
#### 示例
```yaml
- $formkit: code
name: footer_code
label: 页脚代码注入
value: ""
language: yaml
```
### menuCheckbox
#### 描述
菜单复选框,用于选择系统内的导航菜单。其中选择的值为菜单资源 `metadata.name` 的集合。
#### 示例
```yaml
- $formkit: menuCheckbox
name: menus
label: 菜单
value: []
```
### menuRadio
#### 描述
菜单单选框,用于选择系统内的导航菜单。其中选择的值为菜单资源 `metadata.name`
#### 示例
```yaml
- $formkit: menuRadio
name: menu
label: 菜单
value: ""
```
### postSelect
#### 描述
文章选择器,用于选择系统内的文章。其中选择的值为文章资源 `metadata.name`
#### 示例
```yaml
- $formkit: postSelect
name: post
label: 文章
value: ""
```
### singlePageSelect
#### 描述
单页选择器,用于选择系统内的独立页面。其中选择的值为独立页面资源 `metadata.name`
#### 示例
```yaml
- $formkit: singlePageSelect
name: singlePage
label: 单页
value: ""
```
### categorySelect
#### 描述
文章分类选择器,用于选择系统内的文章分类。其中选择的值为文章分类资源 `metadata.name`
#### 示例
```yaml
- $formkit: categorySelect
name: category
label: 分类
value: ""
```
### categoryCheckbox
#### 描述
文章分类复选框,用于选择系统内的文章分类。其中选择的值为文章分类资源 `metadata.name` 的集合。
#### 示例
```yaml
- $formkit: categoryCheckbox
name: categories
label: 分类
value: []
```
### tagSelect
#### 描述
文章标签选择器,用于选择系统内的文章标签。其中选择的值为文章标签资源 `metadata.name`
#### 示例
```yaml
- $formkit: tagSelect
name: tag
label: 标签
value: ""
```
### tagCheckbox
#### 描述
文章标签复选框,用于选择系统内的文章标签。其中选择的值为文章标签资源 `metadata.name` 的集合。
#### 示例
```yaml
- $formkit: tagCheckbox
name: tags
label: 标签
value: []
```

@ -0,0 +1,104 @@
---
title: 与自定义模型交互
description: 了解如果通过代码的方式操作数据
---
Halo 提供了两个类用于与自定义模型数据交互 `ExtensionClient``ReactiveExtensionClient`
它们的本质就是操作数据库,区别在于 `ExtensionClient` 是阻塞式 API`ReactiveExtensionClient` 是响应式 API接口返回值只有两种 Mono 或 Flux它们由 [reactor](https://projectreactor.io/) 提供。
```java
public interface ReactiveExtensionClient {
/**
* Lists Extensions by Extension type, filter and sorter.
*
* @param type is the class type of Extension.
* @param predicate filters the reEnqueue.
* @param comparator sorts the reEnqueue.
* @param <E> is Extension type.
* @return all filtered and sorted Extensions.
*/
<E extends Extension> Flux<E> list(Class<E> type, Predicate<E> predicate,
Comparator<E> comparator);
/**
* Lists Extensions by Extension type, filter, sorter and page info.
*
* @param type is the class type of Extension.
* @param predicate filters the reEnqueue.
* @param comparator sorts the reEnqueue.
* @param page is page number which starts from 0.
* @param size is page size.
* @param <E> is Extension type.
* @return a list of Extensions.
*/
<E extends Extension> Mono<ListResult<E>> list(Class<E> type, Predicate<E> predicate,
Comparator<E> comparator, int page, int size);
/**
* Fetches Extension by its type and name.
*
* @param type is Extension type.
* @param name is Extension name.
* @param <E> is Extension type.
* @return an optional Extension.
*/
<E extends Extension> Mono<E> fetch(Class<E> type, String name);
Mono<Unstructured> fetch(GroupVersionKind gvk, String name);
<E extends Extension> Mono<E> get(Class<E> type, String name);
/**
* Creates an Extension.
*
* @param extension is fresh Extension to be created. Please make sure the Extension name does
* not exist.
* @param <E> is Extension type.
*/
<E extends Extension> Mono<E> create(E extension);
/**
* Updates an Extension.
*
* @param extension is an Extension to be updated. Please make sure the resource version is
* latest.
* @param <E> is Extension type.
*/
<E extends Extension> Mono<E> update(E extension);
/**
* Deletes an Extension.
*
* @param extension is an Extension to be deleted. Please make sure the resource version is
* latest.
* @param <E> is Extension type.
*/
<E extends Extension> Mono<E> delete(E extension);
}
```
### 示例
如果你想在插件中根据 name 参数查询获取到 Person 自定义模型的数据,则可以这样写:
```java
private final ReactiveExtensionClient client;
Mono<Person> getPerson(String name) {
return client.fetch(Person.class, name);
}
```
或者使用阻塞式 API:
```java
private final ExtensionClient client;
Optional<Person> getPerson(String name) {
return client.fetch(Person.class, name);
}
```
我们建议你更多的使用响应式的 `ReactiveExtensionClient` 去替代 `ExtensionClient`

@ -0,0 +1,127 @@
---
title: 自定义模型
description: 了解什么是自定义模型及如何创建
---
Halo 自定义模型主要参考自 [Kubernetes CRD](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/) 。自定义模型遵循 [OpenAPI v3](https://spec.openapis.org/oas/v3.1.0)。设计目的在于为插件提供自定义数据支持。比如某插件需要存储自定义数据,同时也想读取和操作自定义数据。
一个典型的自定义模型 `Java` 代码示例如下:
```java
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import run.halo.app.extension.GroupKind;
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@GVK(group = "my-plugin.halo.run",
version = "v1alpha1",
kind = "Person",
plural = "persons",
singular = "person")
public class Person extends AbstractExtension {
@Schema(description = "The description on name field", maxLength = 100)
private String name;
@Schema(description = "The description on age field", maximum = "150", minimum = "0")
private Integer age;
@Schema(description = "The description on gender field")
private Gender gender;
private Person otherPerson;
public enum Gender {
MALE, FEMALE,
}
}
```
要创建一个自定义模型需要三步:
1. 创建一个类继承 `AbstractExtension`
2. 使用 `GVK` 注解。
3. 在插件 `start()` 生命周期方法中注册自定义模型:
```java
@Autowired
private SchemeManager schemeManager;
@Override
public void start() {
schemeManager.register(Person.class);
}
```
有了自定义模型后可以通过在插件项目的 `src/main/resources/extensions` 目录下声明 `yaml` 文件来创建一个实例,此目录下的所有自定义模型 `yaml` 都会在插件启动时被创建:
```yaml
groupVersion: my-plugin.halo.run/v1alpha1
kind: Person
metadata:
name: fake-person
name: halo
age: 18
gender: male
```
:::tip 释义
- @GVK:此注解标识该类为一个自定义模型,同时必须继承 `AbstractExtension`
- kind表示自定义模型所表示的 REST 资源。
- group表示一组公开的资源通常采用域名形式Halo 项目保留使用空组和任何以“*.halo.run”结尾的组名供其单独使用。
选择群组名称时我们建议选择你的群组或组织拥有的子域例如“widget.mycompany.com”。
- versionAPI 的版本,它与 group 组合使用为 apiVersion=“GROUP/VERSION”例如“api.halo.run/v1alpha1”。
- singular: 资源的单数名称这允许客户端不透明地处理复数和单数必须全部小写。通常为小写的“kind”。
- plural 资源的复数名称,自定义资源在 `/apis/<group>/<version>/.../<plural>` 下提供,必须为全部小写。
- @Schema:属性校验注解,会在创建/修改资源前对资源校验,参考 [schema-validator](https://www.openapi4j.org/schema-validator.html)。
:::
### 自定义模型 API
定义好自定义模型并注册后,会根据 `GVK` 注解自动生成一组 `CRUD` API规则为
`/apis/<group>/<version>/<extension>/{extensionname}/<subextension>`
对于上述 Person 自定义模型将有以下 APIs
```text
GET /apis/my-plugin.halo.run/v1alpha1/persons
PUT /apis/my-plugin.halo.run/v1alpha1/persons/{name}
POST /apis/my-plugin.halo.run/v1alpha1/persons
DELETE /apis/my-plugin.halo.run/v1alpha1/persons/{name}
```
### 自定义 API
在一些场景下,只有自动生成的 `CRUD` API 往往是不够用的,此时可以通过自定义一些 API 来满足功能。
你可以使用 `SpringBoot` 的控制器写法来暴露 API不同的是需要添加 `@ApiVersion` 注解,没有此注解的 `Controller` 将被忽略:
```java
@ApiVersion("v1alpha1")
@RequestMapping("/apples")
@RestController
public class AppleController {
@PostMapping("/starting")
public void starting() {
}
}
```
当插件被启动时Halo 将会为此 AppleController 生成统一路径的 API。API 前缀组成规则如下:
```text
/apis/plugin.api.halo.run/{version}/plugins/{plugin-name}/**
```
示例:
```text
/apis/plugin.api.halo.run/v1alpha1/plugins/my-plugin/apples/starting
```

@ -0,0 +1,36 @@
---
title: 静态资源代理
description: 了解如果使用静态资源代理来访问插件中的静态资源
---
插件中的静态资源如图片等如果想被外部访问到,需要放到 `src/main/resources` 目录下,并通过创建 ReverseProxy 自定义模型来进行静态资源代理访问。
例如 `src/main/resources` 下的 `static` 目录下有一张 `halo.jpg`:
1. 首先需要在 `src/main/resources/extensions` 下创建一个 `yaml`,文件名可以任意。
2. 示例配置如下。
```yaml
apiVersion: plugin.halo.run/v1alpha1
kind: ReverseProxy
metadata:
# name 为此资源的唯一标识名称,不允许重复,为了避免与其他插件冲突,推荐带上插件名称前缀
name: my-plugin-fake-reverse-proxy
rules:
- path: /res/**
file:
directory: static
# 如果想代理 static 下所有静态资源则省略 filename 配置
filename: halo.jpg
```
插件启动后会根据 `/plugins/{plugin-name}/assets/**` 规则生成 API。
因此该 `ReverseProxy` 的访问路径为: `/plugins/my-plugin/assets/res/halo.jpg`
- `rules` 下可以添加多组规则。
- `path` 为路径前缀。
- `file` 表示访问文件系统,目前暂时仅支持这一种。
- `directory` 表示要代理的目标文件目录,它相对于 `src/main/resources/` 目录。
- `filename` 表示要代理的目标文件名。
`directory``filename` 都是可选的,但必须至少有一个被配置。

@ -0,0 +1,103 @@
---
title: API 权限控制
description: 了解如果对插件中的 API 定义角色模板以接入权限控制
---
插件中的 APIs 无论是自定义模型自动生成的 APIs 或者是通过 Controller 自定义的 APIs 都只有超级管理员能够访问,如果想将这些 APIs 授权给其他用户访问则需要定义一些 RoleTemplate 的资源以便可以在用户界面上将其分配给其他角色使用。
RoleTemplate 的 yaml 资源也需要放到 `src/main/resources/extensions` 目录下,文件名称可以任意,它的 kind 为 Role 但需要一个 label `halo.run/role-template: "true"` 来表示它是角色模板。
Halo 的权限控制对同一种资源一般只定义两个 RoleTemplate一个是只读权限另一个是管理权限因此如果没有特殊情况需要更细粒度的控制我们建议你也保持一致
```yaml
apiVersion: v1
kind: Role
metadata:
# 使用 plugin name 作为前缀防止与其他插件冲突
name: my-plugin-role-view-persons
labels:
halo.run/role-template: "true"
annotations:
rbac.authorization.halo.run/module: "Persons Management"
rbac.authorization.halo.run/display-name: "Person Manage"
rbac.authorization.halo.run/ui-permissions: |
["plugin:my-plugin:person:view"]
rules:
- apiGroups: ["my-plugin.halo.run"]
resources: ["my-plugin/persons"]
verbs: ["*"]
---
apiVersion: v1
kind: Role
metadata:
name: my-plugin-role-manage-persons
labels:
halo.run/role-template: "true"
annotations:
rbac.authorization.halo.run/dependencies: |
[ "role-template-view-person" ]
rbac.authorization.halo.run/module: "Persons Management"
rbac.authorization.halo.run/display-name: "Person Manage"
rbac.authorization.halo.run/ui-permissions: |
["plugin:my-plugin:person:manage"]
rules:
- apiGroups: [ "my-plugin.halo.run" ]
resources: [ "my-plugin/persons" ]
verbs: [ "get", "list" ]
```
上述便是根据 [自定义模型](./extension.md) 章节中定义的 Person 自定义模型配置角色模板的示例。
定义了一个用于管理 Person 资源的角色模板 `my-plugin-role-manage-persons`,它具有所有权限,同时定义了一个只允许查询 Person 资源的角色模板 `my-plugin-role-view-persons`
下面让我们回顾一下这些配置:
`rules` 是个数组,它允许配置多组规则:
- `apiGroups` 对应 `GVK` 中的 `group` 所声明的值。
- `resources` 对应 API 中的 resource 部分,`my-plugin` 表示插件名称。
- `verbs` 表示请求动词,可选值为 "create", "delete", "deletecollection", "get", "list", "patch", "update"。对应的 HTTP 请求方式如下表所示:
| HTTP verb | request verb |
| --------- | ------------------------------------------------------------ |
| POST | create |
| GET, HEAD | get (for individual resources), list (for collections, including full object content), watch (for watching an individual resource or collection of resources) |
| PUT | update |
| PATCH | patch |
| DELETE | delete (for individual resources), deletecollection (for collections) |
`metadata.labels` 中必须包含 `halo.run/role-template: "true"` 以表示它此资源要作为角色模板。
`metadata.annotations` 中:
- `rbac.authorization.halo.run/dependencies`:用于声明角色间的依赖关系,例如管理角色必须要依赖查看角色,以避免分配了管理权限却没有查看权限的情况。
- `rbac.authorization.halo.run/module`:角色模板分组名称。在此示例中,管理 Person 的模板角色将和查看 Person 的模板角色将被在 UI 层面归为一组展示。
- `rbac.authorization.halo.run/display-name`:模板角色的显示名称,用于展示为用户可读的名称信息。
- `rbac.authorization.halo.run/ui-permissions`:用于控制 UI 权限,规则为 `plugin:{your-plugin-name}:scope-name`,使用示例为在插件前端部分入口文件 `index.ts` 中用于控制菜单是否显示或者控制页面按钮是否展示:
```javascript
{
path: "",
name: "HelloWorld",
component: DefaultView,
meta: {
permissions: ["plugin:my-plugin:person:view"]
}
}
```
以上定义角色模板的方式适合资源型请求,即需要符合以下规则
- 以 `/api/<version>/<resource>[/<resourceName>/<subresource>/<subresourceName>]` 规则组成 APIs。
- 以 `/apis/<group>/<version>/<resource>[/<resourceName>/<subresource>/<subresourceName>]` 规则组成的 APIs。
注:`[]`包裹的部分表示可选
如果你的 API 不符合以上资源型 API 的规则,则被定型为非资源型 API例如 `/healthz`,需要使用一下配置方式:
```yaml
rules:
# nonResourceURL 中的 '*' 是一个全局通配符
- nonResourceURLs: ["/healthz", "/healthz/*"]
verbs: [ "get", "create"]
```

@ -0,0 +1,675 @@
---
title: Todo List
description: 这个例子展示了如何开发 Todo List 插件
---
本示例用于展示如何从插件模板创建一个插件并写一个 Todo List
首先通过模板仓库创建一个插件,例如叫 `halo-plugin-todolist`
## 配置你的插件
1. 修改 `build.gradle` 中的 `group` 为你自己的,如:
```groovy
group = 'run.halo.tutorial'
```
2. 修改 `settings.gradle` 中的 `rootProject.name`
```groovy
rootProject.name = 'halo-plugin-todolist'
```
3. 修改插件的描述文件 `plugin.yaml`,它位于 `src/main/resources/plugin.yaml`。示例:
```yaml
apiVersion: plugin.halo.run/v1alpha1
kind: Plugin
metadata:
name: todolist
spec:
enabled: true
requires: ">=2.0.0"
author:
name: halo-dev
website: https://halo.run
logo: https://halo.run/logo
homepage: https://github.com/guqing/halo-plugin-hello-world
displayName: "插件 Todo List"
description: "插件开发的 hello world用于学习如何开发一个简单的 Halo 插件"
license:
- name: "MIT"
```
参考链接:
- [SemVer expression](https://github.com/zafarkhaja/jsemver#semver-expressions-api-ranges)
- [表单定义](../form-schema.md)
此时我们已经准备好了可以开发一个 TodoList 插件的一切,下面让我们正式进入 TodoList 插件开发教程。
## 运行插件
为了看到效果,首先我们需要让插件能最简单的运行起来。
1. 在 `src/main/java` 下创建包,如 `run.halo.tutorial`,在创建一个类 `TodoListPlugin`,它继承自 `BasePlugin` 类内容如下:
```java
package run.halo.tutorial;
import org.pf4j.PluginWrapper;
import org.springframework.stereotype.Component;
import run.halo.app.plugin.BasePlugin;
@Component
public class TodoListPlugin extends BasePlugin {
public TodoListPlugin(PluginWrapper wrapper) {
super(wrapper);
}
}
```
`src/main/java` 下的文件结构如下:
```text
.
└── run
└── halo
└── tutorial
└── TodoListPlugin.java
```
然后在项目目录执行命令
```shell
./gradlew build
```
使用 `IntelliJ IDEA` 打开 Halo参考 [Halo 开发环境运行](../core/run.md) 及 [插件入门](../hello-world.md) 配置插件的运行模式和路径:
```yaml
halo:
plugin:
runtime-mode: development
fixed-plugin-path:
# 配置为插件绝对路径
- /Users/guqing/halo-plugin-todolist
```
启动 Halo然后访问 `http://localhost:8090/console`
在插件列表将能看到插件已经被正确启用
![plugin-todolist-in-list-view](/img/todolist-in-list.png)
## 创建一个自定义模型
我们希望 TodoList 能够被持久化以避免重启后数据丢失,因此需要创建一个自定义模型来进行数据持久化。
首先创建一个 `class` 名为 `Todo` 并写入如下内容:
```java
package run.halo.tutorial;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
@Data
@EqualsAndHashCode(callSuper = true)
@GVK(kind = "Todo", group = "todo.plugin.halo.run",
version = "v1alpha1", singular = "todo", plural = "todos")
public class Todo extends AbstractExtension {
@Schema(requiredMode = REQUIRED)
private TodoSpec spec;
@Data
public static class TodoSpec {
@Schema(requiredMode = REQUIRED, minLength = 1)
private String title;
@Schema(defaultValue = "false")
private Boolean done;
}
}
```
然后在 `TodoListPlugin``start` 生命周期方法中注册此自定义模型到 Halo 中。
```diff
// ...
+ import run.halo.app.extension.SchemeManager;
@Component
public class TodoListPlugin extends BasePlugin {
+ private final SchemeManager schemeManager;
- public TodoListPlugin(PluginWrapper wrapper) {
+ public TodoListPlugin(PluginWrapper wrapper, SchemeManager schemeManager) {
super(wrapper);
+ this.schemeManager = schemeManager;
}
@Override
public void start() {
+ // 插件启动时注册自定义模型
+ schemeManager.register(Todo.class);
System.out.println("Hello world 插件启动了!");
}
@Override
public void stop() {
+ // 插件停用时取消注册自定义模型
+ Scheme todoScheme = schemeManager.get(Todo.class);
+ schemeManager.unregister(todoScheme);
System.out.println("Hello world 被停止!");
}
// ....
}
```
然后 build 项目,重启 Halo访问 `http://localhost:8090/swagger-ui.html`
可以找到如下 Todo APIs
![hello world plugin swagger api for toto](/img/halo-plugin-hello-world-todo-swagger-api.png)
由于所有以 `/api``/apis` 开头的 APIs 都需要认证才能访问,因此先在 Swagger UI 界面顶部点击 `Authorize` 认证,然后尝试访问
`GET /apis/todo.plugin.halo.run/v1alpha1/todos` 可以看到如下结果:
```json
{
"page": 0,
"size": 0,
"total": 0,
"items": [],
"first": true,
"last": true,
"hasNext": false,
"hasPrevious": false,
"totalPages": 1
}
```
至此我们完成了一个自定义模型的创建和使用插件生命周期方法实现了自定义模型的注册和删除,下一步我们将编写用户界面,使用这些 APIs 完成 TodoList 功能。
## 编写用户界面
### 目标
我们希望实现如下的用户界面:
- 在左侧菜单添加一个名为 `Todo List` 的菜单项,它属于一个`工具`的组。
- 内容页为一个简单的 Todo List它实现以下功能
- 添加 `Todo item`
- 将一个 `Todo item` 标记为完成,也可以取消完成状态
- 列表有三个 `Tab` 可供切换,用于过滤数据展示
![todo user interface](/img/todo-ui.png)
### 实现
使用模板仓库创建的项目中与 `src` 目录同级有一个 `console` 目录,它即为用户界面的源码目录。
在实现用户界面前我们需要先修改 `console/vite.config.ts` 中的 `pluginName``plugin.yaml` 中的 `metadata.name`,它用来标识此用户界面所属于插件名 pluginName 标识的插件,以便 Halo 加载 console 目录打包产生的文件。
修改完成后执行
```groovy
./gradlew build
```
修改前端项目不需要重启 Halo只需要 build 然后刷新页面,此时能看到多出来一个菜单项:
![hello-world-in-plugin-list](/img/plugin-hello-world.png)
而我们需要实现的目标中也需要一个菜单项,所以直接修改它即可。
打开 `console/src/index.ts` 文件,修改如下:
```diff
export default definePlugin({
- name: "PluginStarter",
+ name: "plugin-hello-world",
components: [],
routes: [
{
parentName: "Root",
route: {
- path: "/example",
+ path: "/todos", // TodoList 的路由 path
children: [
{
path: "",
- name: "Example",
+ name: "TodoList",// 菜单标识名
component: DefaultView,
meta: {
- permissions: ["plugin:apples:view"],
- title: "示例页面",
+ title: "Todo List",//菜单页的浏览器 tab 标题
searchable: true,
menu: {
- name: "示例页面",
+ name: "Todo List",// TODO 菜单显示名称
- group: "示例分组",
= group: "工具",// 所在组名
icon: markRaw(IconGrid),
priority: 0,
},
},
},
],
},
},
],
extensionPoints: {},
activated() {},
deactivated() {},
});
```
完成此步骤后 Console 左侧菜单多了一个名 `工具` 的组,其下有 `Todo List`,浏览器标签页名称也是 `Todo List`
接来下我们需要在右侧内容区域实现 [目标](#目标) 中图示的 Todo 样式,为了快速上手,我们使用 [todomvc](https://todomvc.com/examples/vue/) 提供的 Vue 标准实现。
编辑 `console/src/views/DefaultView.vue` 文件,清空它的内容,并拷贝 [examples/#todomvc](https://vuejs.org/examples/#todomvc) 的所有代码粘贴到此文件中,并执行以下步骤:
1. `cd console` 切换到 `console` 目录。
2. ` pnpm install todomvc-app-css `
3. 修改 `console/src/views/DefaultView.vue` 最底部的 `style` 标签。
```diff
- <style>
+ <style scoped>
- @import "https://unpkg.com/todomvc-app-css@2.4.1/index.css";
+ @import "todomvc-app-css/index.css";
</style>
```
4. 重新 Build 后刷新页面,便能看到目标图所示效果。
通过以上步骤就实现了一个 Todo List 的用户界面功能,但 `Todo` 数据只是被临时存放到了 `LocalStorage` 中,下一步我们将通过自定义模型生成的 APIs 来让用户界面与服务端交互。
### 与服务端数据交互
本章节我们将通过使用 `Axios` 来完成与插件后端 APIs 进行数据交互,文档参考 [axios-http](https://axios-http.com/docs)。
首先需要安装 `Axios` 在 console 目录下执行命令:
```shell
pnpm install axios
```
为了符合最佳实践,将用 TypeScript 改造之前的 todomvc 示例:
1. 创建 types 文件 `console/src/types/index.ts`
```typescript
export interface Metadata {
name: string;
labels?: {
[key: string]: string;
} | null;
annotations?: {
[key: string]: string;
} | null;
version?: number | null;
creationTimestamp?: string | null;
deletionTimestamp?: string | null;
}
export interface TodoSpec {
title: string;
done?: boolean;
}
/**
* 与自定义模型 Todo 对应
*/
export interface Todo {
spec: TodoSpec;
apiVersion: "todo.plugin.halo.run/v1alpha1"; // apiVersion=自定义模型的 group/version
kind: "Todo"; // Todo 自定义模型中 @GVK 注解中的 kind
metadata: Metadata;
}
/**
* Todo 自定义模型生成 list API 所对应的类型
*/
export interface TodoList {
page: number;
size: number;
total: number;
items: Array<Todo>;
first: boolean;
last: boolean;
hasNext: boolean;
hasPrevious: boolean;
totalPages: number;
}
```
编辑 `console/src/views/DefaultView.vue` 文件,将所有内容替换为如下写法:
```typescript
<script setup lang="ts">
import axios from "axios";
import type { Todo, TodoList } from "../types";
import { computed, onMounted, ref } from "vue";
const http = axios.create({
baseURL: "/",
timeout: 1000,
});
interface Tab {
label: string;
}
const todos = ref<TodoList>({
page: 1,
size: 20,
total: 0,
items: [],
first: true,
last: false,
hasNext: false,
hasPrevious: false,
totalPages: 0,
});
const tabs = [
{
label: "All",
},
{
label: "Active",
},
{
label: "Completed",
},
];
const activeTab = ref("All");
/**
* 列表展示的数据
*/
const todoList = computed(() => {
if (activeTab.value === "All") {
return todos.value.items;
}
if (activeTab.value === "Active") {
return filterByDone(false);
}
if (activeTab.value === "Completed") {
return filterByDone(true);
}
return [];
});
const filterByDone = (done: boolean) => {
return todos.value.items.filter((todo) => todo.spec.done === done);
};
// 查看 http://localhost:8090/swagger-ui.html
function handleFetchTodos() {
http
.get<TodoList>("/apis/todo.plugin.halo.run/v1alpha1/todos")
.then((response) => {
todos.value = response.data;
});
}
onMounted(handleFetchTodos);
// 创建的逻辑
const title = ref("");
function handleCreate(e: Event) {
http
.post<Todo>("/apis/todo.plugin.halo.run/v1alpha1/todos", {
metadata: {
// 根据 'todo-' 前缀自动生成 todo 的名称作为唯一标识,可以理解为数据库自动生成主键 id
generateName: "todo-",
},
spec: {
title: title.value,
done: false,
},
kind: "Todo",
apiVersion: "todo.plugin.halo.run/v1alpha1",
})
.then((response) => {
title.value = "";
handleFetchTodos();
});
}
// 更新的逻辑
const selectedTodo = ref<Todo | undefined>();
const handleUpdate = () => {
http
.put<Todo>(
`/apis/todo.plugin.halo.run/v1alpha1/todos/${selectedTodo.value?.metadata.name}`,
selectedTodo.value
)
.then((response) => {
handleFetchTodos();
});
};
function handleDoneChange(todo: Todo) {
todo.spec.done = !todo.spec.done;
http
.put<Todo>(
`/apis/todo.plugin.halo.run/v1alpha1/todos/${todo.metadata.name}`,
todo
)
.then((response) => {
handleFetchTodos();
});
}
// 删除
const handleDelete = (todo: Todo) => {
http
.delete(`/apis/todo.plugin.halo.run/v1alpha1/todos/${todo.metadata.name}`)
.then((response) => {
handleFetchTodos();
});
};
</script>
<template>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input
class="new-todo"
autofocus
v-model="title"
placeholder="What needs to be done?"
@keyup.enter="handleCreate"
/>
</header>
<section class="main" v-show="todos.items.length">
<input
id="toggle-all"
class="toggle-all"
type="checkbox"
:checked="filterByDone(false).length > 0"
/>
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li
v-for="(todo, index) in todoList"
class="todo"
:key="index"
:class="{ completed: todo.spec.done, editing: todo === selectedTodo }"
>
<div class="view">
<input
class="toggle"
type="checkbox"
:checked="todo.spec.done"
@click="handleDoneChange(todo)"
/>
<label @dblclick="selectedTodo = todo">{{ todo.spec.title }}</label>
<button class="destroy" @click="handleDelete(todo)"></button>
</div>
<input
v-if="selectedTodo"
class="edit"
type="text"
v-model="selectedTodo.spec.title"
@vnode-mounted="({ el }) => el.focus()"
@blur="handleUpdate()"
@keyup.enter="handleUpdate()"
@keyup.escape="selectedTodo = undefined"
/>
</li>
</ul>
</section>
<footer class="footer" v-show="todos.total">
<span class="todo-count">
<strong>{{ filterByDone(false).length }}</strong>
<span>
{{ filterByDone(false).length === 1 ? " item" : " items" }} left</span
>
</span>
<ul class="filters">
<li v-for="(tab, index) in tabs" :key="index">
<a
href="javascript:void(0);"
@click="activeTab = tab.label"
:class="{ selected: activeTab === tab.label }"
>
{{ tab.label }}
</a>
</li>
</ul>
<button
class="clear-completed"
@click="() => filterByDone(true).map((todo) => handleDelete(todo))"
v-show="todos.items.length > filterByDone(false).length"
>
Clear completed
</button>
</footer>
</section>
</template>
<style scoped>
@import "todomvc-app-css/index.css";
</style>
```
这在原先的基础上替换为了 `TypeScript` 写法,并去除了数据保存到 `LocalStorage` 的逻辑,这也是我们推荐的方式,可读性更强,且有 `TypeScript` 提供类型提示。
至此我们就完成了与插件后端 APIs 实现 Todo List 数据交互的部分。
### 使用 Icon
目前 Todo 的菜单还是默认的网格样式 Icon`console/src/index.ts` 文件中配置有一个 `icon: markRaw(IconGrid)`。以此为例说明该如何使用其他 `Icon`
1. 安装 [unplugin-icons](https://github.com/antfu/unplugin-icons)。
```shell
pnpm install -D unplugin-icons
pnpm install -D @iconify/json
pnpm install -D @vue/compiler-sfc
```
2. 编辑 `console/vite.config.ts`,在 `defineConfig``plugins` 中添加配置,修改如下。
```diff
+ import Icons from "unplugin-icons/vite";
// https://vitejs.dev/config/
export default defineConfig({
- plugins: [vue(), vueJsx()],
+ plugins: [vue(), vueJsx(), Icons({ compiler: "vue3" })],
```
3. 在 `console/tsconfig.app.json` 中加入 `unplugni-icons``types` 配置。
```diff
{
// ...
"compilerOptions": {
// ...
"paths": {
"@/*": ["./src/*"]
- }
+ },
+ "types": ["unplugin-icons/types/vue"]
}
}
```
4. 到 [icones](https://icones.js.org/) 搜索你想要使用的图标,并点击它,然后选择 `Unplugin Icons`,会复制到剪贴板。
![unplugin icons selector](/img/unplugin-icons-example.png)
5. 编辑 `console/src/index.ts``import` 区域粘贴,并 `icon` 属性。
```diff
- import { IconGrid } from "@halo-dev/components";
+ import VscodeIconsFileTypeLightTodo from "~icons/vscode-icons/file-type-light-todo";
export default definePlugin({
routes: [
{
// ...
route: {
path: "/todos",
children: [
{
// ...
meta: {
// ...
menu: {
// ...
- icon: markRaw(IconGrid),
+ icon: markRaw(VscodeIconsFileTypeLightTodo),
priority: 0,
},
},
},
],
},
},
],
// ...
});
```
### 用户界面使用静态资源
如果你想在用户界面中使用图片,你可以放到 `console/src/assets` 中,例如 `logo.png`,并将其作为 Todo 的 Logo 放到标题旁边
![todo logo example](/img/todo-logo-check-48.png)
需要修改 `console/src/views/DefaultView.vue` 示例如下:
```diff
+ import Logo from "../assets/logo.png";
// ...
<template>
<section class="todoapp">
<header class="header">
<h1>
+ <img :src="Logo" alt="logo" style="display: inline; width: 64px" />
todos
</h1>
//...
```
至此,我们完成了从零开始创建一个 TodoList 插件的所有步骤,希望可以帮助你对 Halo 的插件开发有一个整体的了解。

@ -0,0 +1,64 @@
---
title: 入门
description: 了解如何构建你的第一个插件并在 Halo 中使用它。
---
Halo 提供了一个模板仓库用于创建插件:
1. 打开 [plugin-starter](https://github.com/halo-dev/plugin-starter)。
2. 点击 `Use this template` -> `Create a new repository`
3. 如图所示填写仓库名后点击 `Create repository from template`
![create-repository-for-hello-world-plugin](/img/create-repository-for-hello-world-plugin.png)
你现在已经基于 Halo 插件模板创建了自己的存储库。接下来,你需要将它 `git clone` 到你的计算机上并使用 `IntelliJ IDEA` 打开它。
## 运行插件
现在有了一个空项目,我们需要让插件能最最小化的运行起来。
这很简单,首先你需要构建插件:只需要在 `halo-plugin-hello-world` 项目目录下执行 Gradle 命令
```shell
./gradlew build
```
或者使用 `IntelliJ IDEA` 提供的 `Gradle build` 即可完成插件项目的构建。
第二步就是使用它。
使用 `IntelliJ IDEA` 打开 Halo参考 [Halo 开发环境运行](../core/run.md)。
然后在 `src/main/resources` 下创建一个 `application-local.yaml` 文件并做如下配置:
```yaml
# macOS / Linux
halo:
plugin:
runtime-mode: development
fixed-plugin-path:
# 配置为插件绝对路径
- /Users/guqing/halo-plugin-hello-world
# Windows
halo:
plugin:
runtime-mode: development
fixed-plugin-path:
# 配置为插件绝对路径
- C:\Users\guqing\halo-plugin-hello-world
```
使用此 local profile 启动 Halo
```shell
# macOS / Linux
./gradlew bootRun --args="--spring.profiles.active=dev,local"
# Windows
gradlew.bat bootRun --args="--spring.profiles.active=dev,win,local"
```
然后访问 `http://localhost:8090/console`
在插件列表将能看到插件已经被正确启动,并且在左侧菜单添加了一个 `示例分组`,其下有一个名 `示例页面` 的菜单。
![hello-world-in-plugin-list](/img/plugin-hello-world.png)

@ -0,0 +1,27 @@
---
title: 介绍
description: 插件开发的准备工作
---
插件是由社区创建的程序或应用程序,用于扩展 Halo 的功能。插件在 Halo 中运行并执行一项或多项用户操作。它们允许用户根据自己的喜好扩展或修改 Halo。
## 插件管理
### 支持
Halo 不提供对第三方应用程序的支持。作为插件的开发者你有责任帮助插件的用户解决技术问题issues
当提交插件到 [awesome-halo](https://github.com/halo-sigs/awesome-halo) 时,
您需要添加服务支持联系人Support contact。这可以是用户可以联系的电子邮件地址也可以是网站或帮助中心的链接。
### 版本控制
为了保持 Halo 生态系统的健康、可靠和安全,每次您对自己拥有的插件进行重大更新时,我们建议在遵循 [semantic versioning spec](http://semver.org/) 的基础上,
发布新版本。遵循语义版本控制规范有助于其他依赖你代码的开发人员了解给定版本的更改程度,并在必要时调整自己的代码。
我们建议你的包版本从1.0.0开始并递增,如下:
| Code status | Stage | Rule | Example version |
| ----------------------------------------- | ------------- | -------------------------------------------- | --------------- |
| First release | New product | 从 1.0.0 开始 | 1.0.0 |
| Backward compatible bug fixes | Patch release | 增加第三位数字 | 1.0.1 |
| Backward compatible new features | Minor release | 增加中间数字并将最后一位重置为零 | 1.1.0 |
| Changes that break backward compatibility | Major release | 增加第一位数字并将中间和最后一位数字重置为零 | 2.0.0 |

@ -0,0 +1,37 @@
---
title: 生命周期
description: 了解插件从启动到卸载的过程
---
根据[插件项目文件结构](./structure.md)所展示的 `StarterPlugin.java` 中,具有如下方法:
```java
@Override
public void start() {
System.out.println("插件启动成功!");
}
@Override
public void stop() {
System.out.println("插件停止!");
}
@Override
public void delete() {
System.out.println("插件被删除!");
}
```
### 插件启动
插件被安装后,只加载了插件的 `plugin.yaml`,类及其他资源文件的加载均在启动时进行。
当插件加载完类文件并准备好启动插件后就会调用插件的 `start()` 方法,这有助于插件在启动时做一些事情,例如初始化。
### 插件停止
插件停止时,会删除在启动时创建的自定义资源,例如插件设置等通过 `yaml` 创建的自定义模型资源。
插件定义的自定义模型也需要在此时清理掉。
### 插件删除
插件被卸载时被调用。

@ -0,0 +1,44 @@
---
title: 插件资源文件
description: 了解插件资源文件 plugin.yaml 如何配置
---
一个典型的插件资源文件 plugin.yaml 如下所示:
```yaml
apiVersion: plugin.halo.run/v1alpha1
kind: Plugin
metadata:
name: hello-world
spec:
enabled: true
requires: ">=2.0.0"
author:
name: halo-dev
website: https://halo.run
logo: https://halo.run/logo
# settingName: hello-world-settings
# configMapName: hello-world-configmap
homepage: https://github.com/guqing/halo-plugin-hello-world
displayName: "插件 Hello world"
description: "插件开发的 hello world用于学习如何开发一个简单的 Halo 插件"
license:
- name: "MIT"
```
- `apiVersion``kind`:为固定写法,每个插件写法都是一样的不可变更。
- `metadata.name`:它是插件的唯一标识名,包含不超过 253 个字符,仅包含小写字母、数字或“-”,以字母或数字开头,以字母或数字结尾。
- `spec.enabled`:表示是否要在安装时自动启用插件,仅在插件开发模式下有效。
- `spec.requires`:支持的 Halo 版本SemVer expression, e.g. ">=2.0.0"
- `spec.author`:插件作者的名称和可获得支持的网站地址。
- `spec.logo`:插件 logo可以是域名或相对于项目 src/main/resources 目录的相对文件路径。
- `spec.settingName`:插件配置表单名称,参考表单定义,不需要表单设置则可删除。
- `spec.configMapName`:表单定义对应的值标识名, 推荐命名为 "插件名-configmap",没有配置 `settingName` 则不需要配置此项。
- `spec.homepage`:通常为插件的 GitHub 仓库链接,或可联系到插件作者或插件官网或帮助中心链接等。
- `spec.displayName`:插件的显示名称,它通常是以少数几个字来概括插件的用途。
- `spec.description`:插件描述,用一段话来介绍插件的用途。
- `spec.license`:插件使用的软件协议,参考:<https://en.wikipedia.org/wiki/Software_license>
:::tip Note
如果你在 plugin.yaml 中配置了 `settingName` 但确没有对应的 `Setting` 自定义模型资源文件,会导致插件无法启动,原因是 `Setting` 模型 `metadata.name` 为你配置的 `settingName` 的资源无法找到。
:::

@ -0,0 +1,48 @@
---
title: 插件中的对象管理
description: 了解如何在创建中创建对象和管理对象依赖
---
在插件中你可以使用 [Spring](https://spring.io) 提供的常用 Bean 注解来标注一个类,然后就能使用依赖注入功能注入其他类的对象。这省去了使用工厂创建类和维护的过程。
通过模板插件创建的项目中你都可以看到 `StarterPlugin` 标注了 `@Component` 注解:
```java
@Component
public class StarterPlugin extends BasePlugin {
}
```
假设项目中有一个 `FruitService`,并将其声明了为了 Bean
```java
@Service
public class FruitService {
}
```
你可以在任何同样声明为 Bean 的类中使用[依赖注入](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-dependencies)来使用它:
```java
@Component
public class Demo {
private final FruitService fruitService;
public Demo(FruitService fruitService) {
this.fruitService = fruitService;
}
// use it...
}
```
### 依赖注入 Halo 提供的 Bean
目前 Halo 只提供了少数几个 Bean 可以供插件依赖注入:
- ReactiveExtensionClient
- ExtensionClient
- SchemeManager
- ExternalUrlSupplier
即其他不在上述列表中的类的对象都是不可依赖注入的。

@ -0,0 +1,15 @@
---
title: 准备工作
description: 插件开发的准备工作
---
在 Halo 中,插件是使用 Java 和 JavaScript 编写的UI 使用 [Vuejs](https://vuejs.org) 编写。
在创建你的第一个插件之前,请确保你具备以下条件:
- 你能成功[运行 Halo 2.0.0 及以上版本](../core/run.md)。
- 你应该能够熟练使用 [IntelliJ IDEA](https://www.jetbrains.com/idea/old)。
- 你需要在计算机上安装最新的 LTS 版本的 Node.js。如果你还没有Node.js安装你可以在这里下载 [Node.js 16 LTS](https://nodejs.org/)。
- 你熟悉 Vue 和 TypeScript。
- 你应该熟悉使用 PNPM 进行包管理,你可以在这里下载 [pnpm 7](https://pnpm.io/)。
- Git 是一个版本控制系统,用于跟踪代码的更改。您需要 Git 来下载示例插件并发布插件。

@ -0,0 +1,19 @@
---
title: 发布插件
description: 了解如何与我们的社区分享你的插件
---
> 了解如何与我们的社区分享您的扩展。
## 创建你的 Release
当你完成了你的插件并进行充分测试后,切换到插件目录 Build 一次,当没有发生任何错误你就可以推送到 GitHub 并 `Create a new release`
然后填写 `Release Tag` 和描述点击创建,项目目录下的 `.github/workflows/workflow.yaml` 文件会被 `GitHub Action` 触发并执行,脚本会自动根据你的 `Release Tag` 修改插件版本号然后在 `Release``Asserts` 中包含打包产物--插件的 JAR 文件。
## 分享你的插件
用户可以在你的仓库 Release 下载使用,但为了方便让社区用户看到,你可以在我们的 [awesome-halo](https://github.com/halo-sigs/awesome-halo) 仓库发起一个 Pull Request为此你需要先 Fork [awesome-halo](https://github.com/halo-sigs/awesome-halo) 并按照此仓库的要求添加一行记录是关于你的插件仓库地址和功能描述的,然后推送你的更改并通过 GitHub 向 [awesome-halo](https://github.com/halo-sigs/awesome-halo) 的 `main` 分支发起 Pull Request。
## 等待审核
在你发起 Pull Request 后我们将审查的你的插件并在需要时请求更改。一旦被接受Pull Request 将被合并。

@ -0,0 +1,44 @@
---
title: 插件运行模式
description: 了解插件的运行方式
---
Halo 的插件可以在两种模式下运行:`DEVELOPMENT` 和 `DEPLOYMENT`
`DEPLOYMENT`(默认)模式是插件创建的标准工作流程:为每个插件创建一个新的 Gradle 项目,编码插件(声明新的扩展点和/或添加新的扩展),将插件打包成一个 JAR 文件,部署 JAR 文件到 Halo。
这些操作非常耗时,因此引入了 `DEVELOPMENT` 运行时模式。
对于插件开发人员来说,`DEVELOPMENT` 运行时模式的主要优点是不必打包和部署插件。在开发模式下,您可以以简单快速的模式开发插件。
### 配置
如果你想以 `DEPLOYMENT` 运行插件则做如下配置:
```yaml
halo:
plugin:
runtime-mode: deployment
```
插件的 `deployment` 模式只允许通过安装 JAR 文件的方式运行插件。
而如果你想以 `DEVELOPMENT` 运行插件或开发插件则将 `runtime-mode` 修改为 `development`,同时配置 `fixed-plugin-path` 为插件项目路径,可以配置多个。
```yaml
# macOS / Linux
plugin:
runtime-mode: development
fixed-plugin-path:
# 配置为插件绝对路径
- /Users/guqing/halo-plugin-hello-world
# Windows
halo:
plugin:
runtime-mode: development
fixed-plugin-path:
# 配置为插件绝对路径
- C:\Users\guqing\halo-plugin-hello-world
```
:::tip Note
插件以开发模式运行时由于插件的加载方式与部署模式不同,如果你此时在 Console 安装插件JAR则会提示插件文件找不到而无法启动。
:::

@ -0,0 +1,76 @@
---
title: 项目结构
description: 了解插件的文件结构
---
新创建的插件项目典型的目录结构如下所示:
```text
.
├── LICENSE
├── README.md
├── gradle
│   └── .
├── lib
│   └── halo-2.0.0-SNAPSHOT-plain.jar
├── src
│   ├── main
│   │   ├── java
│   │   │   └── run
│   │   │   └── halo
│   │   │   └── starter
│   │   │   └── StarterPlugin.java
│   │   └── resources
│   │   ├── console
│   │   │   ├── main.js
│   │   │   └── style.css
│   │   └── plugin.yaml
├── gradlew
├── gradlew.bat
├── gradle.properties
├── settings.gradle
├── build.gradle
├── console
│   ├── package.json
│   ├── pnpm-lock.yaml
│   ├── src
│   │   ├── assets
│   │   │   └── logo.svg
│   │   ├── components
│   │   │   └── HelloWorld.vue
│   │   ├── index.ts
│   │   ├── styles
│   │   │   └── index.css
│   │   └── views
│   │   └── DefaultView.vue
│   ├── tsconfig.app.json
│   ├── tsconfig.config.json
│   ├── tsconfig.json
│   ├── tsconfig.vitest.json
│   └── vite.config.ts
```
该目录包含了前端和后端两个部分,让我们依次看一下它们中的每一个。
### 后端部分
所有的后端代码都放在 `src` 目录下,它是一个常规的 `Java` 项目目录结构。
- `StarterPlugin.java` 为插件的后端入口文件。
- `resources` 下的 `plugin.yaml` 为插件的资源描述文件,它是必须的。
- `resources/console` 下的两个文件 `main.js``style.css` 是前端插件部分打包时输出的产物。一个插件可以没有前端部分,因此 `resources/console` 同样可以不存在。
`lib/halo-2.0.0-SNAPSHOT-plain.jar` 它是 Halo 的类型依赖,目前使用 `JAR` 文件的方式引入依赖只是暂时的,后续将会改进它,它只作为编译时依赖使用。
### 前端部分
`console` 目录下为插件的前端部分的工程目录,包括了源码、配置文件和静态资源文件。
同样的,将所有前端项目源码放到 `src` 中。我们建议使用 `TypeScript` 作为编程语言,它可以帮助你在编译时而非运行时捕获错误。
- `src/index.ts` 作为前端部分的插件的入口文件。
- `views` 中存放视图文件。
- `styles` 中存放样式。
- `components` 中放一些公共组件。
- `assets` 用于放静态资源文件。

@ -0,0 +1,64 @@
---
title: 模型元数据
---
在 [元数据表单定义](../annotations-form.md) 我们介绍了如何为模型添加元数据表单,此文档将介绍如何在主题模板中使用元数据。
我们在模板中专门为获取 annotations 数据提供了三个方法,可以更加方便的设置默认值和判断元数据字段是否存在。
## #annotations.get(extension,key)
### 描述
根据对象和元数据的 key 获取元数据的值。
### 示例
```html {4}
<div th:with="menu = ${menuFinder.getPrimary()}">
<ul th:with="menuItems = ${menu.menuItems}">
<li th:each="menuItem : ${menuItems}">
<i th:class="${#annotations.get(menuItem, 'icon')}"></i>
<a th:href="@{${menuItem.status.href}}" th:text="${menuItem.status.displayName}"></a>
</li>
</ul>
</div>
```
## #annotations.getOrDefault(extension,key,defaultValue)
### 描述
根据对象和元数据的 key 获取元数据的值,同时支持设置默认值。
### 示例
```html {4}
<div th:with="menu = ${menuFinder.getPrimary()}">
<ul th:with="menuItems = ${menu.menuItems}">
<li th:each="menuItem : ${menuItems}">
<i th:class="${#annotations.getOrDefault(menuItem, 'icon', 'fa')}"></i>
<a th:href="@{${menuItem.status.href}}" th:text="${menuItem.status.displayName}"></a>
</li>
</ul>
</div>
```
## #annotations.contains(extension,key)
### 描述
根据对象和元数据的 key 判断元数据是否存在。
### 示例
```html {4}
<div th:with="menu = ${menuFinder.getPrimary()}">
<ul th:with="menuItems = ${menu.menuItems}">
<li th:each="menuItem : ${menuItems}">
<i th:if="${#annotations.contains(menuItem, 'icon')}" th:class="${#annotations.get(menuItem, 'icon')}"></i>
<a th:href="@{${menuItem.status.href}}" th:text="${menuItem.status.displayName}"></a>
</li>
</ul>
</div>
```

@ -0,0 +1,47 @@
---
title: 常用代码片段
description: 本文档介绍了常用的代码片段,以便于开发者快速上手。
---
## 布局模板
通常情况下,我们需要一个公共模板来定义页面的布局。
```html title="templates/layout.html"
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org" th:fragment="html (head,content)">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=2" />
<title th:text="${site.title}"></title>
<link rel="stylesheet" th:href="@{/assets/dist/style.css}" />
<script th:src="@{/assets/dist/main.iife.js}"></script>
<th:block th:if="${head != null}">
<th:block th:replace="${head}" />
</th:block>
</head>
<body>
<section>
<th:block th:replace="${content}" />
</section>
</body>
</html>
```
```html title="templates/index.html"
<!DOCTYPE html>
<html
xmlns:th="https://www.thymeleaf.org"
th:replace="~{modules/layout :: html(head = null,content = ~{::content})}"
>
<th:block th:fragment="content">
<!-- 文章列表 -->
<ul>
<li th:each="post : ${posts.items}">
<a th:href="@{${post.status.permalink}}" th:text="${post.spec.title}"></a>
</li>
</ul>
</th:block>
</html>
```

@ -0,0 +1,89 @@
---
title: 配置文件
description: 关于主题配置文件的文档。
---
目前 Halo 2.0 的主题必须在根目录包含 `theme.yaml`,用于配置主题的基本信息,如主题名称、版本、作者等。
## 格式示例
```yaml title="theme.yaml"
apiVersion: theme.halo.run/v1alpha1
kind: Theme
metadata:
name: theme-foo
spec:
displayName: 示例主题
author:
name: halo-dev
website: https://halo.run
description: 一个示例主题
logo: https://halo.run/logo
website: https://github.com/halo-sigs/theme-foo
repo: https://github.com/halo-sigs/theme-foo.git
settingName: "theme-foo-setting"
configMapName: "theme-foo-configMap"
customTemplates:
post:
- name: 文档
description: 文档类型的文章
screenshot:
file: post_documentation.html
category:
- name: 知识库
description: 知识库类型的分类
screenshot:
file: category_knowledge.html
page:
- name: 关于
description: 关于页面
screenshot:
file: page_about.html
version: 1.0.0
require: 2.0.0
```
## 字段详解
| 字段 | 描述 | 是否必填 |
| ------------------------------- | ----------------------------------------------------------------------------- | -------- |
| `metadata.name` | 主题的唯一标识 | 是 |
| `spec.displayName` | 显示名称 | 是 |
| `spec.author.name` | 作者名称 | 否 |
| `spec.author.website` | 作者网站 | 否 |
| `spec.description` | 主题描述 | 否 |
| `spec.logo` | 主题 Logo | 否 |
| `spec.website` | 主题网站 | 否 |
| `spec.repo` | 主题托管地址 | 否 |
| `spec.settingName` | 设置表单定义的名称,需要同时创建对应的 `settings.yaml` 文件 | 否 |
| `spec.configMapName` | 设置持久化配置的 ConfigMap 名称 | 否 |
| `spec.customTemplates.post` | 文章的自定义模板配置,详细文档可查阅 [模板路由](./template-route-mapping#custom-templates) | 否 |
| `spec.customTemplates.category` | 分类的自定义模板配置,详细文档可查阅 [模板路由](./template-route-mapping#custom-templates) | 否 |
| `spec.customTemplates.page` | 独立页面的自定义模板配置,详细文档可查阅 [模板路由](./template-route-mapping#custom-templates) | 否 |
| `spec.version` | 主题版本 | 是 |
| `spec.require` | 所需 Halo 的运行版本 | 是 |
## 更新配置
由于目前 `theme.yaml` 是持久化存储在数据库中的,不会在修改之后主动更新,所以我们在 Console 的主题页面添加了 `重载主题配置` 的选项。
![重载主题配置](/img/theme/reload-theme-config.png)
## 从 1.x 迁移
为了方便主题开发者从 1.x 迁移,我们提供了工具用于迁移配置文件。
工具仓库地址:<https://github.com/halo-sigs/convert-theme-config-to-next>
```bash
# 1.x 版本主题
cd path/to/theme
npx @halo-dev/convert-theme-config-to-next theme
```
执行完成之后即可看到主题目录下生成了 `theme.2.0.yaml` 文件,重命名为 `theme.yaml` 即可。
:::tip
转换完成之后需要修改 `metadata.name`、`spec.settingName` 和 `spec.configMapName`
:::

@ -0,0 +1,10 @@
---
title: Finder API
description: 本文档介绍 Finder API 的使用方法。
---
import DocCardList from '@theme/DocCardList';
目前在主题模板中获取数据可以使用对应路由提供的 [模板变量](./template-variables),但为了满足在任意位置获取数据的需求,我们提供了 Finder API。
<DocCardList />

@ -0,0 +1,187 @@
---
title: 文章分类
description: 文章分类 - CategoryFinder
---
import CategoryVo from "../vo/CategoryVo.md"
import CategoryTreeVo from "../vo/CategoryTreeVo.md"
## getByName(name)
```js
categoryFinder.getByName(name)
```
### 描述
根据 `metadata.name` 获取文章分类。
### 参数
1. `name:string` - 分类的唯一标识 `metadata.name`
### 返回值
[#CategoryVo](#categoryvo)
### 示例
```html
<div th:with="category = ${categoryFinder.getByName('category-foo')}">
<a th:href="@{${category.status.permalink}}" th:text="${category.spec.displayName}"></a>
</div>
```
## getByNames(names)
```js
categoryFinder.getByNames(names)
```
### 描述
根据一组 `metadata.name` 获取文章分类。
### 参数
1. `names:List<string>` - 分类的唯一标识 `metadata.name` 的集合。
### 返回值
List<[#CategoryVo](#categoryvo)>
### 示例
```html
<div th:with="categories = ${categoryFinder.getByNames(['category-foo', 'category-bar'])}">
<a th:each="category : ${categories}" th:href="@{${category.status.permalink}}" th:text="${category.spec.displayName}"></a>
</div>
```
## list(page,size)
```js
categoryFinder.list(page,size)
```
### 描述
根据分页参数获取分类列表。
### 参数
1. `page:int` - 分页页码,从 1 开始
2. `size:int` - 分页条数
### 返回值
[#ListResult<CategoryVo\>](#listresultcategoryvo)
### 示例
```html
<ul th:with="categories = ${categoryFinder.list(1,10)}">
<li th:each="category : ${categories.items}">
<a th:href="@{${category.status.permalink}}" th:text="${category.spec.displayName}"></a>
</li>
</ul>
```
## listAll()
```js
categoryFinder.listAll()
```
### 描述
获取所有文章分类。
### 参数
### 返回值
List<[#CategoryVo](#categoryvo)>
### 示例
```html
<ul th:with="categories = ${categoryFinder.listAll()}">
<li th:each="category : ${categories}">
<a th:href="@{${category.status.permalink}}" th:text="${category.spec.displayName}"></a>
</li>
</ul>
```
## listAsTree()
```js
categoryFinder.listAsTree()
```
### 描述
获取所有文章分类的多层级结构。
### 参数
### 返回值
List<[#CategoryTreeVo](#categorytreevo)>
### 示例
```html
<div th:with="categories = ${categoryFinder.listAsTree()}">
<ul>
<li th:replace="~{modules/category-tree :: single(categories=${categories})}" />
</ul>
</div>
```
```html title="/templates/category-tree.html"
<ul th:fragment="next (categories)">
<li th:fragment="single (categories)" th:each="category : ${categories}">
<a th:href="@{${category.status.permalink}}">
<span th:text="${category.spec.displayName}"> </span>
</a>
<th:block th:if="${not #lists.isEmpty(category.children)}">
<th:block th:replace="~{modules/category-tree :: next (categories=${category.children})}"></th:block>
</th:block>
</li>
</ul>
```
## 类型定义
### CategoryVo
<CategoryVo />
### ListResult<CategoryVo\>
```json title="ListResult<CategoryVo>"
{
"page": 0, // 当前页码
"size": 0, // 每页条数
"total": 0, // 总条数
"items": "List<#CategoryVo>", // 分类列表数据
"first": true, // 是否为第一页
"last": true, // 是否为最后一页
"hasNext": true, // 是否有下一页
"hasPrevious": true, // 是否有上一页
"totalPages": 0 // 总页数
}
```
- [#CategoryVo](#categoryvo)
### CategoryTreeVo
<CategoryTreeVo />
- [#CategoryTreeVo](#categorytreevo)

@ -0,0 +1,155 @@
---
title: 评论
description: 评论 - CommentFinder
---
import CommentVo from "../vo/CommentVo.md"
import ReplyVo from "../vo/ReplyVo.md"
## getByName(name)
```js
commentFinder.getByName(name)
```
### 描述
根据 `metadata.name` 获取评论。
### 参数
1. `name:string` - 评论的唯一标识 `metadata.name`
### 返回值
[#CommentVo](#commentvo)
### 示例
```html
<div th:with="comment = ${commentFinder.getByName('comment-foo')}">
<span th:text="${comment.spec.owner.displayName}"></span>
<div th:text="${comment.spec.content}"></div>
</div>
```
## list(ref,page,size)
```js
commentFinder.list(ref,page,size)
```
### 描述
根据评论的 `metadata.name` 和分页参数获取回复列表。
### 参数
1. `ref:#Ref` - 评论的唯一标识 `metadata.name`
2. `page:int` - 分页页码,从 1 开始
3. `size:int` - 分页条数
- [#Ref](#ref)
### 返回值
[#ListResult<CommentVo\>](#listresultcommentvo)
### 示例
```html
<ul th:with="comments = ${commentFinder.list({ group: 'content.halo.run', version: 'v1alpha1', kind: 'Post', name: 'post-foo' },1,10)}">
<li th:each="comment : ${comments.items}">
<span th:text="${comment.spec.owner.displayName}"></span>
<div th:text="${comment.spec.content}"></div>
</li>
</ul>
```
## listReply(commentName,page,size)
```js
commentFinder.listReply(commentName,page,size)
```
### 描述
根据评论的 `metadata.name` 和分页参数获取回复列表。
### 参数
1. `commentName:string` - 评论的唯一标识 `metadata.name`
1. `page:int` - 分页页码,从 1 开始
2. `size:int` - 分页条数
### 返回值
[#ListResult<ReplyVo\>](#listresultreplyvo)
### 示例
```html
<ul th:with="replies = ${commentFinder.listReply('comment-foo',1,10)}">
<li th:each="reply : ${replies.items}">
<span th:text="${reply.spec.owner.displayName}"></span>
<div th:text="${reply.spec.content}"></div>
</li>
</ul>
```
## 类型定义
### CommentVo
<CommentVo />
### ListResult<CommentVo\>
```json title="ListResult<CommentVo>"
{
"page": 0, // 当前页码
"size": 0, // 每页条数
"total": 0, // 总条数
"items": "List<#CommentVo>", // 评论列表数据
"first": true, // 是否为第一页
"last": true, // 是否为最后一页
"hasNext": true, // 是否有下一页
"hasPrevious": true, // 是否有上一页
"totalPages": 0 // 总页数
}
```
- [#CommentVo](#commentvo)
### ReplyVo
<ReplyVo />
### ListResult<ReplyVo\>
```json title="ListResult<ReplyVo>"
{
"page": 0, // 当前页码
"size": 0, // 每页条数
"total": 0, // 总条数
"items": "List<#ReplyVo>", // 回复列表数据
"first": true, // 是否为第一页
"last": true, // 是否为最后一页
"hasNext": true, // 是否有下一页
"hasPrevious": true, // 是否有上一页
"totalPages": 0 // 总页数
}
```
- [#ReplyVo](#replyvo)
### Ref
```json title="Ref"
{
"group": "string",
"kind": "string",
"version": "string",
"name": "string"
}
```

@ -0,0 +1,64 @@
---
title: 作者
description: 作者 - ContributorFinder
---
import ContributorVo from "../vo/ContributorVo.md"
## getContributor(name)
```js
contributorFinder.getContributor(name)
```
### 描述
根据 `metadata.name` 获取作者。
### 参数
1. `name:string` - 作者的唯一标识 `metadata.name`
### 返回值
[#ContributorVo](#contributorvo)
### 示例
```html
<div th:with="contributor = ${contributorFinder.getContributor('contributor-foo')}">
<h1 th:text="${contributor.displayName}"></h1>
</div>
```
## getContributors(names)
```js
contributorFinder.getContributors(names)
```
### 描述
根据一组 `metadata.name` 获取作者。
### 参数
1. `names:List<string>` - 作者的唯一标识 `metadata.name` 的集合。
### 返回值
List<[#ContributorVo](#contributorvo)>
### 示例
```html
<div th:with="contributors = ${contributorFinder.getContributors(['contributor-foo, 'contributor-bar'])}">
<span th:each="contributor : ${contributors}" th:text="${contributor.displayName}"></span>
</div>
```
## 类型定义
### ContributorVo
<ContributorVo />

@ -0,0 +1,87 @@
---
title: 导航菜单
description: 导航菜单 - MenuFinder
---
import MenuItemVo from "../vo/MenuItemVo.md"
import MenuVo from "../vo/MenuVo.md"
## getByName(name)
```js
menuFinder.getByName(name)
```
### 描述
根据 `metadata.name` 获取菜单。
### 参数
1. `name:string` - 菜单的唯一标识 `metadata.name`
### 返回值
[#MenuVo](#menuvo)
### 示例
```html
<div th:with="menu = ${menuFinder.getByName('menu-foo')}">
<ul th:with="menuItems = ${menu.menuItems}">
<li th:each="menuItem : ${menuItems}">
<a
th:href="@{${menuItem.status.href}}"
th:text="${menuItem.status.displayName}"
th:target="${menuItem.spec.target?.value}"
>
</a>
</li>
</ul>
</div>
```
## getPrimary()
```js
menuFinder.getPrimary()
```
### 描述
获取主菜单。
### 参数
### 返回值
[#MenuVo](#menuvo)
### 示例
```html
<div th:with="menu = ${menuFinder.getPrimary()}">
<ul th:with="menuItems = ${menu.menuItems}">
<li th:each="menuItem : ${menuItems}">
<a
th:href="@{${menuItem.status.href}}"
th:text="${menuItem.status.displayName}"
th:target="${menuItem.spec.target?.value}"
>
</a>
</li>
</ul>
</div>
```
## 类型定义
### MenuVo
<MenuVo />
### MenuItemVo
<MenuItemVo />

@ -0,0 +1,33 @@
---
title: 插件
description: 插件 - PluginFinder
---
## available(pluginName)
```js
pluginFinder.available(pluginName)
```
### 描述
判断一个插件是否可用,会同时判断插件是否安装和启用。
### 参数
1. `pluginName:string` - 插件的唯一标识 `metadata.name`
### 返回值
`boolean` - 插件是否可用
### 示例
```html
<!-- https://github.com/halo-sigs/plugin-search-widget -->
<li th:if="${pluginFinder.available('PluginSearchWidget')}">
<a href="javascript:SearchWidget.open()" title="搜索">
搜索
</a>
</li>
```

@ -0,0 +1,430 @@
---
title: 文章
description: 文章 - PostFinder
---
import CategoryVo from "../vo/CategoryVo.md";
import TagVo from "../vo/TagVo.md";
import PostVo from "../vo/PostVo.md";
import ContentVo from "../vo/ContentVo.md"
import ContributorVo from "../vo/ContributorVo.md"
import ListedPostVo from "../vo/ListedPostVo.md"
## getByName(postName)
```js
postFinder.getByName(postName);
```
### 描述
根据 `metadata.name` 获取文章。
### 参数
1. `postName:string` - 文章的唯一标识 `metadata.name`
### 返回值
[#PostVo](#postvo)
### 示例
```html
<div th:with="post = ${postFinder.getByName('post-foo')}">
<a th:href="@{${post.status.permalink}}" th:text="${post.spec.title}"></a>
</div>
```
## content(postName)
```js
postFinder.content(postName);
```
### 描述
根据文章的 `metadata.name` 单独获取文章内容。
### 参数
1. `postName:string` - 文章的唯一标识 `metadata.name`
### 返回值
[#ContentVo](#contentvo)
### 示例
```html
<div th:with="content = ${postFinder.content('post-foo')}">
<div th:utext="${content.content}"></div>
</div>
```
## cursor(postName)
```js
postFinder.cursor(postName);
```
### 描述
根据文章的 `metadata.name` 获取相邻的文章(上一篇 / 下一篇)。
### 参数
1. `postName:string` - 文章的唯一标识 `metadata.name`
### 返回值
[#NavigationPostVo](#navigationpostvo)
### 示例
```html title="/templates/post.html"
<div th:with="postCursor = ${postFinder.cursor(post.metadata.name)}">
<a
th:if="${postCursor.hasPrevious()}"
th:href="@{${postCursor.previous.status.permalink}}"
>
<span th:text="${postCursor.previous.spec.title}"></span>
</a>
<a
th:if="${postCursor.hasNext()}"
th:href="@{${postCursor.next.status.permalink}}"
>
<span th:text="${postCursor.next.spec.title}"></span>
</a>
</div>
```
## listAll()
```js
postFinder.listAll();
```
### 描述
获取所有文章。
### 参数
### 返回值
List<[#ListedPostVo](#listedpostvo)>
### 示例
```html
<ul th:with="posts = ${postFinder.listAll()}">
<li th:each="post : ${posts}">
<a th:href="@{${post.status.permalink}}" th:text="${post.spec.title}"></a>
</li>
</ul>
```
## list(page,size)
```js
postFinder.list(page, size);
```
### 描述
根据分页参数获取文章列表。
### 参数
1. `page:int` - 分页页码,从 1 开始
2. `size:int` - 分页条数
### 返回值
[#ListResult<ListedPostVo\>](#listresultlistedpostvo)
### 示例
```html
<ul th:with="posts = ${postFinder.list(1,10)}">
<li th:each="post : ${posts.items}">
<a th:href="@{${post.status.permalink}}" th:text="${post.spec.title}"></a>
</li>
</ul>
```
## listByCategory(page,size,categoryName)
```js
postFinder.listByCategory(page, size, categoryName);
```
### 描述
根据分类标识和分页参数获取文章列表。
### 参数
1. `page:int` - 分页页码,从 1 开始
2. `size:int` - 分页条数
3. `categoryName:string` - 文章分类唯一标识 `metadata.name`
### 返回值
[#ListResult<ListedPostVo\>](#listresultlistedpostvo)
### 示例
```html
<ul th:with="posts = ${postFinder.listByCategory(1,10,'category-foo')}">
<li th:each="post : ${posts.items}">
<a th:href="@{${post.status.permalink}}" th:text="${post.spec.title}"></a>
</li>
</ul>
```
## listByTag(page,size,tag)
```js
postFinder.listByTag(page, size, tag);
```
### 描述
根据标签标识和分页参数获取文章列表。
### 参数
1. `page:int` - 分页页码,从 1 开始
2. `size:int` - 分页条数
3. `tag:string` - 文章分类唯一标识 `metadata.name`
### 返回值
[#ListResult<ListedPostVo\>](#listresultlistedpostvo)
### 示例
```html
<ul th:with="posts = ${postFinder.listByTag(1,10,'tag-foo')}">
<li th:each="post : ${posts.items}">
<a th:href="@{${post.status.permalink}}" th:text="${post.spec.title}"></a>
</li>
</ul>
```
## archives(page,size)
```js
postFinder.archives(page, size);
```
### 描述
根据分页参数获取文章归档列表。
### 参数
1. `page:int` - 分页页码,从 1 开始
2. `size:int` - 分页条数
### 返回值
[#ListResult<PostArchiveVo\>](#listresultpostarchivevo)
### 示例
```html
<th:block th:with="archives = ${postFinder.archives(1,10)}">
<th:block th:each="archive : ${archives.items}">
<h1 th:text="${archive.year}"></h1>
<ul>
<th:block th:each="month : ${archive.months}">
<li th:each="post : ${month.posts}">
<a th:href="@{${post.status.permalink}}" th:text="${post.spec.title}">
</a>
</li>
</th:block>
</ul>
</th:block>
</th:block>
```
## archives(page,size,year)
```js
postFinder.archives(page, size, year);
```
### 描述
根据年份和分页参数获取文章归档列表。
### 参数
1. `page:int` - 分页页码,从 1 开始
2. `size:int` - 分页条数
3. `year:string` - 年份
### 返回值
[#ListResult<PostArchiveVo\>](#listresultpostarchivevo)
### 示例
```html
<th:block th:with="archives = ${postFinder.archives(1,10,'2022')}">
<th:block th:each="archive : ${archives.items}">
<h1 th:text="${archive.year}"></h1>
<ul>
<th:block th:each="month : ${archive.months}">
<li th:each="post : ${month.posts}">
<a th:href="@{${post.status.permalink}}" th:text="${post.spec.title}">
</a>
</li>
</th:block>
</ul>
</th:block>
</th:block>
```
## archives(page,size,year,month)
```js
postFinder.archives(page, size, year, month);
```
### 描述
根据年月和分页参数获取文章归档列表。
### 参数
1. `page:int` - 分页页码,从 1 开始
2. `size:int` - 分页条数
3. `year:string` - 年份
4. `month:string` - 月份
### 返回值
[#ListResult<PostArchiveVo\>](#listresultpostarchivevo)
### 示例
```html
<th:block th:with="archives = ${postFinder.archives(1,10,'2022','11')}">
<th:block th:each="archive : ${archives.items}">
<h1 th:text="${archive.year}"></h1>
<ul>
<th:block th:each="month : ${archive.months}">
<li th:each="post : ${month.posts}">
<a th:href="@{${post.status.permalink}}" th:text="${post.spec.title}">
</a>
</li>
</th:block>
</ul>
</th:block>
</th:block>
```
## 类型定义
### CategoryVo
<CategoryVo />
### TagVo
<TagVo />
### ContributorVo
<ContributorVo />
### PostVo
<PostVo />
- [#CategoryVo](#categoryvo)
- [#TagVo](#tagvo)
- [#ContributorVo](#contributorvo)
- [#ContentVo](#contentvo)
### ContentVo
<ContentVo />
### NavigationPostVo
```json title="NavigationPostVo"
{
"previous": "#PostVo", // 上一篇文章
"current": "#PostVo", // 当前文章
"next": "#PostVo" // 下一篇文章
}
```
- [#PostVo](#postvo)
### ListedPostVo
<ListedPostVo />
- [#CategoryVo](#categoryvo)
- [#TagVo](#tagvo)
- [#ContributorVo](#contributorvo)
### ListResult<ListedPostVo\>
```json title="ListResult<ListedPostVo>"
{
"page": 0, // 当前页码
"size": 0, // 每页条数
"total": 0, // 总条数
"items": "List<#ListedPostVo>", // 文章列表数据
"first": true, // 是否为第一页
"last": true, // 是否为最后一页
"hasNext": true, // 是否有下一页
"hasPrevious": true, // 是否有上一页
"totalPages": 0 // 总页数
}
```
- [#ListedPostVo](#listedpostvo)
### PostArchiveVo
```json title="PostArchiveVo"
{
"year": "string",
"months": [
{
"month": "string",
"posts": "#ListedPostVo"
}
]
}
```
- [#ListedPostVo](#listedpostvo)
### ListResult<PostArchiveVo\>
```json title="ListResult<PostArchiveVo>"
{
"page": 0, // 当前页码
"size": 0, // 每页条数
"total": 0, // 总条数
"items": "List<#PostArchiveVo>", // 文章归档数据
"first": true, // 是否为第一页
"last": true, // 是否为最后一页
"hasNext": true, // 是否有下一页
"hasPrevious": true, // 是否有上一页
"totalPages": 0 // 总页数
}
```
- [#PostArchiveVo](#postarchivevo)

@ -0,0 +1,131 @@
---
title: 独立页面
description: 独立页面 - SinglePageFinder
---
import SinglePageVo from "../vo/SinglePageVo.md"
import ListedSinglePageVo from "../vo/ListedSinglePageVo.md"
import ContributorVo from "../vo/ContributorVo.md"
import ContentVo from "../vo/ContentVo.md"
## getByName(pageName)
```js
singlePageFinder.getByName(pageName)
```
### 描述
根据 `metadata.name` 获取独立页面。
### 参数
1. `pageName:string` - 独立页面的唯一标识 `metadata.name`
### 返回值
[#SinglePageVo](#singlepagevo)
### 示例
```html
<div th:with="singlePage = ${singlePageFinder.getByName('page-foo')}">
<a th:href="@{${singlePage.status.permalink}}" th:text="${singlePage.spec.title}"></a>
</div>
```
## content(pageName)
```js
singlePageFinder.content(pageName)
```
### 描述
根据独立页面的 `metadata.name` 单独获取独立页面内容。
### 参数
1. `pageName:string` - 独立页面的唯一标识 `metadata.name`
### 返回值
[#ContentVo](#contentvo)
### 示例
```html
<div th:with="content = ${singlePageFinder.content('page-foo')}">
<div th:utext="${content.content}"></div>
</div>
```
## list(page,size)
```js
singlePageFinder.list(page,size)
```
### 描述
根据分页参数获取独立页面列表。
### 参数
1. `page:int` - 分页页码,从 1 开始
2. `size:int` - 分页条数
### 返回值
[#ListResult<ListedSinglePageVo\>](#listresultlistedsinglepagevo)
### 示例
```html
<ul th:with="singlePages = ${singlePageFinder.list(1,10)}">
<li th:each="singlePage : ${singlePages.items}">
<a th:href="@{${singlePage.status.permalink}}" th:text="${singlePage.spec.title}"></a>
</li>
</ul>
```
## 类型定义
### SinglePageVo
<SinglePageVo />
- [#ContributorVo](#contributorvo)
- [#ContentVo](#contentvo)
### ListedSinglePageVo
- [#ContributorVo](#contributorvo)
<ListedSinglePageVo />
### ListResult<ListedSinglePageVo\>
```json title="ListResult<ListedSinglePageVo>"
{
"page": 0, // 当前页码
"size": 0, // 每页条数
"total": 0, // 总条数
"items": "List<#ListedSinglePageVo>", // 自定义页面列表数据
"first": true, // 是否为第一页
"last": true, // 是否为最后一页
"hasNext": true, // 是否有下一页
"hasPrevious": true, // 是否有上一页
"totalPages": 0 // 总页数
}
```
- [#ListedSinglePageVo](#listedsinglepagevo)
### ContentVo
<ContentVo />
### ContributorVo
<ContributorVo />

@ -0,0 +1,45 @@
---
title: 站点统计
description: 站点统计 - SiteStatsFinder
---
## getStats()
```js
siteStatsFinder.getStats()
```
### 描述
获取站点的统计信息。
### 参数
### 返回值
[#SiteStatsVo](#sitestatsvo)
### 示例
```html
<ul th:with="stats = ${siteStatsFinder.getStats()}">
<li th:text="${stats.visit}"></li>
<li th:text="${stats.post}"></li>
</ul>
```
## 类型定义
### SiteStatsVo
```json title="SiteStatsVo"
{
"visit": 0, // 访问数量
"upvote": 0, // 点赞数量
"comment": 0, // 评论数量
"post": 0, // 文章数量
"category": 0 // 分类数量
}
```

@ -0,0 +1,139 @@
---
title: 文章标签
description: 文章标签 - TagFinder
---
import TagVo from "../vo/TagVo.md"
## getByName(name)
```js
tagFinder.getByName(name)
```
### 描述
根据 `metadata.name` 获取标签。
### 参数
1. `name:string` - 标签的唯一标识 `metadata.name`
### 返回值
[#TagVo](#tagvo)
### 示例
```html
<div th:with="tag = ${tagFinder.getByName('tag-foo')}">
<a th:href="@{${tag.status.permalink}}" th:text="${tag.spec.displayName}"></a>
</div>
```
## getByNames(names)
```js
tagFinder.getByNames(names)
```
### 描述
根据一组 `metadata.name` 获取标签。
### 参数
1. `names:List<string>` - 标签的唯一标识 `metadata.name` 的集合。
### 返回值
List<[#TagVo](#tagvo)>
### 示例
```html
<div th:with="tags = ${tagFinder.getByNames(['tag-foo', 'tag-bar'])}">
<a th:each="tag : ${tags}" th:href="@{${tag.status.permalink}}" th:text="${tag.spec.displayName}"></a>
</div>
```
## list(page,size)
```js
tagFinder.list(page,size)
```
### 描述
根据分页参数获取标签列表。
### 参数
1. `page:int` - 分页页码,从 1 开始
2. `size:int` - 分页条数
### 返回值
[#ListResult<TagVo\>](#listresulttagvo)
### 示例
```html
<ul th:with="tags = ${tagFinder.list(1,10)}">
<li th:each="tag : ${tags.items}">
<a th:href="@{${tag.status.permalink}}" th:text="${tag.spec.displayName}"></a>
</li>
</ul>
```
## listAll()
```js
tagFinder.listAll()
```
### 描述
获取所有文章标签。
### 参数
### 返回值
List<[#TagVo](#tagvo)>
### 示例
```html
<ul th:with="tags = ${tagFinder.listAll()}">
<li th:each="tag : ${tags}">
<a th:href="@{${tag.status.permalink}}" th:text="${tag.spec.displayName}"></a>
</li>
</ul>
```
## 类型定义
### TagVo
<TagVo />
### ListResult<TagVo\>
```json title="ListResult<TagVo>"
{
"page": 0, // 当前页码
"size": 0, // 每页条数
"total": 0, // 总条数
"items": "List<#TagVo>", // 标签列表数据
"first": true, // 是否为第一页
"last": true, // 是否为最后一页
"hasNext": true, // 是否有下一页
"hasPrevious": true, // 是否有上一页
"totalPages": 0 // 总页数
}
```
- [#TagVo](#tagvo)

@ -0,0 +1,119 @@
---
title: 主题
description: 主题 - ThemeFinder
---
## activation()
```js
themeFinder.activation()
```
### 描述
获取当前激活的主题。
### 参数
### 返回值
[#ThemeVo](#themevo)
### 示例
```html
<div th:with="theme = ${themeFinder.activation()}">
<h1 th:text="${theme.spec.displayName}"></h1>
<p th:text="${theme.spec.version}"></p>
</div>
```
## getByName(themeName)
```js
themeFinder.getByName(themeName)
```
### 描述
根据主题的唯一标识 `metadata.name` 获取主题。
### 参数
- `themeName:string` - 主题名称
### 返回值
[#ThemeVo](#themevo)
### 示例
```html
<div th:with="theme = ${themeFinder.getByName('theme-foo')}">
<h1 th:text="${theme.spec.displayName}"></h1>
<p th:text="${theme.spec.version}"></p>
</div>
```
## 类型定义
### ThemeVo
```json title="ThemeVo"
{
"metadata": {
"name": "string", // 唯一标识
"labels": {
"additionalProp1": "string"
},
"annotations": {
"additionalProp1": "string"
},
"creationTimestamp": "2022-11-20T15:27:15.036Z", // 创建时间
},
"spec": {
"displayName": "string", // 显示名称
"author": {
"name": "string", // 作者名称
"website": "string" // 作者网站
},
"description": "string", // 描述
"logo": "string", // Logo
"website": "string", // 网站
"repo": "string", // 仓库地址
"version": "string", // 版本
"require": "string", // 依赖 Halo 的版本
"settingName": "string", // 表单定义的名称,即 Setting 资源的 metadata.name
"configMapName": "string", // 设置项存储的名称,即 ConfigMap 资源的 metadata.name
"customTemplates": {
"post": [
{
"name": "string",
"description": "string",
"screenshot": "string",
"file": "string"
}
],
"category": [
{
"name": "string",
"description": "string",
"screenshot": "string",
"file": "string"
}
],
"page": [
{
"name": "string",
"description": "string",
"screenshot": "string",
"file": "string"
}
]
}
},
"config": {}
}
```

@ -0,0 +1,58 @@
---
title: 全局变量
description: 本文档介绍 Halo 为模板引擎提供的专有全局变量。
---
import SiteSettingVo from "./vo/SiteSettingVo.md"
import ThemeVo from "./vo/ThemeVo.md"
Halo 目前为模板引擎在全局提供了一些变量,本文档将列出已提供的变量以及介绍这些变量的使用方法。
## site
### 描述
提供了部分可公开的系统相关的设置项,其中所有参数均来自于 Console 的系统设置。
### 类型
<SiteSettingVo />
### 示例
显示站点标题:
```html
<h1 th:text="${site.title}"></h1>
```
显示站点 Logo
```html
<img th:src="${site.logo}" alt="Logo" />
```
## theme
### 描述
关于当前激活主题的信息。
### 类型
<ThemeVo />
### 示例
显示主题名称:
```html
<h1 th:text="${theme.spec.displayName}"></h1>
```
在静态资源加入版本号参数,以防止升级之后的缓存问题:
```html
<link rel="stylesheet" th:href="@{/assets/dist/style.css?v={version}(version=${theme.spec.version})}" />
<script th:src="@{/assets/dist/main.iife.js?v={version}(version=${theme.spec.version})}"></script>
```

@ -0,0 +1,104 @@
---
title: 准备工作
description: 主题开发所需的准备工作和基本的项目搭建
---
此文档将讲解 Halo 2.0 主题开发的基本流程,从创建主题项目到最终预览主题效果。
## 搭建开发环境
Halo 在本地开发环境的运行可参考[开发环境运行](../core/run.md),或者使用 [Docker](../../getting-started/install/docker.md) 运行。
:::tip
为了保证在开发时,主题代码可以实时生效,需要注意以下事项:
- 使用 Halo 源码运行时,需要在配置文件中包含如下配置:
```yaml
spring:
thymeleaf:
cache: false
```
- 使用 Docker 运行时,需要添加 `SPRING_THYMELEAF_CACHE=false` 的环境变量。
:::
## 新建一个主题
Halo 的主题存放于工作目录的 `themes` 目录下,即 `~/halo2-dev/themes`,在该目录下新建一个文件夹,例如 `theme-foo`。当前一个最小可被系统加载的主题必须在主题根目录下包含 `theme.yaml` 配置文件。
```yaml title="theme.yaml"
apiVersion: theme.halo.run/v1alpha1
kind: Theme
metadata:
name: theme-foo
spec:
displayName: 示例主题
author:
name: halo-dev
website: https://halo.run
description: 一个示例主题
logo: https://halo.run/logo
website: https://github.com/halo-sigs/theme-foo
repo: https://github.com/halo-sigs/theme-foo.git
settingName: "theme-foo-setting"
configMapName: "theme-foo-configMap"
version: 1.0.0
require: 2.0.0
```
:::info 提示
主题的配置文件详细文档请参考 [配置文件](./config.md)。
:::
:::info 提示
主题项目的目录结构请参考 [主题目录结构](./structure.md)。
:::
## 通过模板创建
目前 Halo 为了让开发者能够尽快搭建主题项目,提供了一些初始模板,开发者可以根据实际需要选择使用。
- [halo-sigs/theme-starter](https://github.com/halo-dev/theme-starter) - 最基础的主题模板,包含了主题的基本目录结构。
- [halo-sigs/theme-vite-starter](https://github.com/halo-dev/theme-vite-starter) - 与 Vite 集成的主题模板,由 Vite 负责资源构建。
- [halo-sigs/theme-modern-starter](https://github.com/halo-dev/theme-modern-starter) - 集成了现代前端技术栈的 Halo 2.0 的主题开发模板。
- [halo-sigs/theme-astro-starter](https://github.com/halo-sigs/theme-astro-starter) - 与 Astro 集成的主题模板,使用 Astro 对模板进行预编译。
:::info 提示
以上 GitHub 都被设置为了模板仓库Template repository点击仓库主页的 `Use this template` 按钮即可通过此模板创建一个新的仓库。
:::
## 创建第一个页面模板
Halo 使用 [Thymeleaf](https://www.thymeleaf.org/) 作为后端模板引擎,后缀为 `.html`,与单纯编写 HTML 一致。在 Halo 的主题中,主题的模板文件存放于 `templates` 目录下,例如 `~/halo2-dev/themes/theme-foo/templates`。为了此文档方便演示,我们先在 `templates` 创建一个首页的模板文件 `index.html`
```html title="templates/index.html"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title th:text="${site.title}"></title>
</head>
<body>
<h1>Hello World!</h1>
<ul>
<li th:each="post : ${posts.items}">
<a th:href="@{${post.status.permalink}}" th:text="${post.spec.title}"></a>
</li>
</ul>
</body>
</html>
```
## 安装主题
目前我们已经创建好了主题的项目,但并不会直接被 Halo 识别和加载,请按照以下的步骤安装和启用主题:
1. 访问 Console 管理界面,进入主题管理页面。
2. 点击右上角 `切换主题` 按钮,在选择主题弹窗中切换到 `未安装` 页面。
3. 找到我们刚刚创建的主题,点击安装即可。
4. 选择刚刚安装的主题,点击右上角的 `启用` 按钮。
此时 Halo 就已经成功加载并使用了该主题。然后我们访问首页 [http://localhost:8090](http://localhost:8090) 就可以看到我们刚刚编写的 `index.html` 模板渲染后的页面了。

@ -0,0 +1,135 @@
---
title: 设置选项
description: 介绍主题如何定义以及使用设置选项。
---
此文档将讲解如何在主题中定义和使用设置项,如 [表单定义](../form-schema) 中所说,目前 Halo 的 Console 端的所有表单都使用了 [FormKit](https://github.com/formkit/formkit) 的方案。
:::tip
有关 FormKit 定义表单的更多信息,请参考 [表单定义](../form-schema),此文档仅针对主题中的设置项进行讲解。
:::
## 定义表单
在主题中要使用设置项非常简单,只需要在主题根目录提供 `settings.yaml`,然后在 `theme.yaml` 中配置 `spec.settingName``spec.configMapName` 即可,在安装或者初始化主题的时候会自动识别并在 Console 端的主题设置中生成表单。
### 示例
```yaml title="theme-foo/theme.yaml" {14,15}
apiVersion: theme.halo.run/v1alpha1
kind: Theme
metadata:
name: theme-foo
spec:
displayName: 示例主题
author:
name: halo-dev
website: https://halo.run
description: 一个示例主题
logo: https://halo.run/logo
website: https://github.com/halo-sigs/theme-foo
repo: https://github.com/halo-sigs/theme-foo.git
settingName: "theme-foo-setting"
configMapName: "theme-foo-configMap"
version: 1.0.0
require: 2.0.0
```
:::tip
`settingName``configMapName` 必须同时配置,且可以自定义名称,但是 `settingName` 必须和 Setting 的 `metadata.name` 一致。
:::
```yaml title="theme-foo/settings.yaml" {4}
apiVersion: v1alpha1
kind: Setting
metadata:
name: theme-foo-setting
spec:
forms:
- group: style
label: 样式
formSchema:
- $formkit: radio
name: color_scheme
label: 默认配色
value: system
options:
- label: 跟随系统
value: system
- label: 深色
value: dark
- label: 浅色
value: light
- $formkit: color
name: background_color
label: 背景颜色
value: "#f2f2f2"
- group: layout
label: 布局
formSchema:
- $formkit: radio
name: nav
label: 导航栏布局
value: "single"
options:
- label: 单栏
value: "single"
- label: 双栏
value: "double"
```
:::tip
Setting 资源的 `metadata.name` 必须和 `theme.yaml` 中的 `spec.settingName` 一致。
:::
### 在主题模板中使用
在主题模板中,需要以 `theme.config.[group].[name]` 的形式进行调用。
其中:
1. `group`: 即 `spec.forms[].group`,如上面示例中的 `style``layout`
2. `name`: 即 `spec.forms[].formSchema[].name`,如上面示例中的 `color_scheme``nav`
示例:
```html
<body th:class="${theme.config.style.color_scheme}">
<!-- do something -->
</body>
```
```html
<ul th:if="${theme.config.layout.nav == 'single'}">
<!-- do something -->
</ul>
<div th:if="${theme.config.layout.nav == 'double'}">
<!-- do something -->
</div>
```
## 更新配置
`theme.yaml` 一样,`settings.yaml` 也是持久化存储在数据库中的,不会在修改之后主动更新。同样在主题详情页面点击 `重载主题配置` 即可。
![重载主题配置](/img/theme/reload-theme-config.png)
## 从 1.x 迁移
为了方便主题开发者从 1.x 迁移,我们提供了工具用于迁移设置表单配置文件。
工具仓库地址:<https://github.com/halo-sigs/convert-theme-config-to-next>
```bash
# 1.x 版本主题
cd path/to/theme
npx @halo-dev/convert-theme-config-to-next settings
```
执行完成之后即可看到主题目录下生成了 `settings.2.0.yaml` 文件,重命名为 `settings.yaml` 即可。
:::tip
转换完成之后需要修改 `metadata.name` 字段。
:::

@ -0,0 +1,55 @@
---
title: 静态资源
description: 本文档介绍主题的静态资源的引用方法。
---
通过 [目录结构](./structure.md) 的讲解我们可以知道,目前主题的静态资源统一托管在 `/templates/assets/` 目录下,下面讲解一下如何在模板中使用,大致会分为两种引入方式。
## 模板标签引用
```html
<link rel="stylesheet" th:href="@{/assets/dist/style.css}" />
<script th:src="@{/assets/dist/main.iife.js}"></script>
<img th:src="@{/assets/images/logo.png}" />
```
其中 `@{/assets/dist/style.css}` 表示引用 `/templates/assets/dist/style.css` 文件。最终会被渲染为:
```html
<link rel="stylesheet" href="/themes/my-theme/assets/dist/style.css" />
```
## API 引用
以上方式仅支持在 HTML 标签中使用,且必须使用 `@{}` 包裹才能渲染为正确的路径。如果需要在非 HTML 标签中得到正确的路径,我们提供了 `#theme.assets()` API。
:::info 注意
需要注意的是,调用 `#theme.assets()` 的时候,资源地址不需要添加 `/assets/`
:::
比如我们需要在 JavaScript 中异步获取一些资源:
```html {3}
<script th:inline="javascript">
loadScript('[(${#theme.assets("/dist/main.iife.js")})]');
// loadScript('/themes/my-theme/assets/dist/main.iife.js');
function loadScript(url) {
return new Promise(function (resolve, reject) {
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
</script>
```
:::info 提示
关于在 JavaScript 中使用 Thymeleaf 语法可以参考 Thymeleaf 官方文档:[JavaScript inlining](https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#javascript-inlining)
:::

@ -0,0 +1,33 @@
---
title: 目录结构
description: 主题的目录结构介绍
---
Halo 2.0 的主题基本目录结构如下:
```bash title="~/halo2-dev/themes/my-theme"
my-theme
├── templates/
│ ├── assets/
│ │ ├── css/
│ │ │ └── style.css
│ │ └── js/
│ │ └── main.js
│ ├── index.html
│ ├── post.html
│ ├── page.html
│ ├── tag.html
│ ├── tags.html
│ ├── category.html
│ ├── categories.html
│ └── archives.html
├── theme.yaml
└── settings.yaml
```
详细说明:
1. `/templates/` - 主题模板目录,存放主题模板文件,所有模板都需要放在这个目录。关于模板的详细说明,请查阅 [模板路由](./template-route-mapping)。
2. `/templates/assets/` - 主题静态资源目录,存放主题的静态资源文件,目前静态资源文件只能放在这个目录,引用方式请查阅 [静态资源](./static-resources)。
3. `/theme.yaml` - 主题配置文件,配置主题的基本信息,如主题名称、版本、作者等。详细文档请查阅 [配置文件](./config)。
4. `/settings.yaml` - 主题设置定义文件,配置主题的设置项表单。详细文档请查阅 [设置选项](./settings)。

@ -0,0 +1,87 @@
---
title: 模板路由
description: 本文档介绍路由与模板的映射关系,以及自定义模板。
---
此文档讲解系统内部提供的路由与模板映射。
## 主要模板
### index.html
站点的首页模板,访问地址为 `/`
### post.html
文章详情页面的模板,访问地址默认为 `/archives/:slug`
### page.html
独立页面详情的模板,访问地址默认为 `/:slug`
### archives.html
文章归档页面的模板,访问地址包括:
- `/archives`
- `/archives/:year`
- `/archives/:year/:month`
### tags.html
标签集合页面的模板,访问地址默认为 `/tags`
### tag.html
标签归档页面的模板,访问地址默认为 `/tags/:slug`
### categories.html
分类集合页面的模板,访问地址默认为 `/categories`
### category.html
分类归档页面的模板,访问地址默认为 `/categories/:slug`
## 自定义模板 {#custom-templates}
一般情况下,上文提到的模板已经能够满足大部分的需求,但如果需要针对某个特定的页面进行自定义,可以通过自定义模板来实现。目前系统支持为 **文章**、**独立页面**和**分类归档** 设置自定义模板:
`theme.yaml``spec` 节点下添加如下配置:
```yaml
customTemplates:
{type}:
- name: {name}
description: {description}
screenshot: {screenshot}
file: {file}.html
```
示例:
```yaml
customTemplates:
post:
- name: 文档
description: 文档类型的文章
screenshot:
file: post_documentation.html
```
字段说明:
- `type`:模板类型,目前支持 `post` `page` `category`
- `name`:模板名称
- `description`:模板描述
- `screenshot`:模板预览图
- `file`:模板文件名,需要在 `/templates/` 目录下创建
最终使用者即可在文章设置、独立页面设置、分类设置中选择自定义模板。
:::info 提示
1. 自定义模板与默认模板的功能相同,区别仅在于可以让使用者选择不同于默认模板风格的模板。
2. 自定义模板的文件名需要以 `.html` 结尾,且需要在 `/templates/` 目录下创建。
:::

@ -0,0 +1,57 @@
---
title: 自定义标签
description: 本文档介绍 Halo 为模板引擎提供的专有标签。
---
Halo 为满足部分代码注入和模板扩展点的需求,提供了一些专有标签,本文档将列出已支持的标签以及介绍这些标签的使用方法。
## halo:comment
### 描述
此标签用作评论组件的扩展点,如果有插件实现了这个扩展点,那么将在编写了此标签的模板中显示插件提供的内容。
### 使用示例
```html title="/templates/post.html"
<div th:if="${pluginFinder.available('PluginCommentWidget')}">
<halo:comment
group="content.halo.run"
kind="Post"
th:attr="name=${post.metadata.name}"
colorScheme="window.main.currentColorScheme"
/>
</div>
```
参数详解:
1. `group` - 自定义模型的分组,目前已支持的模型请参考下面表格。
2. `kind` - 自定义模型的类型,目前已支持的模型请参考下面表格。
3. `name` - 自定义模型数据的唯一标识。
4. `colorScheme` - 评论组件的颜色方案,支持 light 和 dark 两种,支持固定或者 JavaScript 变量。需要注意的是,如果需要固定一个值,那么需要添加单引号,如 'dark'。使用 JavaScript 变量时不需要。
已支持的模型列表:
| 对应模型 | group | kind |
| ---------- | ---------------- | ---------- |
| 文章 | content.halo.run | Post |
| 自定义页面 | content.halo.run | SinglePage |
## halo:footer
### 描述
支持将系统设置中的页脚代码注入内容插入到此标签。
### 使用示例
```html
<footer>
<halo:footer />
</footer>
```
:::info 注意
为了保证 Halo 的功能完整性,建议主题开发者尽可能在主题中实现此标签。
:::

@ -0,0 +1,7 @@
---
title: 模板变量
---
import DocCardList from '@theme/DocCardList';
<DocCardList />

@ -0,0 +1,108 @@
---
title: 文章归档
description: archives.html - /archives
---
import CategoryVo from "../vo/CategoryVo.md";
import TagVo from "../vo/TagVo.md";
import ContributorVo from "../vo/ContributorVo.md";
import ListedPostVo from "../vo/ListedPostVo.md";
## 路由信息
- 模板路径:`/templates/archives.html`
- 访问路径
- `/archives`
- `/archives/:year`
- `/archives/:year/:month`
## 变量
### archives
#### 变量类型
[#UrlContextListResult<PostArchiveVo\>](#urlcontextlistresultpostarchivevo)
#### 示例
```html title="/templates/archives.html"
<th:block th:each="archive : ${archives.items}">
<h1 th:text="${archive.year}"></h1>
<ul>
<th:block th:each="month : ${archive.months}">
<li th:each="post : ${month.posts}">
<a th:href="@{${post.status.permalink}}" th:text="${post.spec.title}">
</a>
</li>
</th:block>
</ul>
</th:block>
<div th:if="${archives.hasPrevious() || archives.hasNext()}">
<a th:href="@{${archives.prevUrl}}">
<span>上一页</span>
</a>
<span th:text="${archives.page} +' / '+ ${archives.total}"></span>
<a th:href="@{${archives.nextUrl}}">
<span>下一页</span>
</a>
</div>
```
## 类型定义
### CategoryVo
<CategoryVo />
### TagVo
<TagVo />
### ContributorVo
<ContributorVo />
### ListedPostVo
<ListedPostVo />
- [#CategoryVo](#categoryvo)
- [#TagVo](#tagvo)
- [#ContributorVo](#contributorvo)
### PostArchiveVo
```json title="PostArchiveVo"
{
"year": "string", // 年份
"months": [ // 按月的文章集合
{
"month": "string", // 月份
"posts": "List<#ListedPostVo>" // 文章列表数据
}
]
}
```
- [#ListedPostVo](#listedpostvo)
### UrlContextListResult<PostArchiveVo\>
```json title="UrlContextListResult<PostArchiveVo>"
{
"page": 0, // 当前页码
"size": 0, // 每页条数
"total": 0, // 总条数
"items": "List<#PostArchiveVo>", // 文章归档数据
"first": true, // 是否为第一页
"last": true, // 是否为最后一页
"hasNext": true, // 是否有下一页
"hasPrevious": true, // 是否有上一页
"totalPages": 0, // 总页数
"nextUrl": "string", // 下一页链接
"prevUrl": "string" // 上一页链接
}
```
- [#PostArchiveVo](#postarchivevo)

@ -0,0 +1,104 @@
---
title: 作者归档
description: author.html - /authors/:name
---
import UserVo from "../vo/UserVo.md"
import CategoryVo from "../vo/CategoryVo.md"
import TagVo from "../vo/TagVo.md"
import ContributorVo from "../vo/ContributorVo.md"
import ListedPostVo from "../vo/ListedPostVo.md"
## 路由信息
- 模板路径:`/templates/author.html`
- 访问路径:`/authors/:name`
## 变量
### author
#### 变量类型
[#UserVo](#uservo)
### posts
#### 变量类型
[#UrlContextListResult<ListedPostVo\>](#urlcontextlistresultlistedpostvo)
#### 示例
```html title="/templates/author.html"
<div>
<h1 th:text="${author.spec.displayName}"></h1>
<ul>
<li th:each="post : ${posts.items}">
<a
th:text="${post.spec.title}"
th:href="${post.status.permalink}"
></a>
</li>
</ul>
<div th:if="${posts.hasPrevious() || posts.hasNext()}">
<a
th:href="@{${posts.prevUrl}}"
>
<span>上一页</span>
</a>
<span th:text="${posts.page} +' / '+ ${posts.total}"></span>
<a
th:href="@{${posts.nextUrl}}"
>
<span>下一页</span>
</a>
</div>
</div>
```
## 类型定义
### UserVo
<UserVo />
### CategoryVo
<CategoryVo />
### TagVo
<TagVo />
### ContributorVo
<ContributorVo />
### ListedPostVo
<ListedPostVo />
- [#CategoryVo](#categoryvo)
- [#TagVo](#tagvo)
- [#ContributorVo](#contributorvo)
### UrlContextListResult<ListedPostVo\>
```json title="UrlContextListResult<ListedPostVo>"
{
"page": 0, // 当前页码
"size": 0, // 每页条数
"total": 0, // 总条数
"items": "List<#ListedPostVo>", // 文章列表数据
"first": true, // 是否为第一页
"last": true, // 是否为最后一页
"hasNext": true, // 是否有下一页
"hasPrevious": true, // 是否有上一页
"totalPages": 0, // 总页数
"nextUrl": "string", // 下一页链接
"prevUrl": "string" // 上一页链接
}
```
- [#ListedPostVo](#listedpostvo)

@ -0,0 +1,54 @@
---
title: 文章分类集合
description: categories.html - /categories
---
import CategoryTreeVo from "../vo/CategoryTreeVo.md"
## 路由信息
- 模板路径:`/templates/categories.html`
- 访问路径:`/categories`
## 变量
### categories
#### 变量类型
List<[#CategoryTreeVo](#categorytreevo)>
#### 示例
```html title="/templates/categories.html"
<ul>
<li th:replace="~{modules/category-tree :: single(categories=${categories})}" />
</ul>
```
```html title="/templates/category-tree.html"
<ul th:fragment="next (categories)">
<li th:fragment="single (categories)" th:each="category : ${categories}">
<a th:href="@{${category.status.permalink}}">
<span th:text="${category.spec.displayName}"> </span>
</a>
<th:block th:if="${not #lists.isEmpty(category.children)}">
<th:block th:replace="~{modules/category-tree :: next (categories=${category.children})}"></th:block>
</th:block>
</li>
</ul>
```
### _templateId
#### 变量值
`categories`
## 类型定义
### CategoryTreeVo
<CategoryTreeVo />
- [#CategoryTreeVo](#categorytreevo)

@ -0,0 +1,105 @@
---
title: 分类归档
description: category.html - /categories/:slug
---
import CategoryVo from "../vo/CategoryVo.md"
import TagVo from "../vo/TagVo.md"
import ContributorVo from "../vo/ContributorVo.md";
import ListedPostVo from "../vo/ListedPostVo.md"
## 路由信息
- 模板路径:`/templates/category.html`
- 访问路径:`/categories/:slug`
## 变量
### category
#### 变量类型
[#CategoryVo](#categoryvo)
### posts
#### 变量类型
[#UrlContextListResult<ListedPostVo\>](#urlcontextlistresultlistedpostvo)
#### 示例
```html title="/templates/category.html"
<div>
<h1 th:text="${category.spec.displayName}"></h1>
<ul>
<li th:each="post : ${posts.items}">
<a
th:text="${post.spec.title}"
th:href="${post.status.permalink}"
></a>
</li>
</ul>
<div th:if="${posts.hasPrevious() || posts.hasNext()}">
<a
th:href="@{${posts.prevUrl}}"
>
<span>上一页</span>
</a>
<span th:text="${posts.page} +' / '+ ${posts.total}"></span>
<a
th:href="@{${posts.nextUrl}}"
>
<span>下一页</span>
</a>
</div>
</div>
```
### _templateId
#### 变量值
`category`
## 类型定义
### CategoryVo
<CategoryVo />
### TagVo
<TagVo />
### ContributorVo
<ContributorVo />
### ListedPostVo
<ListedPostVo />
- [#CategoryVo](#categoryvo)
- [#TagVo](#tagvo)
- [#ContributorVo](#contributorvo)
### UrlContextListResult<ListedPostVo\>
```json title="UrlContextListResult<ListedPostVo>"
{
"page": 0, // 当前页码
"size": 0, // 每页条数
"total": 0, // 总条数
"items": "List<#ListedPostVo>", // 文章列表数据
"first": true, // 是否为第一页
"last": true, // 是否为最后一页
"hasNext": true, // 是否有下一页
"hasPrevious": true, // 是否有上一页
"totalPages": 0, // 总页数
"nextUrl": "string", // 下一页链接
"prevUrl": "string" // 上一页链接
}
```
- [#ListedPostVo](#listedpostvo)

@ -0,0 +1,98 @@
---
title: 首页
description: index.html - /
---
import CategoryVo from "../vo/CategoryVo.md"
import TagVo from "../vo/TagVo.md"
import ContributorVo from "../vo/ContributorVo.md";
import ListedPostVo from "../vo/ListedPostVo.md"
## 路由信息
- 模板路径:`/templates/index.html`
- 访问路径:`/`
## 变量
### posts
#### 变量类型
[#UrlContextListResult<ListedPostVo\>](#urlcontextlistresultlistedpostvo)
#### 示例
```html title="/templates/index.html"
<div>
<ul>
<li th:each="post : ${posts.items}">
<a
th:text="${post.spec.title}"
th:href="${post.status.permalink}"
></a>
</li>
</ul>
<div th:if="${posts.hasPrevious() || posts.hasNext()}">
<a
th:href="@{${posts.prevUrl}}"
>
<span>上一页</span>
</a>
<span th:text="${posts.page} +' / '+ ${posts.total}"></span>
<a
th:href="@{${posts.nextUrl}}"
>
<span>下一页</span>
</a>
</div>
</div>
```
### _templateId
#### 变量值
`index`
## 类型定义
### CategoryVo
<CategoryVo />
### TagVo
<TagVo />
### ContributorVo
<ContributorVo />
### ListedPostVo
<ListedPostVo />
- [#CategoryVo](#categoryvo)
- [#TagVo](#tagvo)
- [#ContributorVo](#contributorvo)
### UrlContextListResult<ListedPostVo\>
```json title="UrlContextListResult<ListedPostVo>"
{
"page": 0, // 当前页码
"size": 0, // 每页条数
"total": 0, // 总条数
"items": "List<#ListedPostVo>", // 文章列表数据
"first": true, // 是否为第一页
"last": true, // 是否为最后一页
"hasNext": true, // 是否有下一页
"hasPrevious": true, // 是否有上一页
"totalPages": 0, // 总页数
"nextUrl": "string", // 下一页链接
"prevUrl": "string" // 上一页链接
}
```
- [#ListedPostVo](#listedpostvo)

@ -0,0 +1,53 @@
---
title: 独立页面
description: page.html - /:slug
---
import SinglePageVo from "../vo/SinglePageVo.md"
import ContributorVo from "../vo/ContributorVo.md"
import ContentVo from "../vo/ContentVo.md"
## 路由信息
- 模板路径:`/templates/page.html`
- 访问路径:`/:slug`
## 变量
### singlePage
#### 变量类型
[#SinglePageVo](#singlepagevo)
#### 示例
```html title="/templates/page.html"
<article>
<h1 th:text="${singlePage.spec.title}"></h1>
<div th:utext="${singlePage.content.content}"> </div>
</article>
```
### _templateId
#### 变量值
`page`
## 类型定义
### SinglePageVo
<SinglePageVo />
- [#ContentVo](#contentvo)
- [#ContributorVo](#contributorvo)
### ContentVo
<ContentVo />
### ContributorVo
<ContributorVo />

@ -0,0 +1,65 @@
---
title: 文章
description: post.html - /archives/:slug
---
import CategoryVo from "../vo/CategoryVo.md"
import TagVo from "../vo/TagVo.md"
import ContentVo from "../vo/ContentVo.md"
import ContributorVo from "../vo/ContributorVo.md"
import PostVo from "../vo/PostVo.md"
## 路由信息
- 模板路径:`/templates/post.html`
- 访问路径:`/archives/:slug`
## 变量
### post
#### 变量类型
[#PostVo](#postvo)
#### 示例
```html title="/templates/post.html"
<article>
<h1 th:text="${post.spec.title}"></h1>
<div th:utext="${post.content.content}"> </div>
</article>
```
### _templateId
#### 变量值
`post`
## 类型定义
### CategoryVo
<CategoryVo />
### TagVo
<TagVo />
### ContributorVo
<ContributorVo />
### ContentVo
<ContentVo />
### PostVo
<PostVo />
- [#CategoryVo](#categoryvo)
- [#TagVo](#tagvo)
- [#ContributorVo](#contributorvo)
- [#ContentVo](#contentvo)

@ -0,0 +1,105 @@
---
title: 标签归档
description: tag.html - /tags/:slug
---
import CategoryVo from "../vo/CategoryVo.md"
import TagVo from "../vo/TagVo.md"
import ContributorVo from "../vo/ContributorVo.md";
import ListedPostVo from "../vo/ListedPostVo.md"
## 路由信息
- 模板路径:`/templates/tag.html`
- 访问路径:`/tags/:slug`
## 变量
### tag
#### 变量类型
[#TagVo](#tagvo)
### posts
#### 变量类型
[#UrlContextListResult<ListedPostVo\>](#urlcontextlistresultlistedpostvo)
#### 示例
```html title="/templates/tag.html"
<div>
<h1 th:text="${tag.spec.displayName}"></h1>
<ul>
<li th:each="post : ${posts.items}">
<a
th:text="${post.spec.title}"
th:href="${post.status.permalink}"
></a>
</li>
</ul>
<div th:if="${posts.hasPrevious() || posts.hasNext()}">
<a
th:href="@{${posts.prevUrl}}"
>
<span>上一页</span>
</a>
<span th:text="${posts.page} +' / '+ ${posts.total}"></span>
<a
th:href="@{${posts.nextUrl}}"
>
<span>下一页</span>
</a>
</div>
</div>
```
### _templateId
#### 变量值
`tag`
## 类型定义
### CategoryVo
<CategoryVo />
### TagVo
<TagVo />
### ContributorVo
<ContributorVo />
### ListedPostVo
<ListedPostVo />
- [#CategoryVo](#categoryvo)
- [#TagVo](#tagvo)
- [#ContributorVo](#contributorvo)
### UrlContextListResult<ListedPostVo\>
```json title="UrlContextListResult<ListedPostVo>"
{
"page": 0, // 当前页码
"size": 0, // 每页条数
"total": 0, // 总条数
"items": "List<#ListedPostVo>", // 文章列表数据
"first": true, // 是否为第一页
"last": true, // 是否为最后一页
"hasNext": true, // 是否有下一页
"hasPrevious": true, // 是否有上一页
"totalPages": 0, // 总页数
"nextUrl": "string", // 下一页链接
"prevUrl": "string" // 上一页链接
}
```
- [#ListedPostVo](#listedpostvo)

@ -0,0 +1,39 @@
---
title: 文章标签集合
description: tags.html - /tags
---
import TagVo from '../vo/TagVo.md'
## 路由信息
- 模板路径:`/templates/tags.html`
- 访问路径:`/tags`
## 变量
### tags
#### 变量类型
List<[#TagVo](#tagvo)>
#### 示例
```html title="/templates/tags.html"
<ul>
<li th:each="tag : ${tags}" th:text="${tag.spec.displayName}" th:href="${tag.status.permalink}" />
</ul>
```
### _templateId
#### 变量值
`tags`
## 类型定义
### TagVo
<TagVo />

@ -0,0 +1,33 @@
```json title="CategoryTreeVo"
{
"metadata": {
"name": "string", // 唯一标识
"labels": {
"additionalProp1": "string"
},
"annotations": {
"additionalProp1": "string"
},
"creationTimestamp": "2022-11-20T14:18:49.230Z", // 创建时间
},
"spec": {
"displayName": "string", // 显示名称
"slug": "string", // 别名,通常用于生成 status.permalink
"description": "string", // 描述
"cover": "string", // 封面图
"template": "string", // 自定义渲染模板名称
"priority": 0, // 排序字段
"children": [ // 下级分类,分类的 metadata.name 集合
"string"
]
},
"status": {
"permalink": "string", // 固定链接
"postCount": 0, // 文章数
"visiblePostCount": 0 // 已发布文章数
},
"children": "List<#CategoryTreeVo>", // 下级分类CategoryTreeVo 的集合
"parentName": "string",
"postCount": 0
}
```

@ -0,0 +1,31 @@
```json title="CategoryVo"
{
"metadata": {
"name": "string", // 唯一标识
"labels": {
"additionalProp1": "string"
},
"annotations": {
"additionalProp1": "string"
},
"creationTimestamp": "2022-11-20T13:06:38.512Z", // 创建时间
},
"spec": {
"displayName": "string", // 显示名称
"slug": "string", // 别名,通常用于生成 status.permalink
"description": "string", // 描述
"cover": "string", // 封面图
"template": "string", // 自定义渲染模板名称
"priority": 0, // 排序字段
"children": [ // 下级分类,分类的 metadata.name 集合
"string"
]
},
"status": {
"permalink": "string", // 固定链接
"postCount": 0, // 文章数
"visiblePostCount": 0 // 已发布文章数
},
"postCount": 0
}
```

@ -0,0 +1,53 @@
```json title="CommentVo"
{
"metadata": {
"name": "string", // 唯一标识
"labels": {
"additionalProp1": "string"
},
"annotations": {
"additionalProp1": "string"
},
"creationTimestamp": "2022-11-20T12:16:19.788Z" // 创建时间
},
"spec": {
"raw": "string", // 原始文本,一般用于给编辑器使用
"content": "string", // 最终渲染的文本
"owner": { // 创建者关联
"kind": "string",
"name": "string",
"displayName": "string",
"annotations": {
"additionalProp1": "string"
}
},
"userAgent": "string", // 评论者 UserAgent 信息
"ipAddress": "string", // 评论者 IP 地址
"priority": 0, // 排序字段
"top": false, // 是否置顶
"allowNotification": true, // 是否允许通知
"approved": false,
"hidden": false,
"subjectRef": { // 引用关联,比如文章、自定义页面
"group": "string",
"version": "string",
"kind": "string",
"name": "string"
},
"lastReadTime": "2022-11-20T12:16:19.788Z"
},
"status": {
"lastReplyTime": "2022-11-20T12:16:19.788Z",
"replyCount": 0, // 回复数量
"unreadReplyCount": 0,
"hasNewReply": true // 是否有新回复
},
"owner": { // 创建者信息
"kind": "string",
"name": "string",
"displayName": "string",
"avatar": "string",
"email": "string"
}
}
```

@ -0,0 +1,6 @@
```json title="ContentVo"
{
"raw": "string", // 原始文本,一般用于给编辑器使用
"content": "string" // 最终渲染的文本
}
```

@ -0,0 +1,9 @@
```json title="ContributorVo"
{
"name": "string", // 用户名
"displayName": "string", // 显示名称
"avatar": "string", // 头像
"bio": "string", // 描述
"permalink": "string" // 作者的文章归档页面链接
}
```

@ -0,0 +1,65 @@
```json title="ListedPostVo"
{
"metadata": {
"name": "string", // 唯一标识
"labels": {
"additionalProp1": "string"
},
"annotations": {
"additionalProp1": "string"
},
"creationTimestamp": "2022-11-20T13:06:38.505Z", // 创建时间
},
"spec": {
"title": "string", // 标题
"slug": "string", // 别名,通常用于生成 status.permalink
"releaseSnapshot": "string",
"headSnapshot": "string",
"baseSnapshot": "string",
"owner": "string", // 创建者名称,即 ContributorVo 的 metadata.name非显示名称
"template": "string", // 自定义渲染模板
"cover": "string", // 封面图
"deleted": false,
"publish": false,
"publishTime": "2022-11-20T13:06:38.505Z", // 发布时间
"pinned": false, // 是否置顶
"allowComment": true, // 是否允许评论
"visible": "PUBLIC",
"priority": 0,
"excerpt": {
"autoGenerate": true, // 是否自动生成摘要
"raw": "string" // 摘要内容
},
"categories": [ // 分类的名称集合,即 Category 的 metadata.name 的集合
"string"
],
"tags": [ // 标签的名称集合,即 Tag 的 metadata.name 的集合
"string"
],
"htmlMetas": [
{
"additionalProp1": "string"
}
]
},
"status": {
"permalink": "string", // 固定链接
"excerpt": "string", // 最终生成的摘要
"inProgress": true,
"lastModifyTime": "2022-11-20T13:06:38.505Z", // 最后修改时间
"commentsCount": 0, // 评论数
"contributors": [ // 贡献者名称Contributor 的 metadata.name 的集合
"string"
]
},
"categories": "List<#CategoryVo>", // 分类的集合
"tags": "List<#TagVo>", // 标签的集合
"contributors": "List<#ContributorVo>", // 贡献者的集合
"owner": "#ContributorVo", // 创建者
"stats": {
"visit": 0, // 访问数量
"upvote": 0, // 点赞数量
"comment": 0 // 评论数量
}
}
```

@ -0,0 +1,57 @@
```json title="ListedSinglePageVo"
{
"metadata": {
"name": "string", // 唯一标识
"labels": {
"additionalProp1": "string"
},
"annotations": {
"additionalProp1": "string"
},
"creationTimestamp": "2022-11-20T14:31:00.876Z" // 创建时间
},
"spec": {
"title": "string", // 标题
"slug": "string", // 别名,通常用于生成 status.permalink
"releaseSnapshot": "string",
"headSnapshot": "string",
"baseSnapshot": "string",
"owner": "string", // 创建者名称,即 ContributorVo 的 metadata.name非显示名称
"template": "string", // 自定义渲染模板
"cover": "string", // 封面图
"deleted": false,
"publish": false,
"publishTime": "2022-11-20T14:31:00.876Z", // 发布时间
"pinned": false, // 是否置顶
"allowComment": true, // 是否允许评论
"visible": "PUBLIC",
"priority": 0,
"excerpt": {
"autoGenerate": true, // 是否自动生成摘要
"raw": "string" // 摘要内容
},
"htmlMetas": [
{
"additionalProp1": "string"
}
]
},
"status": {
"permalink": "string", // 固定链接
"excerpt": "string", // 最终生成的摘要
"inProgress": true,
"lastModifyTime": "2022-11-20T13:06:38.505Z", // 最后修改时间
"commentsCount": 0, // 评论数
"contributors": [ // 贡献者名称Contributor 的 metadata.name 的集合
"string"
]
},
"stats": {
"visit": 0, // 访问数量
"upvote": 0, // 点赞数量
"comment": 0 // 评论数量
},
"contributors": "List<#ContributorVo>", // 贡献者的集合
"owner": "#ContributorVo" // 创建者
}
```

@ -0,0 +1,44 @@
```json title="MenuItemVo"
{
"metadata": {
"name": "string", // 唯一标识
"labels": {
"additionalProp1": "string"
},
"annotations": {
"additionalProp1": "string"
},
"creationTimestamp": "2022-11-20T14:44:58.984Z", // 创建时间
},
"spec": {
"displayName": "string", // 显示名称,但是不要直接使用这个字段进行显示,最终字段为 status.displayName
"href": "string", // 链接,同样不要直接使用这个字段,最终字段为 status.href
"priority": 0, // 排序字段
"children": [ // 下级菜单项,菜单项的 metadata.name 集合
"string"
],
"target": "#Target", // 菜单页面打开方式,枚举类型
"targetRef": { // 与其他资源比如文章的关联,一般无需直接使用
"group": "string",
"version": "string",
"kind": "string",
"name": "string"
},
},
"status": {
"displayName": "string", // 显示名称
"href": "string" // 链接
},
"children": "List<#MenuItemVo>", // 下级菜单项MenuItemVo 的集合
"parentName": "string",
}
```
```java title="Target"
enum Target {
BLANK("_blank"), // 在新窗口打开
SELF("_self"), // 在当前窗口打开
PARENT("_parent"), // 在父窗口打开
TOP("_top"); // 在顶级窗口打开
}
```

@ -0,0 +1,21 @@
```json title="MenuVo"
{
"metadata": {
"name": "string", // 唯一标识
"labels": {
"additionalProp1": "string"
},
"annotations": {
"additionalProp1": "string"
},
"creationTimestamp": "2022-11-20T14:44:58.984Z", // 创建时间
},
"spec": {
"displayName": "string", // 显示名称
"menuItems": [ // 菜单的菜单项名称集合,即 MenuItem 的 metadata.name 的集合
"string"
]
},
"menuItems": "List<#MenuItemVo>" // 菜单项的集合
}
```

@ -0,0 +1,66 @@
```json title="PostVo"
{
"metadata": {
"name": "string", // 唯一标识
"labels": {
"additionalProp1": "string"
},
"annotations": {
"additionalProp1": "string"
},
"creationTimestamp": "2022-11-20T12:45:43.888Z", // 创建时间
},
"spec": {
"title": "string", // 标题
"slug": "string", // 别名,通常用于生成 status.permalink
"releaseSnapshot": "string",
"headSnapshot": "string",
"baseSnapshot": "string",
"owner": "string", // 创建者名称,即 ContributorVo 的 metadata.name非显示名称
"template": "string", // 自定义渲染模板
"cover": "string", // 封面图
"deleted": false,
"publish": false,
"publishTime": "2022-11-20T13:06:38.505Z", // 发布时间
"pinned": false, // 是否置顶
"allowComment": true, // 是否允许评论
"visible": "PUBLIC",
"priority": 0,
"excerpt": {
"autoGenerate": true, // 是否自动生成摘要
"raw": "string" // 摘要内容
},
"categories": [ // 分类的名称集合,即 Category 的 metadata.name 的集合
"string"
],
"tags": [ // 标签的名称集合,即 Tag 的 metadata.name 的集合
"string"
],
"htmlMetas": [
{
"additionalProp1": "string"
}
]
},
"status": {
"permalink": "string", // 固定链接
"excerpt": "string", // 最终生成的摘要
"inProgress": true,
"lastModifyTime": "2022-11-20T13:06:38.505Z", // 最后修改时间
"commentsCount": 0, // 评论数
"contributors": [ // 贡献者名称Contributor 的 metadata.name 的集合
"string"
]
},
"categories": "List<#CategoryVo>", // 分类的集合
"tags": "List<#TagVo>", // 标签的集合
"contributors": "List<#ContributorVo>", // 贡献者的集合
"owner": "#ContributorVo", // 创建者
"stats": {
"visit": 0, // 访问数量
"upvote": 0, // 点赞数量
"comment": 0 // 评论数量
},
"content": "#ContentVo" // 内容
}
```

@ -0,0 +1,42 @@
```json title="ReplyVo"
{
"metadata": {
"name": "string", // 唯一标识
"labels": {
"additionalProp1": "string"
},
"annotations": {
"additionalProp1": "string"
},
"creationTimestamp": "2022-11-20T12:25:32.357Z" // 创建时间
},
"spec": {
"raw": "string", // 原始文本,一般用于给编辑器使用
"content": "string", // 最终渲染的文本
"owner": { // 创建者关联
"kind": "string",
"name": "string",
"displayName": "string",
"annotations": {
"additionalProp1": "string"
}
},
"userAgent": "string", // 评论者 UserAgent 信息
"ipAddress": "string", // 评论者 IP 地址
"priority": 0, // 排序字段
"top": false, // 是否置顶
"allowNotification": true, // 是否允许通知
"approved": false,
"hidden": false,
"commentName": "string", // 被回复的评论名称,即 Comment 的 metadata.name
"quoteReply": "string" // 被回复的回复名称,即 Reply 的 metadata.name
},
"owner": { // 创建者信息
"kind": "string",
"name": "string",
"displayName": "string",
"avatar": "string",
"email": "string"
}
}
```

@ -0,0 +1,58 @@
```json title="SinglePageVo"
{
"metadata": {
"name": "string", // 唯一标识
"labels": {
"additionalProp1": "string"
},
"annotations": {
"additionalProp1": "string"
},
"creationTimestamp": "2022-11-20T14:29:44.601Z", // 创建时间
},
"spec": {
"title": "string", // 标题
"slug": "string", // 别名,通常用于生成 status.permalink
"releaseSnapshot": "string",
"headSnapshot": "string",
"baseSnapshot": "string",
"owner": "string", // 创建者名称,即 ContributorVo 的 metadata.name非显示名称
"template": "string", // 自定义渲染模板
"cover": "string", // 封面图
"deleted": false,
"publish": false,
"publishTime": "2022-11-20T13:06:38.505Z", // 发布时间
"pinned": false, // 是否置顶
"allowComment": true, // 是否允许评论
"visible": "PUBLIC",
"priority": 0,
"excerpt": {
"autoGenerate": true, // 是否自动生成摘要
"raw": "string" // 摘要内容
},
"htmlMetas": [
{
"additionalProp1": "string"
}
]
},
"status": {
"permalink": "string", // 固定链接
"excerpt": "string", // 最终生成的摘要
"inProgress": true,
"lastModifyTime": "2022-11-20T13:06:38.505Z", // 最后修改时间
"commentsCount": 0, // 评论数
"contributors": [ // 贡献者名称Contributor 的 metadata.name 的集合
"string"
]
},
"stats": {
"visit": 0, // 访问数量
"upvote": 0, // 点赞数量
"comment": 0 // 评论数量
},
"contributors": "List<#ContributorVo>", // 贡献者的集合
"owner": "#ContributorVo", // 创建者
"content": "#ContentVo" // 内容
}
```

@ -0,0 +1,26 @@
```json title="SiteSettingVo"
{
"title": "string", // 站点标题
"subtitle": "string", // 站点副标题
"url": "string", // 站点的外部访问链接
"logo": "string", // Logo 地址
"favicon": "string", // Favicon 地址
"allowRegistration": false, // 是否允许注册
"post": { // 文章相关设置
"postPageSize": 10, // 首页默认分页大小
"archivePageSize": 10, // 归档页默认分页大小
"categoryPageSize": 10, // 分类归档页默认分页大小
"tagPageSize": 10 // 标签归档页默认分页大小
},
"seo": { // SEO 相关设置
"blockSpiders": false, // 禁止搜索引擎抓取
"keywords": "string", // 站点全局关键词一般不需要主动使用Halo 会自动插入到 head 标签中
"description": "string" // 站点全局描述一般不需要主动使用Halo 会自动插入到 head 标签中
},
"comment": { // 评论相关设置
"enable": true, // 是否开启评论
"systemUserOnly": false, // 是否只允许登录用户评论
"requireReviewForNew": false // 是否需要审核新评论
}
}
```

@ -0,0 +1,26 @@
```json title="TagVo"
{
"metadata": {
"name": "string", // 唯一标识
"labels": {
"additionalProp1": "string"
},
"annotations": {
"additionalProp1": "string"
},
"creationTimestamp": "2022-11-20T13:06:38.512Z", // 创建时间
},
"spec": {
"displayName": "string", // 显示名称
"slug": "string", // 别名,通常用于生成 status.permalink
"color": "#F9fEB1", // 背景颜色
"cover": "string" // 封面图
},
"status": {
"permalink": "string", // 固定链接
"visiblePostCount": 0, // 已发布文章数
"postCount": 0 // 文章数
},
"postCount": 0
}
```

@ -0,0 +1,36 @@
```json title="ThemeVo"
{
"metadata": {
"name": "string", // 唯一标识
"labels": {
"additionalProp1": "string"
},
"annotations": {
"additionalProp1": "string"
},
"creationTimestamp": "2022-11-20T14:44:58.984Z" // 创建时间
},
"spec": {
"displayName": "string", // 显示名称
"author": { // 作者相关信息
"name": "string", // 作者名称
"website": "string" // 作者网站
},
"description": "string", // 主题描述
"logo": "string", // 主题 Logo
"website": "string", // 主题网站
"repo": "string", // 主题仓库地址
"version": "string", // 主题版本
"requires": "string", // 主题依赖 Halo 版本的设置
"settingName": "string", // 主题设置表单名称
"configMapName": "string", // 主题配置名称
"customTemplates": {} // 主题自定义模板设置
},
"config": {} // 主题配置
}
```
其中:
1. `customTemplates`:一般不会在模板引擎中使用,使用文档请参考:[模板路由#自定义模板](../template-route-mapping.md#custom-templates)
2. `config`:主题配置,使用文档请参考:[设置选项](../settings.md)

@ -0,0 +1,28 @@
```json title="UserVo"
{
"metadata": {
"name": "string", // 唯一标识
"labels": {
"additionalProp1": "string"
},
"annotations": {
"additionalProp1": "string"
},
"creationTimestamp": "2022-11-20T13:06:38.512Z" // 创建时间
},
"spec": {
"displayName": "string", // 显示名称
"avatar": "string", // 头像链接
"email": "string", // 邮箱地址
"phone": "string", // 电话号码
"bio": 0, // 描述
"registeredAt": "2022-11-20T13:06:38.512Z", // 注册时间
"twoFactorAuthEnabled": false, // 是否启用二次验证
"disabled": false // 是否禁用
},
"status": {
"lastLoginAt": "2022-11-20T13:06:38.512Z", // 最后登录时间
"permalink": "string" // 作者的文章归档页面链接
}
}
```

@ -0,0 +1,238 @@
---
title: 配置参考
description: Halo 配置文件的详细介绍及参考
---
Halo 的配置文件名为 `application.yaml`,其必须位于[工作目录](/getting-started/prepare#工作目录) `~/.halo` 下。 Halo 会读取该目录下的配置文件进行加载。
:::info
如果您是单独下载的官方配置文件,则必须将文件名 application-template.yaml 重命名为 application.yaml
:::
如下将详细列出配置文件 `application.yaml` 中所有的配置项。
## 基础配置
基础配置中的配置设置一般来说是**必要的**,且必须在 application.yaml 里进行定义。
### 端口
用于指定 HTTP 服务器监听的端口Halo 默认设置为 `8090`
```yaml
server:
port: 8090
```
请注意,如果您选择设置端口为 `80`,则需要确保您的 80 端口未被占用,通常**不建议**直接设置为 80 端口。
### 数据库
Halo 目前支持 `H2``MySQL` 数据库。
:::tip
得益于我们使用的 ORM 框架Halo 在首次启动的时候会自动根据实体类创建表结构,无需通过 SQL 脚本自行创建,也不会提供所谓的 SQL 脚本。所以此步骤仅需配置好数据库连接地址和用户名密码即可。注意H2 无需手动创建数据库MySQL 需要。
:::
#### H2
:::info
推荐使用 **H2**,较其他数据库来说更为方便。
:::
```yaml
spring:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:file:~/.halo/db/halo
username: admin
password: 123456
h2:
console:
settings:
web-allow-others: false
path: /h2-console
enabled: false
```
**注意事项**
- `url` 为默认的数据本地存储地址,请勿修改。
- 默认的数据库账户和密码为 `admin``123456`,建议将其修改,并妥善保存(此用户名和密码在 Halo 第一次启动的时候将自动创建。并且不支持首次启动后,通过修改配置文件中的账户或者密码,如果修改,再次启动将提示用户名或者密码错误。)。
- 线上环境中,`h2` 的配置使用默认即可。如果需要手动修改一些数据,可将 `web-allow-others``enabled` 设为 `true` 来开启 h2 控制台,访问路径为 `ip:端口/h2-console`。`JDBC URL``username``password` 使用配置文件中的即可。
:::warning
特别注意:在开启 `h2-console` 并完成所需操作之后,一定要再次关闭 `h2-console` 并重启,不要长时间将 `h2-console` 处于开启状态,这可能会有隐性的安全风险。
:::
#### MySQL
```yaml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/halodb?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: 123456
```
**要求**
1. 版本5.7 +
2. 字符集Character Set`utf8mb4`
3. 排序规则Collate`utf8mb4_bin`
4. 存储引擎:`InnoDB`
综上,建议创建数据库采用下面的命令:
```bash
create database halodb character set utf8mb4 collate utf8mb4_bin;
```
**注意事项**
- `username``password` 需要修改为您的 MySQL 数据库账号和密码。
- 默认时区为 `Asia/Shanghai`,如果与您所在时区不一致,则可以修改为您所在的时区。
## 高级配置
高级配置中的配置设置是可选的,如果不需要,可以略过。
### 后台路径
Halo 支持自定义后台管理的**根路径**。
```yaml
halo:
# Your admin client path is https://your-domain/{admin-path}
admin-path: admin
```
注意:仅为改动后台管理的根路径,因此前后不带 `/`
### 缓存
某些情况下,需要用户根据需求来设置缓存数据的保存方式,例如将缓存数据持久化保存在本地。
```yaml
halo:
# memory or level or redis
cache: memory
```
目前支持三种策略:
- `memory` 将数据缓存至内存,重启服务缓存将清空。
- `level` 将数据缓存至本地,重启服务不会清空缓存。
- `redis` 将数据缓存至 Redis重启服务不会清空缓存如需分布式部署 Halo请选用此种缓存方式。
**注意事项**
- 如果选用 Redis 缓存方式,请在配置文件加入 Redis 相关配置,完整的配置示例如下:
```yaml
server:
port: 8090
# Response data gzip.
compression:
enabled: true
spring:
datasource:
# MySQL database configuration.
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/halodb?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: 123456
redis:
# Redis cache configuration.
port: 6379
database: 0
host: 127.0.0.1
password: 123456
halo:
# Your admin client path is https://your-domain/{admin-path}
admin-path: admin
# memory or level or redis
cache: redis
```
### 压缩
启用压缩对于减少带宽和加快页面加载非常有用,在**未使用** `Nginx``Caddy` 等反向代理服务器时(反向代理服务器通常是默认开启 Gzip 的),可以考虑开启系统自带的 Gzip 功能。
```yaml
server:
# Response data gzip.
compression:
enabled: true
```
## 示例配置文件
:::info
建议根据使用的数据库类型查看。
:::
### H2 Database
```yaml
server:
port: 8090
# Response data gzip.
compression:
enabled: true
spring:
datasource:
# H2 database configuration.
driver-class-name: org.h2.Driver
url: jdbc:h2:file:~/.halo/db/halo
username: admin
password: 123456
# H2 database console configuration.
h2:
console:
settings:
web-allow-others: false
path: /h2-console
enabled: false
halo:
# Your admin client path is https://your-domain/{admin-path}
admin-path: admin
# memory or level or redis
cache: memory
```
### MySQL
```yaml
server:
port: 8090
# Response data gzip.
compression:
enabled: true
spring:
datasource:
# MySQL database configuration.
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/halodb?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: 123456
halo:
# Your admin client path is https://your-domain/{admin-path}
admin-path: admin
# memory or level or redis
cache: memory
```
官方的完整示例配置文件可以在 [https://dl.halo.run/config/](https://dl.halo.run/config/) 下找到。

@ -0,0 +1,25 @@
---
title: 资源下载
description: 目前所有与 Halo 相关的下载地址
---
## GitHub
:::note
如果您的服务器在海外,推荐从 GitHub 下载。
:::
- [运行包](https://github.com/halo-dev/halo/releases)
- [配置文件](https://github.com/halo-dev/halo-common)
## 官方镜像源
- [https://download.halo.run](https://download.halo.run)
此镜像源由 [Nova Kwok](https://nova.moe/) 提供并维护。
## 三方镜像源
- [https://halo.cary.tech](https://halo.cary.tech)
此镜像源由 [新逸Cary](https://blog.xinac.cn) 提供并维护。

@ -0,0 +1,32 @@
---
title: 第一篇文章
description: 安装完成后,如何写下第一篇文章。
---
<!-- markdownlint-disable MD034 -->
<video src="https://assets.halo.run/first-post.mp4" controls style={{width: '100%', borderRadius: '8px'}}/>
## 登录管理端
浏览器访问 `/console` 即可进入 Halo 管理端。
## 新建文章
Halo 安装完成后,默认初始化了一篇 `Hello Halo` 文章,接下来我们将创建并发布一篇自己的文章。
1. 在 Halo 管理端,点击仪表盘页面中的 `创建文章` 快捷入口,即可进入到文章编辑页面。
2. 在文章编辑器中,你可以尽情写下你想展现的内容。
3. 当内容编辑完成后,点击右上角的 `发布` 按钮,给文章设置一个合适的标题和别名,同时可以设置文章所属分类、标签及其他一些高级设置。
4. 确认无误后,点击弹框下方的 `发布` 按钮,我们的第一篇文章就成功发布了。
:::info
关于文章相关其他功能及文章各种设置项的说明,请参考《[用户指南-文章](../user-guide/posts.md)》章节
:::
## 查看文章
文章发布成功后,就可以在主题端查看到我们刚刚创建的文章了。
浏览器访问域名进入站点首页,站点展示的内容及样式由当前启用的主题所决定。
接下来,选一款喜欢的主题,尽情体验 Halo 吧!

@ -0,0 +1,78 @@
---
title: 使用 1Panel 部署
description: 使用 1Panel 部署
---
import DockerArgs from "./slots/docker-args.md"
:::info
在继续操作之前,我们推荐您先阅读[《写在前面》](../prepare),这可以快速帮助你了解 Halo。
:::
## 1Panel 简介
[1Panel](https://1panel.cn) 是一个现代化、开源的 Linux 服务器运维管理面板。
![1Panel 截图](/img/install/1panel/1panel.png)
### 功能
- **快速建站**:深度集成 WordPress 和 Halo域名绑定、SSL 证书配置等一键搞定。
- **高效管理**:通过 Web 端轻松管理 Linux 服务器,包括应用管理、主机监控、文件管理、数据库管理、容器管理等。
- **安全可靠**:最小漏洞暴露面,提供防火墙和安全审计等功能。
- **一键备份**:支持一键备份和恢复,备份数据云端存储,永不丢失。
### 安装
关于 1Panel 的安装部署与基础功能介绍,请参考 [1Panel 官方文档](https://1panel.cn/docs/installation/online_installation/)。此处假设你已经完成了 1Panel 的安装部署,并对其功能有了基础了解。
### 安装基础软件
在安装 Halo 之前,我们需要先在 1Panel 上安装好所需的软件,包括 OpenResty 和 MySQL。在接下来的文档中我们会默认你已经安装好了这两个软件并不再赘述。
![OpenResty 和 MySQL](/img/install/1panel/openresty-mysql.png)
## 安装 Halo 应用
进入应用商店应用列表,选择其中的 Halo 应用进行安装。
![选择 Halo 应用](/img/install/1panel/app-store-halo.png)
在应用详情页选择最新的 Halo 版本进行安装。
![选择 Halo 版本](/img/install/1panel/install-halo.png)
参数说明:
- **名称**:要创建的 Halo 应用的名称。
- **数据库服务**Halo 应用使用的数据库应用支持下拉选择已安装的数据库应用1Panel 会自动配置 Halo 使用该数据库。
- **数据库名**Halo 应用使用的数据库名称1Panel 会在选中的数据库中自动创建这个数据库。
- **数据库用户**Halo 应用使用的数据库用户名1Panel 会在选中的数据库中自动创建这个用户,并添加对应的数据库授权。
- **数据库用户密码**Halo 应用使用的数据库用户密码1Panel 会在选中的数据库中自动为上一步创建的用户配置该密码。
- **超级管理员用户名**Halo 应用初始化创建的超级管理员用户名。
- **超级管理员密码**Halo 应用初始化创建的超级管理员密码。
- **外部访问地址**Halo 应用的最终访问地址,如果有为 Halo 规划域名,需要配置为域名格式,例如 `http://halo.example.com`。否则配置为 `http://服务器IP:PORT`,例如 `http://192.168.1.1:8090`
- **端口**Halo 应用的服务端口。
开始安装后页面自动跳转到已安装应用列表,等待刚刚安装的 Halo 应用变为已启动状态。
![Halo 运行状态](/img/install/1panel/halo-status.png)
此时便可以通过配置的外部访问地址来访问 Halo 了。
![访问 Halo](/img/install/1panel/halo-console.png)
## 创建网站
完成 Halo 应用的安装后,此时并不会自动创建一个网站,我们需要手动创建一个网站,然后将 Halo 应用绑定到这个网站上才能使用域名访问。
点击 1Panel 菜单的 **网站**,进入网站列表页,点击 **创建网站** 按钮。
![创建网站](/img/install/1panel/new-site.png)
1. 在已装应用中选择我们刚刚新建的 Halo 应用。
2. 正确填写主域名,需要注意的是需要提前解析好域名到服务器 IP。
最后,点击确认按钮,等待网站创建完成。
![网站列表](/img/install/1panel/site.png)

@ -0,0 +1,359 @@
---
title: 使用 Docker Compose 部署
description: 使用 Docker Compose 部署
---
import DockerArgs from "./slots/docker-args.md"
:::info
在继续操作之前,我们推荐您先阅读[《写在前面》](../prepare.md),这可以快速帮助你了解 Halo。
:::
## 环境搭建
- Docker 安装文档:<https://docs.docker.com/engine/install/>
- Docker Compose 安装文档:<https://docs.docker.com/compose/install/>
:::tip
我们推荐按照 Docker 官方文档安装 Docker 和 Docker Compose因为部分 Linux 发行版软件仓库中的 Docker 版本可能过旧。
:::
## 创建容器组
可用的 Halo 2.10 的 Docker 镜像:
- [halohub/halo](https://hub.docker.com/r/halohub/halo)
- [ghcr.io/halo-dev/halo](https://github.com/halo-dev/halo/pkgs/container/halo)
:::info 注意
目前 Halo 2 并未更新 Docker 的 latest 标签镜像,主要因为 Halo 2 不兼容 1.x 版本,防止使用者误操作。我们推荐使用固定版本的标签,比如 `halohub/halo:2.10` 或者 `halohub/halo:2.10.0`
- `halohub/halo:2.10`:表示最新的 2.10.x 版本,即每次发布 patch 版本都会同时更新 `halohub/halo:2.10` 镜像。
- `halohub/halo:2.10.0`:表示一个具体的版本。
后续文档以 `halohub/halo:2.10` 为例。
:::
1. 在系统任意位置创建一个文件夹,此文档以 `~/halo` 为例。
```bash
mkdir ~/halo && cd ~/halo
```
:::info
注意后续操作中Halo 产生的所有数据都会保存在这个目录,请妥善保存。
:::
2. 创建 `docker-compose.yaml`
此文档提供两种场景的 Docker Compose 配置文件,请根据你的需要**选择一种**。
:::info
需要注意的是,此文档为了更加方便的管理配置,所有与 Halo 相关的配置都使用 Docker 容器启动参数代替,所以无需创建 application.yaml 文件。
:::
1. 创建 Halo + PostgreSQL 的实例:
```yaml {24-30,47} title="~/halo/docker-compose.yaml"
version: "3"
services:
halo:
image: halohub/halo:2.10
container_name: halo
restart: on-failure:3
depends_on:
halodb:
condition: service_healthy
networks:
halo_network:
volumes:
- ./halo2:/root/.halo2
ports:
- "8090:8090"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8090/actuator/health/readiness"]
interval: 30s
timeout: 5s
retries: 5
start_period: 30s
command:
- --spring.r2dbc.url=r2dbc:pool:postgresql://halodb/halo
- --spring.r2dbc.username=halo
# PostgreSQL 的密码,请保证与下方 POSTGRES_PASSWORD 的变量值一致。
- --spring.r2dbc.password=openpostgresql
- --spring.sql.init.platform=postgresql
# 外部访问地址,请根据实际需要修改
- --halo.external-url=http://localhost:8090/
halodb:
image: postgres:15.4
container_name: halodb
restart: on-failure:3
networks:
halo_network:
volumes:
- ./db:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: [ "CMD", "pg_isready" ]
interval: 10s
timeout: 5s
retries: 5
environment:
- POSTGRES_PASSWORD=openpostgresql
- POSTGRES_USER=halo
- POSTGRES_DB=halo
- PGUSER=halo
networks:
halo_network:
```
2. 创建 Halo + MySQL 的实例:
```yaml {24-30,55} title="~/halo/docker-compose.yaml"
version: "3"
services:
halo:
image: halohub/halo:2.10
container_name: halo
restart: on-failure:3
depends_on:
halodb:
condition: service_healthy
networks:
halo_network:
volumes:
- ./halo2:/root/.halo2
ports:
- "8090:8090"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8090/actuator/health/readiness"]
interval: 30s
timeout: 5s
retries: 5
start_period: 30s
command:
- --spring.r2dbc.url=r2dbc:pool:mysql://halodb:3306/halo
- --spring.r2dbc.username=root
# MySQL 的密码,请保证与下方 MYSQL_ROOT_PASSWORD 的变量值一致。
- --spring.r2dbc.password=o#DwN&JSa56
- --spring.sql.init.platform=mysql
# 外部访问地址,请根据实际需要修改
- --halo.external-url=http://localhost:8090/
halodb:
image: mysql:8.1.0
container_name: halodb
restart: on-failure:3
networks:
halo_network:
command:
- --default-authentication-plugin=caching_sha2_password
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_general_ci
- --explicit_defaults_for_timestamp=true
volumes:
- ./mysql:/var/lib/mysql
- ./mysqlBackup:/data/mysqlBackup
ports:
- "3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
interval: 3s
retries: 5
start_period: 30s
environment:
# 请修改此密码,并对应修改上方 Halo 服务的 SPRING_R2DBC_PASSWORD 变量值
- MYSQL_ROOT_PASSWORD=o#DwN&JSa56
- MYSQL_DATABASE=halo
networks:
halo_network:
```
3. 仅创建 Halo 实例(使用默认的 H2 数据库,**不推荐用于生产环境,建议体验和测试的时候使用**
```yaml {19-24} title="~/halo/docker-compose.yaml"
version: "3"
services:
halo:
image: halohub/halo:2.10
container_name: halo
restart: on-failure:3
volumes:
- ./halo2:/root/.halo2
ports:
- "8090:8090"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8090/actuator/health/readiness"]
interval: 30s
timeout: 5s
retries: 5
start_period: 30s
command:
# 外部访问地址,请根据实际需要修改
- --halo.external-url=http://localhost:8090/
```
4. 仅创建 Halo 实例使用已有外部数据库MySQL 为例):
```yaml {8,12-20} title="~/halo/docker-compose.yaml"
version: "3"
services:
halo:
image: halohub/halo:2.10
container_name: halo
restart: on-failure:3
network_mode: "host"
volumes:
- ./halo2:/root/.halo2
command:
# 修改为自己已有的 MySQL 配置
- --spring.r2dbc.url=r2dbc:pool:mysql://localhost:3306/halo
- --spring.r2dbc.username=root
- --spring.r2dbc.password=
- --spring.sql.init.platform=mysql
# 外部访问地址,请根据实际需要修改
- --halo.external-url=http://localhost:8090/
# 端口号 默认8090
- --server.port=8090
```
参数详解:
<DockerArgs />
3. 启动 Halo 服务
```bash
docker-compose up -d
```
实时查看日志:
```bash
docker-compose logs -f
```
4. 用浏览器访问 /console 即可进入 Halo 管理页面,首次启动会进入初始化页面。
:::tip
如果需要配置域名访问,建议先配置好反向代理以及域名解析再进行初始化。如果通过 `http://ip:端口号` 的形式无法访问,请到服务器厂商后台将运行的端口号添加到安全组,如果服务器使用了 Linux 面板,请检查此 Linux 面板是否有还有安全组配置,需要同样将端口号添加到安全组。
:::
## 更新容器组
1. 停止运行中的容器组
```bash
cd ~/halo && docker-compose down
```
2. 备份数据(重要)
```bash
cp -r ~/halo ~/halo.archive
```
> 需要注意的是,`halo.archive` 文件名不一定要根据此文档命名,这里仅仅是个示例。
3. 更新 Halo 服务
修改 `docker-compose.yaml` 中配置的镜像版本。
```yaml {3}
services:
halo:
image: halohub/halo:2.10
container_name: halo
```
```bash
docker-compose pull halo
```
```bash
docker-compose up -d
```
## 反向代理
你可以在下面的反向代理软件中任选一项,我们假设你已经安装好了其中一项,并对其的基本操作有一定了解。 如果你对它们没有任何了解,可以参考我们更为详细的反向代理文档:
1. 使用 [OneinStack](../install/other/oneinstack.md)
2. 使用 [Nginx Proxy Manager](../install/other/nginxproxymanager.md)
3. 使用 [Traefik](../install/other/traefik.md)
### Nginx
```nginx {2,7,10}
upstream halo {
server 127.0.0.1:8090;
}
server {
listen 80;
listen [::]:80;
server_name www.yourdomain.com;
client_max_body_size 1024m;
location / {
proxy_pass http://halo;
proxy_set_header HOST $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
```
### Caddy 2
```txt {1,5}
www.yourdomain.com
encode gzip
reverse_proxy 127.0.0.1:8090
```
### Traefik
更新 halo 容器组的配置
1. `networks` 中引入已存在的网络 `traefik`(此网络需要 [提前创建](../install/other/traefik.md#创建-traefik)
2. `services.halo.networks` 中添加网络 `traefik`
3. 修改外部地址为你的域名
4. 声明路由规则、开启 TLS
```yaml {4-5,16,20,25-31}
version: "3.8"
networks:
traefik:
external: true
halo:
services:
halo:
image: halohub/halo:2.10
container_name: halo
restart: on-failure:3
volumes:
- ./halo2:/root/.halo2
networks:
- traefik
- halo
command:
# 外部访问地址,请根据实际需要修改
- --halo.external-url=https://yourdomain.com
labels:
traefik.enable: "true"
traefik.docker.network: traefik
traefik.http.routers.halo.rule: Host(`yourdomain.com`)
traefik.http.routers.halo.tls: "true"
traefik.http.routers.halo.tls.certresolver: myresolver
traefik.http.services.halo.loadbalancer.server.port: 8090
```

@ -0,0 +1,97 @@
---
title: 使用 Docker 部署
description: 使用 Docker 部署
---
import DockerArgs from "./slots/docker-args.md"
:::info
在继续操作之前,我们推荐您先阅读[《写在前面》](../prepare),这可以快速帮助你了解 Halo。
:::
:::tip
此文档仅提供使用默认 H2 数据库的 Docker 运行方式,主要用于体验和测试,在生产环境我们不推荐使用 H2 数据库。
如果需要使用其他数据库部署,我们推荐使用 Docker Compose 部署:[使用 Docker Compose 部署](./docker-compose)
:::
## 环境搭建
- Docker 安装文档:<https://docs.docker.com/engine/install/>
:::tip
我们推荐按照 Docker 官方文档安装 Docker因为部分 Linux 发行版软件仓库中的 Docker 版本可能过旧。
:::
## 使用 Docker 镜像
可用的 Halo 2.10 的 Docker 镜像:
- [halohub/halo](https://hub.docker.com/r/halohub/halo)
- [ghcr.io/halo-dev/halo](https://github.com/halo-dev/halo/pkgs/container/halo)
:::info 注意
目前 Halo 2 并未更新 Docker 的 latest 标签镜像,主要因为 Halo 2 不兼容 1.x 版本,防止使用者误操作。我们推荐使用固定版本的标签,比如 `halohub/halo:2.10` 或者 `halohub/halo:2.10.0`
- `halohub/halo:2.10`:表示最新的 2.10.x 版本,即每次发布 patch 版本都会同时更新 `halohub/halo:2.10` 镜像。
- `halohub/halo:2.10.0`:表示一个具体的版本。
后续文档以 `halohub/halo:2.10` 为例。
:::
1. 创建容器
```bash
docker run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.10
```
:::info
注意:此命令默认使用自带的 H2 Database 数据库。如需使用 PostgreSQL请参考[使用 Docker Compose 部署](./docker-compose)
:::
- **-it**:开启输入功能并连接伪终端
- **-d**:后台运行容器
- **--name**:为容器指定一个名称
- **-p**:端口映射,格式为 `主机(宿主)端口:容器端口` ,可在 `application.yaml` 配置。
- **-v**:工作目录映射。形式为:`-v 宿主机路径:/root/.halo2`,后者不能修改。
变量详解:
<DockerArgs />
1. 用浏览器访问 `/console` 即可进入 Halo 管理页面,首次启动会进入初始化页面。
:::tip
如果需要配置域名访问,建议先配置好反向代理以及域名解析再进行初始化。如果通过 `http://ip:端口号` 的形式无法访问,请到服务器厂商后台将运行的端口号添加到安全组,如果服务器使用了 Linux 面板,请检查此 Linux 面板是否有还有安全组配置,需要同样将端口号添加到安全组。
:::
## 升级版本
1. 拉取新版本镜像
```bash
docker pull halohub/halo:2.10
```
2. 停止运行中的容器
```bash
docker stop halo
docker rm halo
```
3. 备份数据(重要)
```bash
cp -r ~/.halo2 ~/halo2.archive
```
> 需要注意的是,`halo2.archive` 文件名不一定要根据此文档命名,这里仅仅是个示例。
4. 更新 Halo
修改版本号后,按照最初安装的方式,重新创建容器即可。
```bash
docker run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.10
```

@ -0,0 +1,142 @@
---
title: 使用 Helm 部署
description: 使用 Helm Chart 在 Kubernetes 集群中部署
---
import DockerArgs from "./slots/docker-args.md"
:::info
在继续操作之前,我们推荐您先阅读[《写在前面》](../prepare.md),这可以快速帮助你了解 Halo。
:::
## 先决条件
1. 拥有可用的 1.19 或更高版本的 Kubernetes 集群
2. 安装有 Helm 客户端 3.2 或更高的版本
3. 指定 StorageClass 自动创建存储卷时需要底层存储驱动支持
4. 用户对 Kubernetes 及 Helm 相关概念及如何使用有基本了解
## 快速部署
通过以下命令可以使用默认安装参数快速部署 Halo。默认参数下该 Chart 会自动部署 PostgreSQL 数据库给 Halo 使用,同时 Halo 工作目录及 PostgreSQL 数据目录通过指定 StorageClass 的方式自动创建存储卷进行持久化。
```bash
# 添加 Halo 项目的 Helm Charts 仓库
helm repo add halo https://halo-sigs.github.io/charts/
# 从 chart 仓库中更新本地可用chart的信息
helm repo update
# 使用默认参数,在当前的 Kubernetes namespace 中安装 Halo
helm install halo halo/halo
```
命令执行成功后会返回类似下文中的提示,通过提示中的命令可以获取到 NodePort 方式的 Halo 访问地址及默认的控制台管理员用户名和密码。
```text
NAME: halo
LAST DEPLOYED: Sun Jun 25 15:49:53 2023
NAMESPACE: halo
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
CHART NAME: halo
CHART VERSION: 1.1.0
APP VERSION: 2.6.1
** Please be patient while the chart is being deployed **
Your Halo site can be accessed through the following DNS name from within your cluster:
halo.halo.svc.cluster.local (port 80)
To access your Halo site from outside the cluster follow the steps below:
1. Get the Halo URL by running these commands:
export NODE_PORT=$(kubectl get --namespace halo -o jsonpath="{.spec.ports[0].nodePort}" services halo)
export NODE_IP=$(kubectl get nodes --namespace halo -o jsonpath="{.items[0].status.addresses[0].address}")
echo "Halo URL: http://$NODE_IP:$NODE_PORT/"
echo "Halo Console URL: http://$NODE_IP:$NODE_PORT/console"
2. Open a browser and access Halo using the obtained URL.
3. Login with the following credentials below to see your site:
echo Username: admin
echo Password: $(kubectl get secret --namespace halo halo -o jsonpath="{.data.halo-password}" | base64 -d)
```
:::info 参数说明
- 使用 Halo Helm Chart 仓库中 [values.yaml](https://github.com/halo-sigs/charts/blob/main/charts/halo/values.yaml) 文件中的默认参数进行安装;
- 关于 PostgreSQL 数据库的更多参数说明,请参考 [Bitnami PostgreSQL Chart](https://github.com/bitnami/charts/tree/main/bitnami/postgresql#parameters),在原有参数格式上增加 `postgresql.` 前缀即可。
:::
## 卸载
使用命令 `helm uninstall halo` 可卸载已安装的 Halo 应用,其中 `halo` 为安装时指定的 Halo 应用名称。
:::warning
卸载应用前请确认数据库及 Halo 工作空间中的文件已进行备份或不再需要。
:::
## 使用 MySQL 数据库
当用户希望使用 MySQL 数据库而非默认的 PostgreSQL 数据库时,可以参考以下命令进行部署。
```bash
helm install halo halo/halo --set mysql.enabled=true --set postgresql.enabled=false
```
:::info 参数说明
- `mysql.enabled=true` 自动安装 MySQL 数据库;
- `postgresql.enabled=false` 不自动安装 PostgreSQL 数据库;
- 关于 mysql 的更多参数说明,请参考 [Bitnami MySQL Chart](https://github.com/bitnami/charts/tree/main/bitnami/mysql#parameters),在原有参数格式上增加 `mysql.` 前缀即可。
:::
## 使用已有的数据库
当用户希望使用已有的数据库而非自动安装新数据库时,可以参考以下命令进行部署。
```bash
helm install halo halo/halo \
--set mysql.enabled=false \
--set postgresql.enabled=false \
--set externalDatabase.platform=mysql \
--set externalDatabase.host=mysql \
--set externalDatabase.port=3306 \
--set externalDatabase.user=halo \
--set externalDatabase.password=0P0gJrCyzz \
--set externalDatabase.database=halo
```
:::info 参数说明
- `mysql.enabled=true` 不自动安装 MySQL 数据库;
- `postgresql.enabled=false` 不自动安装 PostgreSQL 数据库;
- `externalDatabase.platform=mysql`:外部数据库类型,例如 `postgresql`、`mysql`
- `externalDatabase.host=mysql`:外部数据库连接地址;
- `externalDatabase.port=3306`:外部数据库连接端口;
- `externalDatabase.user=halo`:外部数据库用户名;
- `externalDatabase.password=0P0gJrCyzz`:外部数据库密码;
- `externalDatabase.database=halo`:外部数据库库名;
:::
## 创建 Ingress
当用户希望通过 Ingress 将 Halo 应用暴露到 Kubernetes 集群外进行访问时,可以参考以下安装参数。
```bash
helm install halo halo/halo --set ingress.enabled=true --set ingress.hostname=demo.halo.run
```
对于已有的 Halo 应用,可以通过如下命令更新应用参数,为 Halo 应用创建 Ingress。
```bash
helm upgrade halo halo/halo --set ingress.enabled=true --set ingress.hostname=demo.halo.run
```
## 其他参数
完整参数列表请参考 Halo 项目的 [Helm Chart 仓库](https://github.com/halo-sigs/charts#parameters)。

@ -0,0 +1,164 @@
---
title: 与 Nginx Proxy Manager 配合使用
description: 使用 Nginx Proxy Manager 管理 Halo 服务的反向代理
---
## Halo 部署
参见 [使用 Docker Compose 部署](../docker-compose)
:::info
`「反向代理」` 部分不进行操作,保证 Halo 服务运行无误即可。
:::
## 简介
顾名思义Nginx Proxy Manager 就是一个 Nginx 的代理管理器,它最大的特点是简单方便。
即使是没有 Nginx 基础的小伙伴,也能轻松地用它来完成反向代理的操作,而且因为自带面板,操作极其简单,非常适合配合 docker 搭建的应用使用。
Nginx Proxy Manager 后台还可以一键申请 SSL 证书,并且会自动续期,方便省心。
下面我们就来介绍如何用 Nginx Proxy Manger 来配合 Halo实现反向代理和 HTTPS 访问。
## 安装 Nginx Proxy Manager
> 说明:默认你的服务器已经安装了 Docker 和 Docker Compose如果你没有安装可以参考[使用 Docker Compose 部署](../docker-compose) 的环境搭建部分来进行安装。
点击下方链接进入 Nginx Proxy Manager以下简称 NPM 官网:<https://nginxproxymanager.com/>
我们可以直接选择 [快速安装](https://nginxproxymanager.com/guide/#quick-setup)。
首先,我们创建一个文件夹来存放 NPM 的 `docker-compose.yml` 文件:
```bash
mkdir -p ~/data/docker_data/nginxproxymanager # 创建一个 npm 的文件夹
cd ~/data/docker_data/nginxproxymanager # 进入该文件夹
vi docker-compose.yml
```
在英文状态的输入法下,按下 `i`,左下角出现 `--INSERT--` 后,粘贴填入下面的内容:
```yaml
version: '3'
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
- '80:80' # 不建议修改端口
- '81:81' # 可以把冒号左边的 81 端口修改成你服务器上没有被占用的端口
- '443:443' # 不建议修改端口
volumes:
- ./data:/data # 点号表示当前文件夹,冒号左边的意思是在当前文件夹下创建一个 data 目录,用于存放数据,如果不存在的话,会自动创建
- ./letsencrypt:/etc/letsencrypt # 点号表示当前文件夹,冒号左边的意思是在当前文件夹下创建一个 letsencrypt 目录,用于存放证书,如果不存在的话,会自动创建
```
> 注意:安装了 NPM 之后,就不需要再安装 Nginx 了,否则会端口冲突(不建议修改 NPM 的 80、443 端口)。如果你的服务器安装了宝塔面板,也可以和 NPM 一起使用,只要你到软件后台把宝塔安装的 Nginx 关闭或者卸载即可。
之后,同样在英文输入法下,按一下 `esc`,然后 `:wq` 保存退出。
启动 NPM
```bash
docker-compose up -d # -d 表示后台运行
docker compose up -d # 如果你用的是 docker-compose-plugin 的话,用这条命令
```
不出意外,此时你使用 [http://127.0.0.1:81](http://127.0.0.1:81/) 就可以访问 NPM 的网页端了。(注意把 `127.0.0.1` 替换成你实际服务器的 IP
:::info
1. 不知道服务器 IP可以直接在命令行输入curl ip.sb会显示当前服务器的 IP。
2. 遇到访问不了的情况,请再次检查在宝塔面板的防火墙和服务商的后台防火墙是否打开对应了端口。
:::
默认登陆的用户名:`admin@example.com` 密码:`changeme`
第一次登陆会提示更改用户名和密码,建议修改一个复杂一点的密码。
至此,我们已经完成了 Nginx Proxy Manager 的搭建,之后就可以用它给我们的 Halo 或者其他 Web 应用做反向代理了。
## 配置 Halo 的反向代理
首先我们登陆网页端之后,会弹出修改用户名和密码的对话框,我们根据自己的实际来修改自己的用户名和邮箱。
![Nginx Proxy Manager 1](/img/nginx-proxy-manager/Nginx-Proxy-Manager-1.png)
保存之后,会让我们修改密码(建议用一个复杂的密码)。
![Nginx Proxy Manager 2](/img/nginx-proxy-manager/Nginx-Proxy-Manager-2.png)
接着我们就可以来给 Halo 来添加一个反向代理了。
点击 `Proxy Hosts`
![Nginx Proxy Manager 3](/img/nginx-proxy-manager/Nginx-Proxy-Manager-3.png)
接着点击 `Add Proxy Host`,弹出如下对话框:
![Nginx Proxy Manager 4](/img/nginx-proxy-manager/Nginx-Proxy-Manager-4.png)
看起来都是英文,很复杂,但是其实很简单,我们只要用到其中的几个功能即可,这边稍微解释一下:
- `Domain Names` :填我们 Halo 网站的域名,首先记得做好 DNS 解析,把域名绑定到我们的服务器的 IP 上
- `Scheme` :默认 `http` 即可,除非你有自签名证书
- `Forward Hostname/IP` :填入服务器的 IP或者 Docker 容器内部的 IP如果 NPM 和 Halo 搭建在同一台服务器上的话)
- `Forward Port`:填入 Halo 映射出的端口,这边默认是 `8090`
- `Cache Assets` :缓存,可以选择打开
- `Block Common Exploits` 阻止常见的漏洞,可以选择打开
- `Websockets Support` WS 支持,可以选择打开
- `Access List` 这个是 NPM 自带的一个限制访问功能,这边我们不管,后续可以自行研究。
以下是一个样列:
![Nginx Proxy Manager 5](/img/nginx-proxy-manager/Nginx-Proxy-Manager-5.png)
因为样例的 NPM 和 Halo 搭建在同一台 VPS 上,所以这边的 IP图中填的是 `172.17.0.1`,为 Docker 容器内部的 IP 地址,
可以通过下面的命令查询:
```bash
ip addr show docker0
```
```bash {3}
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:e4:a3:b5:b9 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
```
这边的 IP 是 `172.17.0.1`,填入这个 IP可以不用打开防火墙的 `8090` 端口。
当然,如果你的 NPM 和 Halo 不在同一台服务上,你需要在 IP 部分填入 **你的 Halo 所在的服务器的 IP**,并在服务商(部分服务商如腾讯、阿里)的后台打开 `8090` 端口。
## 一键申请 SSL 证书
接着我们来申请一张 SSL 证书,让我们的网站支持 `https` 访问。
![Nginx Proxy Manager 6](/img/nginx-proxy-manager/Nginx-Proxy-Manager-6.png)
![Nginx Proxy Manager 7](/img/nginx-proxy-manager/Nginx-Proxy-Manager-7.png)
如图所示,记得打开强制 SSL其他四个的功能请自行研究这边不多做讨论。
:::info
1. 申请证书需要你提前将域名解析到 NPM 所在的服务器的 IP 上;
2. 如果你使用的是国内的服务器,默认 `80``443` 端口是关闭的,你需要备案之后才能使用;
3. 如果你使用了 CloudFlare 的 DNS 服务,记得把小黄云关闭(即不开启 CDN
:::
不出意外,你将成功申请到 SSL 证书,证书会三个月自动续期。
再次点开配置,查看一下,将强制 SSL 打开。
![Nginx Proxy Manager 8](/img/nginx-proxy-manager/Nginx-Proxy-Manager-8.png)
至此,你已经成功完成了 Halo 的反向代理,快尝试使用域名访问一下看看吧!
> 同样的,举一反三,试试把你的 NPM 也用一个域名来反向代理一下吧。(小提示:你需要再解析一个域名(可以是二级域名)到 NPM 所在的服务器上,反代页面需要填的 IP 可以填 docker 容器内的 IP 也可以填服务器的 IP端口填 `81` 即可)

@ -0,0 +1,274 @@
---
title: 与 OneinStack 配合使用
description: 使用 OneinStack 管理 Halo 服务的反向代理
---
## Halo 部署
参见 [使用 Docker Compose 部署](../docker-compose.md)
:::info
`「反向代理」` 部分不进行操作,保证 Halo 服务运行无误即可。
:::
## 通过 OneinStack 安装 Nginx
点击下方链接进入 OneinStack 官网,仅选择 `安装 Nginx`,其他的都可以取消选择。
<https://oneinstack.com/auto>
最后点击 `复制安装命令` 到服务器执行即可。如果你仅安装 Nginx你的链接应该是这样
```bash
wget -c http://mirrors.linuxeye.com/oneinstack-full.tar.gz && tar xzf oneinstack-full.tar.gz && ./oneinstack/install.sh --nginx_option 1
```
:::info
这一步会经过编译安装,可能会导致安装时间很漫长,这主要取决于你服务器的性能。
:::
出现下面的信息即代表安装成功:
```bash
Nginx installed successfully!
Created symlink from /etc/systemd/system/multi-user.target.wants/nginx.service to /usr/lib/systemd/system/nginx.service.
Redirecting to /bin/systemctl start nginx.service
####################Congratulations########################
Total OneinStack Install Time: 5 minutes
Nginx install dir: /usr/local/nginx
```
## 创建 vhost
> 即创建一个站点,你可以通过这样的方式在你的服务器创建无限个站点。接下来的目的就是创建一个站点,并反向代理到 Halo。这一步在此教程使用 `demo.halo.run` 这个域名做演示,实际情况请修改此域名。
1. 进入到 oneinstack 目录,执行 vhost 创建命令
```bash
cd oneinstack
```
```bash
sh vhost.sh
```
2. 按照提示选择或输入相关信息
```bash
What Are You Doing?
1. Use HTTP Only
2. Use your own SSL Certificate and Key
3. Use Let's Encrypt to Create SSL Certificate and Key
q. Exit
Please input the correct option:
```
这一步是选择证书配置方式,如果你有自己的证书,输入 <kbd>2</kbd> 即可。如果需要使用 `Let's Encrypt` 申请证书,选择 <kbd>3</kbd> 即可。
```bash
Please input domain(example: www.example.com):
```
输入自己的域名即可,前提是已经提前解析好了域名。
```bash
Please input the directory for the domain:demo.halo.run :
(Default directory: /data/wwwroot/demo.halo.run):
```
提示输入站点根目录,因为我们是使用 Nginx 的反向代理,所以这个目录是没有必要配置的,我们直接使用默认的即可(直接回车)。
```bash
Do you want to add more domain name? [y/n]:
```
是否需要添加其他域名,按照需要选择即可,如果不需要,输入 <kbd>n</kbd> 并回车确认。
```bash
Do you want to add hotlink protection? [y/n]:
```
是否需要做防盗链处理,按照需要选择即可。
```bash
Allow Rewrite rule? [y/n]:
```
路径重写配置,我们不需要,选择 <kbd>n</kbd> 回车确定即可。
```bash
Allow Nginx/Tengine/OpenResty access_log? [y/n]:
```
Nginx 的请求日志,建议选择 <kbd>y</kbd>
这样就完成了 vhost 站点的创建,最终会输出站点的相关信息:
```bash
Your domain: demo.halo.run
Virtualhost conf: /usr/local/nginx/conf/vhost/demo.halo.run.conf
Directory of: /data/wwwroot/demo.halo.run
```
Nginx 的配置文件即 `/usr/local/nginx/conf/vhost/demo.halo.run.conf`
## 修改 Nginx 配置文件
上方创建 vhost 的过程并没有创建反向代理的配置,所以需要我们自己修改一下配置文件。
1. 使用你熟悉的工具打开配置文件,此教程使用 vim。
```bash
vim /usr/local/nginx/conf/vhost/demo.halo.run.conf
```
2. 删除一些不必要的配置
```nginx
location ~ [^/]\.php(/|$) {
#fastcgi_pass remote_php_ip:9000;
fastcgi_pass unix:/dev/shm/php-cgi.sock;
fastcgi_index index.php;
include fastcgi.conf;
}
```
此段配置是针对 php 应用的,所以可以删掉。
3. 添加 `upstream` 配置
`server` 的同级节点添加如下配置:
```nginx {2}
upstream halo {
server 127.0.0.1:8090;
}
```
4. 在 `server` 节点添加如下配置
```nginx {6}
location / {
proxy_set_header HOST $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://halo;
}
```
5. 修改 `location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|flv|mp4|ico)$` 节点
```nginx {2}
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|flv|mp4|ico)$ {
proxy_pass http://halo;
expires 30d;
access_log off;
}
```
6. 修改 `location ~ .*\.(js|css)?$` 节点
```nginx {2}
location ~ .*\.(js|css)?$ {
proxy_pass http://halo;
expires 7d;
access_log off;
}
```
如果不按照第 56 步操作,请求一些图片或者样式文件不会经过 Halo所以请不要忽略此配置。
7. 添加 acme.sh 续签验证路由
OneinStack 使用的 acme.sh 管理证书,如果你在创建 vhost 的时候选择了使用 `Let's Encrypt` 申请证书,那么 OneinStack 会在系统内添加一个定时任务去自动续签证书acme.sh 默认验证站点所有权的方式为在站点根目录生成一个文件(.well-known来做验证由于配置了反向代理所以在验证的时候是无法直接访问到站点目录下的 .well-known 文件夹下的验证文件的。需要添加如下配置:
```nginx {4}
location ^~ /.well-known/acme-challenge/ {
default_type "text/plain";
allow all;
root /data/wwwroot/demo.halo.run/;
}
```
至此,配置修改完毕,保存即可。最终你的配置文件可能如下面配置一样:
```nginx {2,20,29,34,41-47,51}
upstream halo {
server 127.0.0.1:8090;
}
server {
listen 80;
listen [::]:80;
listen 443 ssl http2;
listen [::]:443 ssl http2;
ssl_certificate /usr/local/nginx/conf/ssl/demo.halo.run.crt;
ssl_certificate_key /usr/local/nginx/conf/ssl/demo.halo.run.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-GCM-SHA256:TLS13-AES-128-CCM-8-SHA256:TLS13-AES-128-CCM-SHA256:EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_timeout 10m;
ssl_session_cache builtin:1000 shared:SSL:10m;
ssl_buffer_size 1400;
add_header Strict-Transport-Security max-age=15768000;
ssl_stapling on;
ssl_stapling_verify on;
server_name demo.halo.run;
access_log /data/wwwlogs/demo.halo.run_nginx.log combined;
index index.html index.htm index.php;
root /data/wwwroot/demo.halo.run;
if ($ssl_protocol = "") { return 301 https://$host$request_uri; }
include /usr/local/nginx/conf/rewrite/none.conf;
#error_page 404 /404.html;
#error_page 502 /502.html;
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|flv|mp4|ico)$ {
proxy_pass http://halo;
expires 30d;
access_log off;
}
location ~ .*\.(js|css)?$ {
proxy_pass http://halo;
expires 7d;
access_log off;
}
location ~ /(\.user\.ini|\.ht|\.git|\.svn|\.project|LICENSE|README\.md) {
deny all;
}
location / {
proxy_set_header HOST $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://halo;
}
location ^~ /.well-known/acme-challenge/ {
default_type "text/plain";
allow all;
root /data/wwwroot/demo.halo.run/;
}
}
```
## 重载 Nginx 使配置生效
验证 nginx 配置
```bash
nginx -t
```
如果输出如下提示则代表配置有效:
```bash
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful
```
重载 Nginx 配置:
```bash
nginx -s reload
```
至此,整个教程完毕,现在你可以访问域名检查是否已经配置成功。

@ -0,0 +1,117 @@
---
title: 与 Traefik 配合使用
description: 使用 Traefik 管理 Halo 服务的反向代理
---
## Halo 部署
参见 [使用 Docker Compose 部署](../docker-compose.md)
:::info
`「反向代理」` 部分不进行操作,保证 Halo 服务运行无误即可。
:::
## 简介
[Traefik](https://traefik.io/traefik/) 是一款开源的反向代理与负载均衡工具,它监听后端的变化并自动更新服务配置。
它与传统反向代理最大的区别,是支持声明式的动态路由规则,大大简化网关规则的配置。而且还有诸多实用特性,例如:健康检查、多实例负载均衡、能够实现 Let's Encrypt 证书的自动签发、验证与续期等等。
## 创建 Traefik
下面的配置中,创建了 Traefik 实例。并且做了基础的几项配置:
1. 监听了宿主机的 80、443 端口,并自动将 80 端口的请求重定向到 443 端口。[文档](https://doc.traefik.io/traefik/routing/entrypoints/)
2. 开启 Docker 服务发现,监听检测 Docker 容器声明的服务关系。[文档](https://doc.traefik.io/traefik/providers/docker/#provider-configuration)
3. 开启 Traefik Dashboard建议使用二级域名的形式示例`traefik.yourdomain.com`)。[文档](https://doc.traefik.io/traefik/operations/dashboard/#dashboard-router-rule)
4. 开启证书自动生成,通过 ACME 自动管理 TLS 证书的申请、校验与续期。[文档](https://doc.traefik.io/traefik/https/acme/)
:::caution
ACME 证书 (`/acme.json`) 一定要 [持久化](https://doc.traefik.io/traefik/https/acme/#storage),否则每次重启 Traefik 服务,都会去申请签发证书。可能会触发 Let's
Encrypt 的 [速率限制](https://letsencrypt.org/zh-cn/docs/rate-limits/),导致签名的域名一段时间内无法签发新的证书。
:::
```yaml {19,31,35,41} showLineNumbers
version: "3.8"
networks:
traefik:
name: traefik
attachable: true
services:
traefik:
image: traefik:v2.9
container_name: traefik
networks:
- traefik
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./acme.json:/acme.json
command: >
--api.dashboard=true
--entrypoints.web.address=:80
--entrypoints.websecure.address=:443
--entrypoints.web.http.redirections.entrypoint.to=websecure
--entrypoints.web.http.redirections.entrypoint.scheme=https
--providers.docker=true
--providers.docker.endpoint=unix:///var/run/docker.sock
--providers.docker.watch=true
--providers.docker.exposedByDefault=false
--certificatesResolvers.myresolver.acme.httpChallenge.entryPoint=web
--certificatesresolvers.myresolver.acme.email=your-mail@mail.com
labels:
traefik.enable: "true"
traefik.docker.network: traefik
traefik.http.routers.dashboard.rule: Host(`traefik.yourdomain.com`)
traefik.http.routers.dashboard.tls: "true"
traefik.http.routers.dashboard.tls.certresolver: myresolver
traefik.http.routers.dashboard.service: "api@internal"
traefik.http.routers.dashboard.middlewares: auth
# 账号密码 admin/P@88w0rd 生成 echo $(htpasswd -nb user password) | sed -e s/\\$/\\$\\$/g
traefik.http.middlewares.auth.basicauth.users: "admin:$$apr1$$q8q0qpzT$$lvzMP7VYd9EUcG/wkIsAN."
```
## 配置 Halo 的反向代理
这里以最简配置h2 数据库Halo 服务的 Docker 配置举例。只需做以下调整:
1. 顶层 `networks` 中添加了外部网络 `traefik`
2. `services.halo.networks` 中添加了 `traefik` 网络
3. `services.halo.labels` 中声明了 Traefik 配置
1. 路由规则为 `yourdomain.com`
2. 开启 TLS
3. 指定了服务端口为 8090
```yaml {4-5,16,20,25-31} showLineNumbers
version: "3.8"
networks:
traefik:
external: true
halo:
services:
halo:
image: halohub/halo:2.10
container_name: halo
restart: on-failure:3
volumes:
- ./halo2:/root/.halo2
networks:
- traefik
- halo
command:
# 外部访问地址,请根据实际需要修改
- --halo.external-url=https://yourdomain.com
labels:
traefik.enable: "true"
traefik.docker.network: traefik
traefik.http.routers.halo.rule: Host(`yourdomain.com`)
traefik.http.routers.halo.tls: "true"
traefik.http.routers.halo.tls.certresolver: myresolver
traefik.http.services.halo.loadbalancer.server.port: 8090
```

@ -0,0 +1,243 @@
---
title: 使用 Podman 部署
description: 使用 Podman 部署
---
import DockerArgs from "./slots/docker-args.md"
## 前言
:::info
在继续操作之前,我们推荐您先阅读[《写在前面》](../prepare),这可以快速帮助你了解 Halo。
:::
:::info
什么是 Podman ?
Podman全称 POD 管理器)是一款用于在 Linux® 系统上开发、管理和运行容器的开源工具。Podman 最初由红帽® 工程师联合开源社区一同开发,它可利用 lipod 库来管理整个容器生态系统。
Podman 采用无守护进程的包容性架构,因此可以更安全、更简单地进行容器管理,再加上 Buildah 和 Skopeo 等与之配套的工具和功能,开发人员能够按照自身需求来量身定制容器环境。
为什么选择 Podman 而不是 Docker ?
这个需要视情况而定, 如果您的主机配置不是很高, 您使用 Docker 时, 因为 Docker 自带的守护进程可能会雪上加霜, 它会大量占用您的资源。
而 Podman 采用无守护进程架构,而且容器是无根模式,您可以在占用资源极小的情况下运行镜像,并且获得很高的安全性。
而且 Podman 与 Docker 高度兼容,您不需要做特别配置即可将 Docker 容器运行在Podman 上。
[什么是 Podman?](https://www.redhat.com/zh/topics/containers/what-is-podman)
[Podman ArchWiki](https://wiki.archlinux.org/title/Podman)
[Podman 官网](https://podman.io/)
:::
:::tip
此文档提供使用默认 H2 数据库和 Postgresql 数据库的 Podman 运行示例,在生产环境我们不推荐使用 H2 数据库。
:::
## 环境搭建
- Podman 安装文档:<https://podman.io/docs/installation>
:::tip
我们推荐您先阅读 Podman 官方文档对 Podman 有了相关了解后再考虑通过Linux包管理系统安装 Podman 或者使用文档中指定的方式安装 。
:::
## 使用 Docker 镜像
:::tip
为什么是 Docker 镜像?
通过[前言](#前言)我们已经了解了 Podman ,其中提到 ***Podman 与 Docker 高度兼容*** ,正是因为 Podman 完全是为了替代 Docker 而诞生,所以原本的 Docker 生态中的镜像我们可以无需更改直接使用。
:::
可用的 Halo 2.10 的 Docker 镜像:
- [halohub/halo](https://hub.docker.com/r/halohub/halo)
- [ghcr.io/halo-dev/halo](https://github.com/halo-dev/halo/pkgs/container/halo)
:::info 注意
目前 Halo 2 并未更新 Docker 的 latest 标签镜像,主要因为 Halo 2 不兼容 1.x 版本,防止使用者误操作。我们推荐使用固定版本的标签,比如 `halohub/halo:2.10` 或者 `halohub/halo:2.10.0`
- `halohub/halo:2.10`:表示最新的 2.10.x 版本,即每次发布 patch 版本都会同时更新 `halohub/halo:2.10` 镜像。
- `halohub/halo:2.10.0`:表示一个具体的版本。
后续文档以 `halohub/halo:2.10` 为例。
:::
1. 创建容器
```bash
mkdir -p ~/.halo2
podman run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.10
```
:::info
注意:此命令默认使用自带的 H2 Database 数据库。如需使用 PostgreSQL请参考[使用 Docker Compose 部署](./docker-compose)
:::
- **-it**:开启输入功能并连接伪终端
- **-d**:后台运行容器
- **--name**:为容器指定一个名称
- **-p**:端口映射,格式为 `主机(宿主)端口:容器端口` ,可在 `application.yaml` 配置。
- **-v**:工作目录映射。形式为:`-v 宿主机路径:/root/.halo2`,后者不能修改。
变量详解:
<DockerArgs />
1. 用浏览器访问 `/console` 即可进入 Halo 管理页面,首次启动会进入初始化页面。
:::tip
如果需要配置域名访问,建议先配置好反向代理以及域名解析再进行初始化。如果通过 `http://ip:端口号` 的形式无法访问,请到服务器厂商后台将运行的端口号添加到安全组,如果服务器使用了 Linux 面板,请检查此 Linux 面板是否有还有安全组配置,需要同样将端口号添加到安全组。
:::
## 升级版本
1. 拉取新版本镜像
```bash
podman pull halohub/halo:2.10
```
2. 停止运行中的容器
```bash
podman stop halo
podman rm halo
```
3. 备份数据(重要)
```bash
cp -r ~/.halo2 ~/halo2.archive
```
> 需要注意的是,`halo2.archive` 文件名不一定要根据此文档命名,这里仅仅是个示例。
4. 更新 Halo
修改版本号后,按照最初安装的方式,重新创建容器即可。
```bash
mkdir -p ~/.halo2
podman run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.10
```
## 使用 [Podman Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html)
:::tip
Podman 没有和 Docker 类似的管理进程,在低配置的主机上更友好。
但是使用 Podman 想要开机后自动启动,官方推荐一种和 systemd 服务类似的语法文件,即 Podman Quadlet。
:::
下面是一个使用 Podstgresql 数据库的示例:
```bash
mkdir -p /opt/podman-data/halo
mkdir -p /etc/containers/systemd
vim /etc/containers/systemd/halo.container
```
```conf
[Unit]
Description=The halo container
Wants=network-online.target
After=network-online.target
[Container]
AutoUpdate=registry
ContainerName=halo
User=60000
Group=60000
UserNS=keep-id:uid=60000,gid=60000
Environment=JVM_OPTS="-Xmx512m -Xms256m"
Environment=HALO_WORK_DIR="/.halo"
Environment=SPRING_CONFIG_LOCATION="optional:classpath:/;optional:file:/.halo/"
Environment=TZ=Asia/Shanghai
Volume=/opt/podman-data/halo:/.halo
PublishPort=127.0.0.1:8090:8090
Image=ghcr.io/halo-dev/halo:2.10
Exec=--halo.external-url=https://localhost:8090 --spring.sql.init.platform=postgresql --spring.r2dbc.url=r2dbc:pool:postgresql://127.0.0.1:5432/my-db --spring.r2dbc.username=my-user --spring.r2dbc.password=my-password --halo.cache.page.disabled=false
[Service]
Restart=always
RestartSec=30s
StartLimitInterval=30
TimeoutStartSec=900
TimeoutStopSec=70
[Install]
WantedBy=multi-user.target default.target
```
```bash
systemctl daemon-reload
systemctl start halo
# 只需要systemctl start halo.
# 之后重启会自动启动不需要enable服务.
```
Podman Quadlet 解析:
`[Unit]` 部分:
- Wants和After字段指定了Halo在什么服务后启动。
`[Container]` 部分:
- `AutoUpdate=registry`指定了自动拉取容器。假设后续Halo镜像支持了`latest`标签,你需要`systemctl enable --now podman-auto-update.timer`以启用容器自动更新。本文示例`ghcr.io/halo-dev/halo:2.10`,将会自动更新适用与`2.10`版本的patch例如您创建容器时是`2.10.1`,在官方发布`2.10.2`版本时,容器会自动更新到`2.10.2`。
- `ContainerName=`指定了 systemd 将生成的服务名称。
- `User=60000 Group=60000 UserNS=keep-id:uid=60000,gid=60000` 限制容器以 id 60000 的用户运行提高安全性。注意这个id 60000请根据你实际想要运行的用户名来修改可通过`id user`获得你的用户的id.
- `Environment=`字段指定了容器的环境变量,其中你需要注意的是`Environment=HALO_WORK_DIR="/.halo"` `Environment=SPRING_CONFIG_LOCATION="optional:classpath:/;optional:file:/.halo/"`这两个变量中的`/.halo`路径。
- `Volume=`字段指定挂载到容器储存Halo配置文件的路径请仔细观察`/opt/podman-data/halo:/.halo`其中的`/.halo`要与上面需要注意的环境变量路径要一致。
- `PublishPort=`和docker -p命令一致即需要映射的端口。
- `Image=ghcr.io/halo-dev/halo` 即Docker镜像的地址注意要完整的。比如`ghcr.io`这个路径就不能少,如果你没有配置 Podman 的 registries 文件,此路径就必不可少,建议写全。
- `Exec=` 即附加到Halo容器的 Command ,具体变量参考上方的 DockerArgs 。多个变量以空格分开。
`[Service]` 部分:
即原生systemd语法
- `Restart` 指定遇到错误后总是重启容器
- `RestartSec` 重启的间隔时间
- `StartLimitInterval` 重启的次数,超过这个次数将不再重启。
- `TimeoutStartSec` 启动容器的超时时间,建议不要修改,因为每次开机后 Podman 将自动拉取容器,这时也许耗时会很长,这些时间是算在启动时间中的。如果定义太小的时间,可能将导致 Podman 无法拉取容器镜像。
- `TimeoutStopSec` 停止容器时的超时时间,`systemctl stop halo` 假设使用这个命令,如果停止时间超过了`TimeoutStopSec`定义的时间将会被系统Kill.
`[Install]` 部分:
此部分请查看systemd文档不建议修改。
使用默认的 root 用户运行时无需定义 `User=60000 Group=60000 UserNS=keep-id:uid=60000,gid=60000``Environment=HALO_WORK_DIR="/.halo"` `Environment=SPRING_CONFIG_LOCATION="optional:classpath:/;optional:file:/.halo/"`
示例:
```bash
mkdir -p /opt/podman-data/halo
mkdir -p /etc/containers/systemd
vim /etc/containers/systemd/halo.container
```
```conf
# /etc/containers/systemd/halo.container
[Unit]
Description=The halo container
Wants=network-online.target
After=network-online.target
[Container]
AutoUpdate=registry
ContainerName=halo
Volume=/opt/podman-data/halo:/root/.halo
PublishPort=127.0.0.1:8090:8090
Image=ghcr.io/halo-dev/halo:2.10
Exec=--halo.external-url=https://localhost:8090 --spring.sql.init.platform=postgresql --spring.r2dbc.url=r2dbc:pool:postgresql://127.0.0.1:5432/my-db --spring.r2dbc.username=my-user --spring.r2dbc.password=my-password --halo.cache.page.disabled=false
[Service]
Restart=always
RestartSec=30s
StartLimitInterval=30
TimeoutStartSec=900
TimeoutStopSec=70
[Install]
WantedBy=multi-user.target default.target
```

@ -0,0 +1,17 @@
| 参数名 | 描述 |
| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `spring.r2dbc.url` | 数据库连接地址,详细可查阅下方的 `数据库配置` |
| `spring.r2dbc.username` | 数据库用户名 |
| `spring.r2dbc.password` | 数据库密码 |
| `spring.sql.init.platform` | 数据库平台名称,支持 `postgresql`、`mysql`、`h2` |
| `halo.external-url` | 外部访问链接,如果需要在公网访问,需要配置为实际访问地址 |
| `halo.cache.page.disabled` | 是否禁用页面缓存,默认为禁用,如需页面缓存可以手动添加此配置,并设置为 `false`<br />开启缓存之后,在登录的情况下不会经过缓存,且默认一个小时会清理掉不活跃的缓存,也可以在 Console 仪表盘的快捷访问中手动清理缓存。 |
数据库配置:
| 链接方式 | 链接地址格式 | `spring.sql.init.platform` |
| ----------- | ---------------------------------------------------------------------------------- | -------------------------- |
| PostgreSQL | `r2dbc:pool:postgresql://{HOST}:{PORT}/{DATABASE}` | postgresql |
| MySQL | `r2dbc:pool:mysql://{HOST}:{PORT}/{DATABASE}` | mysql |
| MariaDB | `r2dbc:pool:mariadb://{HOST}:{PORT}/{DATABASE}` | mysql |
| H2 Database | `r2dbc:h2:file:///${halo.work-dir}/db/halo-next?MODE=MySQL&DB_CLOSE_ON_EXIT=FALSE` | h2 |

@ -0,0 +1,80 @@
---
title: 从 Halo 1.x 迁移
description: 从 Halo 1.x 迁移的完整指南和注意事项
---
因为 Halo 2.0 的底层架构变动,无法兼容 1.x 的数据,导致无法平滑升级,所以需要进行数据迁移。为此,我们提供了从 Halo 1.5 / 1.6 版本迁移的插件。在进行迁移之前,**有几点注意事项和要求,如果你目前无法满足,建议先暂缓迁移。**
- Halo 版本必须为 1.5.x 或 1.6.x。如果不满足需要先升级到 1.5.x 或 1.6.x 版本。
- Halo 2.0 不兼容 1.x 的主题,建议在升级前先查询你正在使用的主题是否已经支持 2.0。你可以访问 [halo-sigs/awesome-halo](https://github.com/halo-sigs/awesome-halo) 或 [应用市场](https://halo.run/store/apps?type=THEME) 查阅目前支持的主题。
- Halo 2.0 目前没有内置 Markdown 编辑器,如果需要重新编辑迁移后的文章,需要额外安装 Markdown 编辑器插件。目前社区已经提供了以下插件:
- StackEdit<https://halo.run/store/apps/app-hDXMG>
- ByteMD<https://halo.run/store/apps/app-HTyhC>
- Halo 2.0 不再内置友情链接、日志、图库等模块,需要安装额外的插件,目前官方已提供:
- 链接管理:<https://halo.run/store/apps/app-hfbQg>
- 图库:<https://halo.run/store/apps/app-BmQJW>
- 瞬间(原日志):<https://halo.run/store/apps/app-SnwWD>
- Halo 2.0 不再内置外部云存储的支持。需要安装额外的插件,目前官方已提供:
- S3兼容国内主流的云存储<https://halo.run/store/apps/app-Qxhpp>
- 阿里云 OSS<https://halo.run/store/apps/app-wCJCD>
- 在迁移过程中不会保留旧版本的用户数据,迁移完成之后,关于文章等数据的关联都将改为 Halo 2.0 的新用户。
- 为了防止直接升级 2.0 导致 1.x 的数据受到破坏,我们已经将工作目录由 `~/.halo` 变更为 `~/.halo2`
- 目前 Halo 2.0 仅提供 Docker 部署方式,没有提供可执行 JAR 包,但可以自编译,请参考 [构建](../developer-guide/core/build.md) 文档
- 可以考虑先在本地运行一个 Halo 2.0,模拟一下导入,检查导入后是否满足要求。
如果遇到了迁移过程中的问题,也可以向我们提交 Issue: <https://github.com/halo-dev/halo/issues/new/choose>,以上暂不支持的功能我们也会陆续完善。
## 备份数据
在进行迁移操作之前,我们强烈建议先**完整备份所有数据**,可以参考 [备份迁移](https://docs.halo.run/user-guide/backup-migration) 进行整站备份。
## 导出数据文件
在 Halo 1.5.x / 1.6.x 后台的小工具中提供了数据导出的功能,将最新的数据进行备份,然后下载即可。这个数据文件包含了数据库所有的数据,后续我们在 2.0 的导入插件中就是通过这个文件进行数据导入。
![halo-data-export.png](/img/halo-data-export.png)
## 部署 Halo 2.0
可以参考以下文档进行部署:
- [使用 Docker 部署](./install/docker.md)
- [使用 Docker Compose 部署](./install/docker-compose.md)
:::tip
可以考虑暂时保留旧版本的 Halo等到迁移完成之后再移除。如果需要保留旧版本的 Halo请注意在部署 Halo 2.0 的时候使用其他端口然后在反向代理Nginx中修改为 Halo 2.0 的运行端口即可。
:::
## 移动附件
- 本地存储的附件,只需要将 1.x 工作目录的 `upload` 目录里面的所有文件夹移动到 2.0 工作目录下的 `attachments/migrate-from-1.x` 文件夹即可。
- 云存储的附件迁移会在迁移插件中进行。
## 安装插件
在迁移过程中,需要提前安装必要的插件:
- 站点迁移:<https://halo.run/store/apps/app-TlUBt>
- 链接管理:<https://halo.run/store/apps/app-hfbQg>
- 图库:<https://halo.run/store/apps/app-BmQJW>
- 瞬间(原日志):<https://halo.run/store/apps/app-SnwWD>
- S3如果需要迁移存在云存储的附件需要安装<https://halo.run/store/apps/app-Qxhpp>
## 配置存储策略
> 如果在 Halo 1.x 中未使用云存储,可以跳过此步骤。
1. 安装 S3 插件。
2. 进入附件管理页面。
3. 点击页面右上角的 **存储策略** 按钮。
4. 创建存储策略,选择 **S3 Object Storage**
5. 填写相关配置,点击 **保存** 即可。
## 迁移
![Migrate Plugin](/img/migrate/halo2.0-migrate-plugin.png)
1. 点击左侧菜单的迁移进入迁移页面。
2. 点击 **选择文件** 按钮,选择在 Halo 1.5.x / 1.6.x 导出的数据文件JSON 格式)。
3. 如果在 1.x 中使用了云存储,会弹出选择云存储的对话框,选择之前创建的存储策略即可。
4. 最后点击页面下方的 **执行导入** 即可。

@ -0,0 +1,98 @@
---
title: 写在前面
description: 在开始前,您需要了解的事项
---
## 环境要求
这里将讲述运行 Halo 所要求的一些软硬件的配置,我们建议您在运行或者部署之前先浏览一遍此页面。
### 硬件配置
:::tip
如果您要使用服务器进行部署 Halo您需要注意的是Halo 目前不支持市面上的云虚拟主机,请使用云服务器或者 VPS。
:::
#### CPU
无特别要求。目前我们的 [Docker 镜像](https://hub.docker.com/r/halohub/halo) 也已经支持多平台。
#### 内存
为了获得更好的体验,我们建议至少配置 1G 的 RAM。
#### 磁盘
无特别要求理论上如果不大量在服务器上传附件Halo 对磁盘的容量要求并不是很高。但我们推荐最好使用 SSD 硬盘的服务器,能更快的运行 Halo。
#### 网络
Halo 目前必须在外网畅通的情况下使用,否则会导致页面异常。
### 软件环境
Halo 理论上可以运行在任何支持 Docker 及 Java 的平台。
#### Docker
必须在运行环境安装好 [Docker](https://www.docker.com/) 环境,目前 Halo 的默认安装运行方式均使用容器。
#### JRE可选
目前 Halo 的默认及推荐安装方式为 Docker 容器运行,使用 jar 包运行的方式需要用户自行构建 jar 包。
:::info
当前版本2.0)需要 JRE 17 的版本,推荐使用 OpenJDK 17。
:::
#### PostgreSQL可选
也可以使用系统自带的 H2 Database 数据库,无需安装。但不推荐在生产环境中使用 H2 Database。
#### Web 服务器(可选)
如果您部署在生产环境,那么你很可能需要进行域名绑定,这时候我们推荐使用诸如 [Nginx](http://nginx.org/)、[Caddy](https://caddyserver.com/) 之类的 Web 服务器进行反向代理。但需要注意的是,目前 Halo 不支持代理到子目录halo.run/blog
#### Wget可选
后续的文档中,我们会使用 wget 为例,用于下载所需要的文件,所以请确保服务器已经安装好了这个软件包。当然,下载文件不限制工具,如果你对其他工具熟悉,可以忽略。
#### VIM可选
后续的文档中,我们会使用 vim 为例,用于修改一些必要的配置文件,所以同样请确保服务器已经安装了这个软件包。当前,修改文档也不限制工具,如果你对其他编辑软件熟悉,也可以忽略。
## 浏览器支持
1. 用户前台:视主题所支持的情况而定,由于目前的评论模块使用了 [Vuejs](https://cn.vuejs.org/v2/guide/installation.html#%E5%85%BC%E5%AE%B9%E6%80%A7) 开发,所以在 [Vuejs](https://cn.vuejs.org/v2/guide/installation.html#%E5%85%BC%E5%AE%B9%E6%80%A7) 不支持的某些浏览器中无法正常显示评论区域。
2. 管理后台:支持目前常见的现代浏览器,具体视 [Vuejs](https://cn.vuejs.org/v2/guide/installation.html#%E5%85%BC%E5%AE%B9%E6%80%A7) 框架的支持情况而定。
## 名词解释
这里将列出后续文档中一些和 Halo 相关的名词含义。
### ~(符号)
代表当前系统下的 [用户目录](https://zh.wikipedia.org/wiki/%E5%AE%B6%E7%9B%AE%E5%BD%95)。
### 镜像
指 Halo 构建所产生的 [Docker 镜像](https://docs.docker.com/engine/reference/commandline/images/)。用户通过该镜像启动 Halo 应用。
### 工作目录
指 Halo 所依赖的工作目录,在 Halo 运行的时候会在系统当前用户目录下产生一个 `.halo2` 的文件夹,绝对路径为 `~/.halo2`。由于这个工作目录是固定的,所以上面所说的 `运行包`不限制所存放的位置,里面通常包含下列目录或文件:
1. `db`:存放 H2 Database 的物理文件,如果你使用其他数据库,那么不会存在这个目录。
2. `themes`:里面包含用户所安装的主题。
2. `plugins`:里面包含用户所安装的插件。
5. `attachments`:附件目录。
4. `logs`:运行日志目录。
6. `application.yaml`:配置文件。
### 主题
包含了各种站点页面模板的资源包。用户访问 Halo 站点浏览到的内容及样式,由 Halo 管理端所配置使用的主题所决定。
### 插件
用于扩展 Halo 功能的软件包。插件独立于 Halo 核心应用,可以单独安装、升级、卸载。

@ -0,0 +1,78 @@
---
id: intro
sidebar_label: 简介
title: ''
sidebar_position: 1
hide_title: true
slug: /
---
<p align="center">
<a href="https://halo.run" target="_blank" rel="noopener noreferrer">
<img width="100" src="https://halo.run/logo" alt="Halo logo" />
</a>
</p>
<p align="center"><b>Halo</b> [ˈheɪloʊ],强大易用的开源建站工具。</p>
<p align="center">
<a href="https://github.com/halo-dev/halo/releases"><img alt="GitHub release" src="https://img.shields.io/github/release/halo-dev/halo.svg?style=flat-square&include_prereleases" /></a>
<a href="https://hub.docker.com/r/halohub/halo"><img alt="Docker pulls" src="https://img.shields.io/docker/pulls/halohub/halo?style=flat-square" /></a>
<a href="https://github.com/halo-dev/halo/commits"><img alt="GitHub last commit" src="https://img.shields.io/github/last-commit/halo-dev/halo.svg?style=flat-square" /></a>
<a href="https://github.com/halo-dev/halo/actions"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/halo-dev/halo/halo.yaml?branch=main&style=flat-square" /></a>
<a href="https://codecov.io/gh/halo-dev/halo"><img alt="Codecov percentage" src="https://img.shields.io/codecov/c/github/halo-dev/halo/main?style=flat-square&token=YsRUg9fall"/></a>
<br />
<a href="https://halo.run">官网</a>
<a href="https://docs.halo.run">文档</a>
<a href="https://bbs.halo.run">社区</a>
<a href="https://gitee.com/halo-dev">Gitee</a>
<a href="https://t.me/halo_dev">Telegram 频道</a>
</p>
------------------------------
## 快速开始
```bash
docker run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.10
```
以上仅作为体验使用,详细部署文档请查阅:<https://docs.halo.run/getting-started/install/docker-compose>
## 在线体验
- 环境地址:<https://demo.halo.run>
- 后台地址:<https://demo.halo.run/console>
- 用户名:`demo`
- 密码:`P@ssw0rd123..`
## 生态
可访问 [awesome-halo](https://github.com/halo-sigs/awesome-halo) 查看已经适用于 Halo 2.0 的主题和插件,以及适用于 Halo
1.x 的相关仓库。
## 许可证
[![license](https://img.shields.io/github/license/halo-dev/halo.svg?style=flat-square)](https://github.com/halo-dev/halo/blob/master/LICENSE)
Halo 使用 GPL-v3.0 协议开源,请遵守开源协议。
## 赞助
如果 Halo 对你有帮助,欢迎[赞助我们](https://afdian.net/a/halo-dev),感谢以下赞助者对 Halo 项目的支持:
<p align="center">
<a target="_blank" href="https://afdian.net/a/halo-dev">
<img alt="sponsors" src="https://github.com/halo-sigs/sponsor-images/blob/main/sponsorkit/sponsors.svg?raw=true" />
</a>
</p>
## 贡献
参考 [CONTRIBUTING](https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md)。
<a href="https://github.com/halo-dev/halo/graphs/contributors"><img src="https://opencollective.com/halo/contributors.svg?width=890&button=false" /></a>
## 状态
![Repobeats analytics](https://repobeats.axiom.co/api/embed/ad008b2151c22e7cf734d2688befaa795d593b95.svg "Repobeats analytics image")

@ -0,0 +1,108 @@
---
title: 附件
description: 附件管理相关功能说明
---
## 存储策略
为了能够更加灵活地管理附件的存储位置Halo 提供了存储策略的概念。
Halo 默认提供了本地的存储策略类型,你还可以通过安装插件的方式扩展其他的存储策略类型。一个存储策略包含了存储提供者,具体存储位置等使用该类型存储所必要的各种信息。
:::info
目前 Halo 官方维护了两个存储策略插件,可以根据需要选择安装:
1. 阿里云 OSS<https://halo.run/store/apps/app-wCJCD>
2. Amazon S3 协议(主流云存储厂商均支持):<https://halo.run/store/apps/app-Qxhpp>
:::
你可以点击附件页面右上角的 `存储策略` 按钮对存储策略进行管理。
### 新建存储策略
点击存储策略列表右上方的 `+` 添加按钮即可新建一个存储策略。
![添加存储策略](/img/user-guide/attachments/attachment-policy-add.png)
添加时首先需要选择一种存储策略类型,系统内置的存储策略为本地存储,图中的 `S3 Object Storage` 由[对象存储Amazon S3 协议)](https://halo.run/store/apps/app-Qxhpp)提供,此文档以本地存储为例。
![添加本地存储策略](/img/user-guide/attachments/attachment-policy-add-local.png)
添加一个本地存储时,你需要输入名称及存储位置信息。其中的存储位置决定了使用该存储策略的附件,在服务器上的实际存储路径,路径规则为 `{Halo 工作目录}/attachments/{存储位置}`,其中的 Halo 工作目录由安装时指定的参数决定,默认为 `~/.halo2`
:::info
默认的 Docker 部署方式,实际存储位置由挂载到 Halo 容器工作目录的服务器目录所决定。
:::
### 管理存储策略
点击存储策略列表指定存储所在行后方的 `···` 更多操作按钮即可对该存储策略进行编辑或删除。
![存储策略操作](/img/user-guide/attachments/attachment-policy-operate.png)
:::info
为了保护附件安全避免用户误操作,当存储策略下存在附件时,该存储策略不允许删除。如果确定要删除某个存储策略及该存储策略中的所有附件,可以先按照存储策略对附件进行筛选,先批量删除存储策略下的所有附件,再删除存储策略。
:::
## 附件分组
通过附件分组功能可以方便地将同一类型、同一用途的附件划分到一个分组中,方便后续附件的管理和插件。
附件所使用的存储策略决定了附件的实际存储位置和 URL 规则,而附件分组功能仅是逻辑上的归类划分,不会影响附件的存储位置及 URL。
### 新建分组
点击附件列表上方的 `添加分组` 按钮即可新建一个分组。
![添加分组](/img/user-guide/attachments/attachment-group-add.png)
### 删除分组
点击附件列表上方指定分组上的 `···` 更多按钮,可以对分组进行重命名或删除操作。
Halo 目前提供了两种分组删除策略:
1. **删除并将附件移动至未分组**:分组被删将被删除,分组下的附件移动到未分组中。
2. **删除并同时删除附件**:先删除下的所有附件后,再删除该分组。
:::warning
当使用 `删除并同时删除附件` 方式时,分组下的所有附件会被同时删除且不可恢复、无法找回,请谨慎进行该操作。
:::
## 上传附件
点击附件列表右上方的 `上传` 按钮即可上传新的附件到 Halo。
![上传附件](/img/user-guide/attachments/attachment-upload.png)
1. **分组选择**:你可以选择需要上传到的分组,为了方便,选择之后会在浏览器记住这个选项。
2. **存储策略选择**:你可以选择需要使用的存储策略,为了方便,选择之后会在浏览器记住这个选项。
3. **上传区域**:同时支持拖拽文件、点击上传区域选择文件、粘贴文件。
## 查看附件
点击附件列表中的某一个附件即可查看该附件的详细信息。
![附件详情](/img/user-guide/attachments/attachment-detail.png)
1. **预览区域**:目前支持图片、视频、音频的预览。
2. **链接**目前可以显示并复制链接、HTML 格式代码、Markdown 格式代码。
## 删除附件
目前有两种删除附件的方式,你可以选中一些附件进行批量删除或者点击指定附件所在行后方的 `···` 更多按钮,对单个附件进行删除操作。
![批量操作附件](/img/user-guide/attachments/attachment-batch-operate.png)
:::warning
附件删除后不可恢复、无法找回,请谨慎进行该操作。
:::
## 移动附件
![附件移动](/img/user-guide/attachments/attachment-move.png)
与批量删除操作类似,你可以选中多个附件后在上方的批量操作按钮中选择 `移动` 操作,将所选附件移动到指定的分组中。
:::info
需要注意的是,目前移动附件只支持移动分组,不支持存储策略的移动。
:::

@ -0,0 +1,38 @@
---
title: 备份迁移
description: 关于备份和迁移的最佳实践
---
:::info
在开始之前,我们推荐你先阅读 [《写在前面》](/getting-started/prepare) 的名词解释部分。
:::
## 备份
### 数据备份
目前 Halo 在后台的小工具中提供了数据导出的功能,此功能的作用为导出数据库的所有数据,格式为 `JSON`。通常可以作为切换数据库类型的时候使用。需要注意的是,此备份仅仅为备份数据,不包含其他诸如主题、附件等资料。如下图:
![halo-data-export.png](/img/halo-data-export.png)
点击右下角的备份按钮即可导出所有数据,之后点击备份文件的标题即可下载。
### 整站备份
通过 [《写在前面》](/getting-started/prepare) 的名词解释部分我们可以知道Halo 的所有数据都是存放在当前用户目录的工作目录(.halo下的使用 MySQL 数据库除外,你还需要导出 MySQL 数据)。**所以我们备份整站的数据仅需备份这个目录即可**,不管你使用何种方式。不过,为了操作方便,我们也在后台的小工具中提供了备份整站数据的功能,和上面所说的数据备份一致,点击备份按钮即可打包工作目录文件夹。如下图:
![halo-workspace-export.png](/img/halo-workspace-export.png)
## 迁移
### 导入数据
此功能为导入上面所说的数据备份产生的数据文件JSON 格式),并非整站备份的工作目录文件。需要注意的是,此功能仅在站点初始化的时候支持。如下图:
![halo-data-import.png](/img/halo-data-import.png)
上传文件之后,点击导入即可。
### 整站迁移
此操作通常用于迁移服务器,基于上面 **整站备份** 所说Halo 的所有数据都是存放于当前用户目录的工作目录(.halo下的。当然这仅限于使用 **H2 Database** 的情况下,如果你使用的 MySQL那么还需要手动导出 MySQL 数据。所以,我们迁移服务器仅仅需要将工作目录的备份文件上传到新服务器的用户目录下解压,然后按照 [《安装指南》](/getting-started/install/linux) 重新安装即可。MySQL 用户还需要做的就是手动导出 MySQL 数据,并在新服务器上导入。

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save