主题开发

2026-07-25

本文面向主题开发者。站点运营者安装与使用主题见 快速开始;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 workmake 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 代码库
  • demo Demo 地址
  • screenshots 主题截图 URL 列表(可选)
  • default_template 主题默认模板名称(可被 everkm.yaml#default_template 覆盖)
  • folders 主题预设的目录发布配置,字段与 everkm.yaml#folders 相同;站点仅写需要覆盖的 path 与字段(见 目录配置 · 合并规则
  • config 主题级配置,在模板中通过 __config 读取,与项目 everkm.yaml#config 合并(项目覆盖主题)

everkm-theme.yaml 不包含 inject_jsinject_cssredirects——这些属于站点运营与部署范畴,见 站点配置

folders 典型用途:预设与模板配套的 URL 前缀(如 url_slug: posts)、默认 templateurl_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 版。

页面上下文

renderPageprops 参数(对应 TypeScript PageContext):

字段类型说明
request_idstring本次请求唯一 ID,调用 everkm.* API 时必须传入
postPostItem | null命中文章时的文章元数据
breadcrumbsBreadcrumbResolved[]面包屑
configRecord<string, any>合并后的站点配置
qsRecord<string, any>当前目录的 query 合并结果
tpl_pathstring当前使用的模板路径
page_pathstring当前页面路径
langstring当前语言代码
hoststring | null主机名
env_is_previewboolean是否为预览模式

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 文案与语言包见 多语言