Skip to content

Commit 874d2c5

Browse files
authored
feat: Add Finder API for post word statistics
1 parent 3f553d0 commit 874d2c5

14 files changed

Lines changed: 642 additions & 23 deletions

File tree

.github/workflows/cd.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ on:
88
jobs:
99
cd:
1010
uses: halo-sigs/reusable-workflows/.github/workflows/plugin-cd.yaml@v3
11+
secrets:
12+
halo-pat: ${{ secrets.HALO_PAT }}
1113
permissions:
1214
contents: write
1315
with:
1416
ui-path: "ui"
15-
pnpm-version: 9
16-
node-version: 22
17+
pnpm-version: 10
18+
node-version: 24
1719
java-version: 21
1820
# Remove skip-appstore-release and set app-id if you want to release to the App Store
1921
skip-appstore-release: true

.github/workflows/ci-test.yaml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Test
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches: [ main ]
7+
pull_request:
8+
types: [ opened, synchronize, reopened ]
9+
10+
jobs:
11+
build:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v5
15+
with:
16+
submodules: true
17+
- name: Set up JDK 21
18+
uses: actions/setup-java@v5
19+
with:
20+
distribution: 'temurin'
21+
cache: 'gradle'
22+
java-version: 21
23+
- uses: pnpm/action-setup@v4
24+
name: Install pnpm
25+
id: pnpm-install
26+
with:
27+
version: 10
28+
run_install: false
29+
- name: Set up Node.js
30+
uses: actions/setup-node@v5
31+
with:
32+
node-version: 24
33+
cache: 'pnpm'
34+
cache-dependency-path: 'ui/pnpm-lock.yaml'
35+
- name: Make gradlew executable
36+
run: chmod +x ./gradlew
37+
- name: gradlew test
38+
run: |
39+
./gradlew test
40+
- name: Front test
41+
run: |
42+
cd ui
43+
pnpm test

.github/workflows/ci.yaml

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
name: CI
22

33
on:
4-
push:
5-
branches:
6-
- main
7-
pull_request:
8-
branches:
9-
- main
4+
# push:
5+
# branches:
6+
# - main
7+
# pull_request:
8+
# branches:
9+
# - main
10+
workflow_dispatch:
1011

1112
jobs:
1213
ci:
1314
uses: halo-sigs/reusable-workflows/.github/workflows/plugin-ci.yaml@v3
1415
with:
1516
ui-path: "ui"
16-
pnpm-version: 9
17-
node-version: 22
17+
pnpm-version: 10
18+
node-version: 24
1819
java-version: 21

.github/workflows/workflow.yaml

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
name: Build Plugin JAR File
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches: [ main ]
7+
release:
8+
types:
9+
- created
10+
pull_request:
11+
types: [ opened, synchronize, reopened ]
12+
13+
jobs:
14+
build:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v5
18+
with:
19+
submodules: true
20+
- name: Set up JDK 21
21+
uses: actions/setup-java@v5
22+
with:
23+
distribution: 'temurin'
24+
cache: 'gradle'
25+
java-version: 21
26+
- uses: pnpm/action-setup@v4
27+
name: Install pnpm
28+
id: pnpm-install
29+
with:
30+
version: 10
31+
run_install: false
32+
- name: Set up Node.js
33+
uses: actions/setup-node@v5
34+
with:
35+
node-version: 24
36+
cache: 'pnpm'
37+
cache-dependency-path: 'ui/pnpm-lock.yaml'
38+
- name: Make gradlew executable
39+
run: chmod +x ./gradlew
40+
- name: Install Frontend Dependencies
41+
run: |
42+
./gradlew pnpmInstall
43+
- name: Build with Gradle
44+
run: |
45+
# Set the version with tag name when releasing
46+
version=${{ github.event.release.tag_name }}
47+
version=${version#v}
48+
if [ -n "$version" ]; then
49+
sed -i "s/version=.*-SNAPSHOT$/version=$version/1" gradle.properties
50+
fi
51+
./gradlew clean build -x test
52+
- name: Archive extra-api jar
53+
uses: actions/upload-artifact@v4
54+
with:
55+
name: extra-api
56+
path: |
57+
build/libs/*.jar
58+
retention-days: 1
59+
60+
github-release:
61+
runs-on: ubuntu-latest
62+
needs: build
63+
if: github.event_name == 'release'
64+
steps:
65+
- name: Download extra-api jar
66+
uses: actions/download-artifact@v2
67+
with:
68+
name: extra-api
69+
path: build/libs
70+
- name: Get Name of Artifact
71+
id: get_artifact
72+
run: |
73+
ARTIFACT_PATHNAME=$(ls build/libs/*.jar | head -n 1)
74+
ARTIFACT_NAME=$(basename ${ARTIFACT_PATHNAME})
75+
echo "Artifact pathname: ${ARTIFACT_PATHNAME}"
76+
echo "Artifact name: ${ARTIFACT_NAME}"
77+
echo "ARTIFACT_PATHNAME=${ARTIFACT_PATHNAME}" >> $GITHUB_ENV
78+
echo "ARTIFACT_NAME=${ARTIFACT_NAME}" >> $GITHUB_ENV
79+
echo "RELEASE_ID=${{ github.event.release.id }}" >> $GITHUB_ENV
80+
- name: Upload a Release Asset
81+
uses: actions/github-script@v2
82+
if: github.event_name == 'release'
83+
with:
84+
github-token: ${{secrets.GITHUB_TOKEN}}
85+
script: |
86+
console.log('environment', process.versions);
87+
88+
const fs = require('fs').promises;
89+
90+
const { repo: { owner, repo }, sha } = context;
91+
console.log({ owner, repo, sha });
92+
93+
const releaseId = process.env.RELEASE_ID
94+
const artifactPathName = process.env.ARTIFACT_PATHNAME
95+
const artifactName = process.env.ARTIFACT_NAME
96+
console.log('Releasing', releaseId, artifactPathName, artifactName)
97+
98+
await github.repos.uploadReleaseAsset({
99+
owner, repo,
100+
release_id: releaseId,
101+
name: artifactName,
102+
data: await fs.readFile(artifactPathName)
103+
});

README.md

Lines changed: 125 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,135 @@
11
# halo-plugin-extra-api
22

3-
halo-plugin-extra-api - Halo 插件
4-
53
## 简介
64

75
一个为 Halo CMS 提供额外 API 的轻量级插件。
86

9-
## 已提供 API
7+
## 核心理念
8+
9+
> CMS 的价值就是在服务端管理和处理内容,为什么要把简单的数据处理推到前端去增加复杂度呢?
10+
11+
让 CMS 回归"内容即数据"的本质,减少**不必要**的前端异步请求和动态渲染。
12+
13+
这个插件正是基于这个理念:
14+
- 让复杂的逻辑在后端处理
15+
- 前端模板只负责展示
16+
- 提供简洁的 Finder API
17+
- 减少不必要的 JavaScript 依赖
18+
19+
<details><summary>前端动态加载方式 vs 后端服务端渲染方式</summary>
20+
21+
| 对比维度 | 前端动态加载方式 | 后端服务端渲染方式 |
22+
|---------|-----------------|-------------------|
23+
| **性能表现** | ❌ 需要额外 HTTP 请求,增加延迟 | ✅ 服务端渲染,一次性输出 |
24+
| **用户体验** | ❌ 页面闪烁,先显示占位符后填充数据 | ✅ 内容立即可见,无加载状态 |
25+
| **SEO 友好** | ❌ 搜索引擎难以抓取动态内容 | ✅ 服务端渲染,完全 SEO 友好 |
26+
| **错误处理** | ❌ 需要处理网络失败、超时等异常 | ✅ 服务端统一异常处理,减轻主题作者心智负担 |
27+
| **开发复杂度** | ❌ 需要编写 JS 代码、状态管理、DOM 操作 | ✅ 模板中直接调用,代码简洁 |
28+
| **缓存策略** | ❌ 需要前端缓存逻辑或重复请求 | ✅ 可利用模板缓存和服务端缓存 |
29+
| **首屏渲染 (FCP)** | ❌ 需要等待 JS 执行和 API 响应 | ✅ HTML 直接包含内容,渲染更快 |
30+
| **最大内容绘制 (LCP)** | ❌ 动态内容加载延迟主要内容显示 | ✅ 关键内容随页面一起渲染 |
31+
| **累积布局偏移 (CLS)** | ❌ 内容异步加载可能导致页面跳动 | ✅ 静态布局,无意外的布局变化 |
32+
| **交互响应 (INP)** | ❌ JS 执行和 DOM 操作影响交互性能 | ✅ 减少 JS 负担,交互更流畅 |
33+
34+
</details>
35+
36+
## TODO
37+
38+
- [] 缓存文章字数统计 API 结果
39+
- [] 提供随机文章 API
40+
- [] 提供预计阅读时间 API,及相关配置项
41+
42+
## API 文档
43+
44+
### 检测本插件是否启用
45+
46+
**描述**
47+
48+
检测 ExtraAPI 插件是否已安装并启用。建议在主题中使用本插件 API 前先进行检测,以确保插件可用性。
49+
50+
**参数**
51+
- `extra-api` - 本插件的标识符(`metadata.name`
1052

11-
### 文章字数统计
53+
**返回值**
54+
- `boolean` - 插件可用时返回 true,否则返回 false
1255

13-
本插件提供了一个 API 用于查询文章字数,可查询指定文章/全部文章总和。
56+
**示例**
57+
```html
58+
<!--/* 先检测插件可用性,再使用 API */-->
59+
<th:block th:if="${pluginFinder.available('extra-api')}">
60+
<span
61+
th:text="|总字数:${extraApiStatsFinder.wordCount()}|"
62+
></span>
63+
</th:block>
64+
65+
<!--/* 写在一个标签内也可以,th:if 的优先级比 th:text 高 */-->
66+
<span
67+
th:if="${pluginFinder.available('extra-api')}"
68+
th:text="|总字数:${extraApiStatsFinder.wordCount()}|"
69+
></span>
70+
71+
<!--/* 自然模板写法 */-->
72+
<span th:if="${pluginFinder.available('extra-api')}">总字数:[[${extraApiStatsFinder.wordCount()}]]</span>
73+
```
74+
75+
**说明**
76+
77+
使用 `pluginFinder.available('extra-api')` 可以优雅地处理插件依赖,避免在插件未安装时出现模板错误,提升主题的兼容性和用户体验。
78+
79+
### 统计信息 API
80+
81+
**Finder 名称:** `extraApiStatsFinder`
82+
83+
#### 文章字数统计
84+
85+
```javascript
86+
extraApiStatsFinder.wordCount({
87+
name: 'post-metadata-name', // 可选,未传入则统计全部文章字数总和
88+
version: 'release' | 'draft' // 可选,默认 'release'
89+
});
90+
```
91+
92+
```javascript
93+
extraApiStatsFinder.wordCount();
94+
```
95+
96+
**描述**
97+
98+
- 参数说明:
99+
- `name`:文章的 `metadata.name`,可选参数。未传入时统计全部文章字数总和。
100+
- `version`:统计版本,可选 `release`(默认)或 `draft`
101+
- 计数规则:
102+
- 中文、日文、韩文等 CJK 字符按每个字符计 1。
103+
- ASCII 连续字母/数字按 1 个单词计数。
104+
- 标点符号和空格不计入统计。
105+
- 错误处理:
106+
- 输入为空或文章不存在时返回 0,不会抛出异常。
107+
- 性能说明:
108+
- 单次调用开销较小,适合在模板中直接使用。
109+
110+
**参数**
111+
- `name:string` – 文章 `metadata.name`(可选,不传则统计全站)
112+
- `version:string` – 统计版本,可选 `release`(默认)或 `draft`
113+
114+
**返回值**
115+
- `int` – 字数统计结果(非负),不存在或参数缺失时返回 0
116+
117+
**使用示例**
118+
```html
119+
<!--/* 统计文章已发布版本的字,适用于 /templates/post.html */-->
120+
<span th:text="${extraApiStatsFinder.wordCount({name: post.metadata.name})}"></span>
121+
122+
<!--/* 统计文章最新版本的字数(含草稿),适用于 /templates/post.html */-->
123+
<span th:text="${extraApiStatsFinder.wordCount({name: post.metadata.name, version: 'draft'})}"></span>
124+
125+
<!--/* 统计全站已发布文章的总字数,适用于全部模板 */-->
126+
<span th:text="${extraApiStatsFinder.wordCount()}"></span>
127+
<!--/* 与下方写法等价 */-->
128+
<span th:text="${extraApiStatsFinder.wordCount({})}"></span>
129+
130+
<!--/* 统计全站所有文章最新版本的总字数(含草稿),适用于全部模板 */-->
131+
<span th:text="${extraApiStatsFinder.wordCount({version: 'draft'})}"></span>
132+
```
14133

15134
## 开发环境
16135

@@ -40,4 +159,4 @@ pnpm dev
40159

41160
## 许可证
42161

43-
[AGPL-3.0](./LICENSE) © HowieHz
162+
[AGPL-3.0](./LICENSE) © HowieHz

build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ repositories {
1313
dependencies {
1414
implementation platform('run.halo.tools.platform:plugin:2.21.0')
1515
compileOnly 'run.halo.app:api'
16+
17+
// Add Spring dependencies for IDE support
18+
compileOnly 'org.springframework:spring-context'
19+
compileOnly 'org.springframework:spring-core'
20+
compileOnly 'org.springframework:spring-beans'
21+
compileOnly 'org.springframework.data:spring-data-commons'
22+
compileOnly 'io.projectreactor:reactor-core'
1623

1724
testImplementation 'run.halo.app:api'
1825
testImplementation 'org.springframework.boot:spring-boot-starter-test'

settings.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ pluginManagement {
33
gradlePluginPortal()
44
}
55
}
6-
rootProject.name = 'plugin-halo-plugin-extra-api'
6+
rootProject.name = 'extra-api'
77
include 'ui'

src/main/java/top/howiehz/halopluginextraapi/HaloPluginExtraApiPlugin.java renamed to src/main/java/top/howiehz/halo/plugin/extra/api/HaloPluginExtraApiPlugin.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package top.howiehz.halopluginextraapi;
1+
package top.howiehz.halo.plugin.extra.api;
22

33
import org.springframework.stereotype.Component;
44
import run.halo.app.plugin.BasePlugin;

0 commit comments

Comments
 (0)