本文面向主题开发者。站点运营者安装与使用主题见 快速开始;Tera 模板语法见 基础介绍,内置函数见 内置函数。
快速开始
官方参考项目:theme-youlog{target="_blank"}。仓库将主题源码与演示站点放在一起,便于本地预览。主题功能与 config 字段说明见主题目录 README。
theme-youlog/
├── HOME.md # 演示内容(Markdown)
├── __everkm/
│ ├── everkm.yaml # 演示站点配置
│ └── theme/youlog/ # 主题源码(模板、前端、构建脚本)
│ ├── templates/
│ ├── src/
│ └── everkm-theme.yaml
工作区目录约定见 目录结构。
安装依赖
git clone https://github.com/everkm/theme-youlog.git
cd theme-youlog/__everkm
# 安装 everkm-publish CLI
pnpm install
# 安装主题前端依赖
cd theme/youlog && pnpm i
本地预览
在 __everkm/ 下启动预览服务:
cd theme-youlog/__everkm
everkm-publish serve \
--work-dir ../ \
--theme youlog \
--theme-dev \
--listen 0.0.0.0:9081
浏览器访问 http://localhost:9081{target=_blank}。
--work-dir ../:工作区指向仓库根目录(演示 Markdown 与__everkm/everkm.yaml)--theme youlog:使用__everkm/theme/youlog/下的主题--theme-dev:监听主题源码变更并自动重载
若主题使用 TSX 开发(如 youlog),另开终端在 theme/youlog/ 下运行前端 watch 构建:
cd theme-youlog/__everkm/theme/youlog
pnpm run dev:jsrender # watch 浏览器资源 + templates/everkm-render.js
# 或 pnpm run dev # 仅 watch 浏览器端资源
导出静态站点
cd theme-youlog/__everkm
everkm-publish serve \
--work-dir ../ \
--theme youlog \
--theme-dev \
--export
构建与安装主题包
主题打包由前端构建脚本完成,everkm-publish 负责安装已打包的 .zip:
cd theme-youlog/__everkm/theme/youlog
# 1. 构建前端资源与 everkm-render.js
pnpm run build:jsrender
# 2. 组装可发布目录
mkdir -p dist/assets
cp -r templates dist/
cp assets-manifest.json everkm-theme.yaml dist/
cp assets/* dist/assets/
# 3. 打包(可选,zip 内顶层目录为 youlog/)
mkdir .dist && mv dist .dist/youlog
cd .dist && zip -r ../youlog.zip youlog && cd .. && rm -rf .dist
# 4. 安装到站点
everkm-publish theme install ../youlog.zip
theme-youlog 仓库在 __everkm/ 与 theme/youlog/ 下提供了 Makefile(如 make work、make build),是对上述命令的便捷封装,可按需使用。
主题元信息 everkm-theme.yaml
name: youlog
version: 0.1.0
author: dayu<dayu@dayu.me>
repository: https://github.com/everkm/theme-youlog
demo: https://youlog.theme.everkm.com/
default_template: post.html
# 主题预设目录发布配置(站点 everkm.yaml#folders 按 path 覆盖)
folders:
"/":
hide_in_url: true
"/blog/":
url_slug: posts
template: list.html
url_id_suffix: false
config:
site:
name: Demo Blog
字段说明:
name模板名称(仅主题侧;勿写入站点everkm.yaml)version版本号author作者repository代码库demoDemo 地址screenshots主题截图 URL 列表(可选)default_template主题默认模板名称(可被everkm.yaml#default_template覆盖)folders主题预设的目录发布配置,字段与everkm.yaml#folders相同;站点仅写需要覆盖的 path 与字段(见 目录配置 · 合并规则)config主题级配置,在模板中通过__config读取,与项目everkm.yaml#config合并(项目覆盖主题)
everkm-theme.yaml 不包含 inject_js、inject_css、redirects——这些属于站点运营与部署范畴,见 站点配置。
folders 典型用途:预设与模板配套的 URL 前缀(如 url_slug: posts)、默认 template、url_id_suffix: false 等。演示站点 everkm.yaml 可只声明增量,避免重复配置。
站点运营者安装主题:
everkm-publish theme install everkm/youlog@0.5.6
everkm-publish theme install /path/to/youlog.zip --force
远程安装对齐 ekmp-themes{target="_blank"} 官方目录。init 默认主题为 paper。
页面渲染回退
预览与导出时,若某 URL 对应的 Tera 模板不存在(例如部分主题仅有 JS 版 index.html),会自动改用主题自带的 JS 渲染 路径,使首页与文章页均可正常显示。主题开发者可同时提供 Tera 与 JS 两套模板,或仅维护 JS 版。
JS 渲染(JsRender)
主题可用 JavaScript 替代或补充 Tera 模板。everkm-publish 内置轻量级 JS 运行时,加载主题提供的 everkm-render.js 并调用其导出函数完成页面渲染。
完整的 TypeScript 类型定义见官方主题:https://github.com/everkm/theme-youlog/tree/master/__everkm/theme/youlog/src/types{target="_blank"}。
接口结构
templates/everkm-render.js 需导出三个函数,用原生 JavaScript 即可实现:
// 健康检查,启动时验证脚本可用
export function ping() {
return "pong";
}
// 页面渲染
// name: 模板名(如 "index.html"、"book"),由 folders.template 或 default_template 决定
// props: 页面上下文 JSON 对象(见下方)
// 返回: 完整 HTML 字符串(含 <!DOCTYPE html>)
export async function renderPage(name, props) {
const requestId = props.request_id;
const doc = everkm.post_detail(requestId, { id: props.post.id });
const css = everkm.assets(requestId, { type: "css", section: "mytheme" });
const js = everkm.assets(requestId, { type: "js", section: "mytheme" });
return `<!DOCTYPE html>
<html>
<head>${css}</head>
<body>
<h1>${doc.title}</h1>
${doc.content}
${js}
</body>
</html>`;
}
// dCard 渲染
// name: 卡片名(如 "audio"、"download")
// props: 卡片参数 + page_context(所在页面的完整上下文)
export async function renderDcard(name, props) {
if (name === "audio") {
return `<audio src="${props.media}" controls></audio>`;
}
throw new Error(`Unknown dcard: ${name}`);
}
渲染流程
请求 URL
→ 确定模板名(folders.template / default_template / URL 路径)
→ Tera 模板存在?→ Tera 渲染
→ 不存在?→ JsRender: renderPage(name, props)
→ 注入静态资源
dCard 同理:先尝试本地 .dcard.html 模板,不存在时调用 renderDcard。
主题可以同时提供 Tera 和 JS 两套模板,Tera 优先;也可以仅维护 JS 版。
页面上下文
renderPage 的 props 参数(对应 TypeScript PageContext):
| 字段 | 类型 | 说明 |
|---|---|---|
request_id | string | 本次请求唯一 ID,调用 everkm.* API 时必须传入 |
post | PostItem | null | 命中文章时的文章元数据 |
breadcrumbs | BreadcrumbResolved[] | 面包屑 |
config | Record<string, any> | 合并后的站点配置 |
qs | Record<string, any> | 当前目录的 query 合并结果 |
tpl_path | string | 当前使用的模板路径 |
page_path | string | 当前页面路径 |
lang | string | 当前语言代码 |
host | string | null | 主机名 |
env_is_preview | boolean | 是否为预览模式 |
dCard 的 props 除卡片参数外,还通过 page_context 注入所在页面的完整 PageContext。
everkm 全局 API
JS 全局对象 everkm 提供与 Tera 内置函数对等的数据查询能力,完整类型定义见 https://github.com/everkm/theme-youlog/blob/master/__everkm/theme/youlog/src/types/everkm.d.ts{target="_blank"}。
| 方法 | 说明 |
|---|---|
everkm.posts(requestId, args?) | 文章列表 |
everkm.post_detail(requestId, args) | 文章详情(含 Markdown 转 HTML 正文) |
everkm.post_meta(requestId, args) | 文章元数据 |
everkm.has_post(requestId, args) | 文章是否存在 |
everkm.nav_tree(requestId, args) | 导航树 |
everkm.nav_path(requestId, args) | 导航路径 |
everkm.nav_indicator(requestId, args) | 上/下一篇导航 |
everkm.config(requestId, args) | 读取配置值 |
everkm.assets(requestId, args) | 静态资源标签(CSS/JS) |
everkm.base_url(requestId) | 站点基础 URL |
everkm.asset_base_url(requestId) | 资源基础 URL |
everkm.data(requestId, args) | 数据源查询 |
everkm.markdown_to_html(content) | Markdown 转 HTML |
everkm.media(requestId, args) | 本地媒体资源 URL |
everkm.media_remote(requestId, args) | 远程媒体 URL |
everkm.media_dimension(requestId, args) | 媒体尺寸(宽/高) |
everkm.page_query(requestId, args) | 分页查询 |
everkm.env(requestId, args) | 环境变量 |
everkm.lang() | 当前语言代码 |
使用 TSX 开发
用原生 JS 拼接 HTML 适合简单场景。对于复杂主题,推荐使用 TSX(JSX)结构开发——用组件化方式组织页面,构建时输出到 everkm-render.js。
这不是传统意义的 SSR(没有运行时服务端),而是利用 TSX 的组件结构在 JS 沙箱内完成一次性渲染,输出静态 HTML 字符串。
以 SolidJS{target="_blank"} 为例即可——体积小、无虚拟 DOM,适合 JS 沙箱环境;无需学习状态管理、路由等框架全家桶,只要会用 TSX 写出 HTML:
目录结构(参考官方主题 theme-youlog{target="_blank"}):
theme/
├── src/
│ ├── entries/
│ │ ├── jsrender.ts # JS 渲染入口(导出 ping / renderPage / renderDcard)
│ │ └── browser.ts # 浏览器端入口(交互逻辑)
│ ├── pages/
│ │ ├── index.tsx # 页面渲染主逻辑
│ │ └── book.tsx # 文档页组件
│ ├── layout/
│ │ ├── RootLayout.tsx # 根布局
│ │ ├── Sidebar.tsx # 侧边栏
│ │ └── ArticleContent.tsx
│ ├── components/ # 通用组件
│ ├── dcard/
│ │ └── index.tsx # dCard 渲染
│ └── types/
│ ├── context.d.ts # PageContext / PostItem 类型定义
│ └── everkm.d.ts # everkm.* API 类型定义
├── templates/ # 构建输出目录(everkm-render.js 落在此处)
├── build.js # esbuild 构建脚本
└── package.json
入口文件(src/entries/jsrender.ts):
import { renderPage } from "../pages";
import { renderDcard } from "../dcard";
function ping() {
return "pong";
}
export { ping, renderPage, renderDcard };
页面渲染(src/pages/index.tsx):
import { renderToStringAsync } from "solid-js/web";
import { RootLayout } from "../layout/RootLayout";
import { BookPage } from "./book";
async function renderPage(compName: string, props: any) {
const html = await renderToStringAsync(() => {
switch (compName) {
case "book":
return (
<RootLayout context={props}>
<BookPage props={props} />
</RootLayout>
);
default:
throw new Error(`Page ${compName} not found`);
}
});
// 在 JS 渲染阶段注入 CSS/JS 资源
const requestId = props.request_id;
const css = everkm.assets(requestId, { type: "css", section: "mytheme" }) || "";
const js = everkm.assets(requestId, { type: "js", section: "mytheme" }) || "";
const withCss = html.replace(/<\/head>/i, `${css}</head>`);
const withJs = withCss.replace(/<\/body>/i, `${js}</body>`);
return `<!DOCTYPE html>${withJs}`;
}
export { renderPage };
dCard 渲染(src/dcard/index.tsx):
import { renderToStringAsync } from "solid-js/web";
export async function renderDcard(name: string, props: any) {
return await renderToStringAsync(() => {
switch (name) {
case "items":
return <ItemsCard {...props} />;
default:
throw new Error(`Dcard ${name} not found`);
}
});
}
构建
JS 渲染入口与浏览器端代码通常共用同一 esbuild 构建流程,通过环境变量区分:
// package.json
{
"scripts": {
"build": "NODE_ENV=production node build.js",
"build:jsrender": "JSRENDER=true NODE_ENV=production node build.js"
}
}
构建输出到 templates/everkm-render.js。主题打包时该文件随 templates/ 一起发布。
开发调试
everkm-publish serve \
--work-dir ../ \
--theme youlog \
--theme-dev
# 前端 watch(在 theme/youlog/ 目录下)
pnpm run dev:jsrender
与 Tera 的关系:JsRender 不是 Tera 的子集,而是对等的渲染后端。主题可以同时提供 Tera 模板(.html)和 JS 渲染(everkm-render.js),Tera 优先;也可以仅维护 JS 版。
一切实现细节可参考官方主题 theme-youlog{target="_blank"}。
站点扩展模板
除主题自带模板外,可在 __everkm/extend/templates/ 放置站点级模板覆盖。预览时 extend/ 下的模板与 assets 变更会自动重载。详见 快速开始。
主题 UI 文案与语言包见 多语言。