dby-theme

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

公众号排版主题工作室(都爆鸭)

WeChat Official Account Layout Theme Studio (Doubaoya)

排版是一份声明式的
themeJson
(配色 palette + 每个标签的 inline-style 模板),渲染器按它把 Markdown 确定性地渲成公众号内联样式 HTML。按描述生成 / 修改合法的 themeJson, 本地自检 + 预览,再存回服务端设为默认;发文钉了
--theme
/
config.mdTheme
时再落一份本机文件。 渲染 / 存主题都免费不扣点。

Layout is a declarative
themeJson
(palette + inline-style template for each tag), and the renderer deterministically converts Markdown into WeChat inline-style HTML according to it. Generate / modify a valid themeJson based on the description, run local self-check + preview, then save it back to the server as the default; when publishing with
--theme
/
config.mdTheme
pinned, also write a local file. Rendering / saving themes is free, no credits deducted.

⚠️ 四条硬红线(先读,全程守住)

⚠️ Four Hard Red Lines (Read First, Hold Throughout)

红线 1 · 微信兼容:标题装饰只能挂在有文字的元素自身

Red Line 1 · WeChat Compatibility: Title decorations must be attached to the element with text itself

公众号编辑器会 strip 掉没有文字、纯靠 inline-block 宽高 + background 撑色块的空
<section>
/
<span>
。 标题引导条 / 强调竖条 / 下划线必须作为 inline style 直接写在标题元素自身,绝不用独立的空装饰块。
  • ✅ 对:
    elements.h2.style
    里直接写
    border-left:4px solid {{accent}};border-bottom:1px solid {{border}};padding:0 0 8px 11px;
  • ❌ 错:用
    wrapBefore
    注入一个空的
    <section style='width:32px;height:4px;background:…'>
    当色条(无文字 → 被 strip)。
The WeChat editor strips empty
<section>
/
<span>
elements that have no text and rely purely on inline-block width/height + background color blocks
. Title guide bars / emphasis vertical bars / underlines must be written directly as inline styles on the title element itself, never as separate empty decoration blocks.
  • ✅ Correct: write
    border-left:4px solid {{accent}};border-bottom:1px solid {{border}};padding:0 0 8px 11px;
    directly in
    elements.h2.style
  • ❌ Wrong: use
    wrapBefore
    to inject an empty
    <section style='width:32px;height:4px;background:…'>
    as a color bar (no text → stripped).

红线 2 · 微信兼容:别给整篇套
decorations.articleWrap
外框

Red Line 2 · WeChat Compatibility: Don't wrap the whole article in a
decorations.articleWrap
outer frame

decorations.articleWrap
包整篇正文的浅底圆角卡片在公众号里会变成一道突兀的边框。
articleWrap.before
/
after
留空字符串;「呼吸感」用段间距(
margin
/
line-height
)和局部卡片(如 blockquote 圆角卡)实现。
decorations.articleWrap
, which wraps the entire article body in a light rounded card, will turn into a jarring border in the WeChat editor. Leave
articleWrap.before
/
after
as empty strings; achieve "breathing room" using paragraph spacing (
margin
/
line-height
) and local cards (e.g., blockquote rounded cards).

红线 3 · 安全:themeJson 会内联进公众号草稿,服务端会拒收不合法主题

Red Line 3 · Safety: themeJson will be inlined into WeChat drafts, and the server will reject invalid themes

服务端
POST
/
PUT
/
render
都会
validateTheme
,不过直接 400。生成后先跑
scripts/validate-theme.mjs
自检——具体校验规则(顶层键、 palette/page 必填字段、style 禁写清单、体积上限)以它为准,不在这里另存一份会漂移的清单。
微信编辑器官方规范另有八条「不拒但走样」的写法(
position
、
text-align:start
、固定像素宽、自定义字体栈、文字底下铺渐变…), 清单与可读性基线见
references/theme-schema.md
末尾;校验器对其中四条打 warning。
The server's
POST
/
PUT
/
render
all call
validateTheme
, returning 400 if invalid. After generating, first run
scripts/validate-theme.mjs
for self-check — the specific validation rules (top-level keys, required fields in palette/page, forbidden style list, size limits) follow that script, and we don't keep a separate drifting list here.
WeChat editor's official spec also has eight "not rejected but will look wrong" patterns (
position
,
text-align:start
, fixed pixel widths, custom font stacks, gradients under text…). The list and readability baseline are at the end of
references/theme-schema.md
; the validator warns on four of them.

红线 4 · 数据安全:动手改前必须落盘备份,没有备份不改

Red Line 4 · Data Safety: Must back up to disk before modifying; no backup, no modification

改主题是就地覆写(
PUT
/
POST + isDefault:true
),服务端不保留历史版本,改坏了没有第二处能找回。 第 1 步的 GET 命令已经把落盘备份写进命令本身——照抄不删;备份文件路径记下来,改坏了照
references/deploy-scope.md
「回滚」一节把它 PUT 回去。

Changing a theme is an in-place overwrite (
PUT
/
POST + isDefault:true
). The server does not keep history versions, and if you break it, there is no second place to recover. The GET command in Step 1 already includes disk backup in the command itself — copy it as-is, don't delete. Note the backup file path; if you break it, PUT it back according to the "Rollback" section of
references/deploy-scope.md
.

契约:服务端主题 CRUD + 渲染(以 doubaoya.com 主仓 render-routes.ts 为准)

Contract: Server-side Theme CRUD + Rendering (as per doubaoya.com main repo render-routes.ts)

⚠️ 先读
dby-gateway/references/protocol.md
再发请求(鉴权、密钥怎么拿、 信封与报错码全在那一份)。本 skill 自己的六条主题路由(
GET/POST /api/wechat/theme
、
GET /api/wechat/themes
、
POST /api/wechat/render
、
PUT
/
DELETE /api/wechat/theme/:id
) 的请求体与返回 → 读
references/crud.md
,不需要就别读。
bash
export DOUBAOYA_API_KEY="dyh_你的密钥"
BASE="${DOUBAOYA_BASE_URL:-https://doubaoya.com}"
AUTH="Authorization: Bearer $DOUBAOYA_API_KEY"
render 的主题解析优先级
themeJson
(传了就用这份)>
themeId
> 我的默认主题 > 兜底
benya-clean
,所以预览编辑中的主题就传
themeJson
; 一人一默认,
isDefault:true
时服务端在事务里把我其他主题的
isDefault
置反。
⚠️ Read
dby-gateway/references/protocol.md
first before making requests (auth, how to get the key, envelope and error codes are all in that one file). For this skill's six theme routes (
GET/POST /api/wechat/theme
,
GET /api/wechat/themes
,
POST /api/wechat/render
,
PUT
/
DELETE /api/wechat/theme/:id
), the request bodies and responses are in
references/crud.md
; don't read unless needed.
bash
export DOUBAOYA_API_KEY="dyh_你的密钥"
BASE="${DOUBAOYA_BASE_URL:-https://doubaoya.com}"
AUTH="Authorization: Bearer $DOUBAOYA_API_KEY"
Theme resolution priority for render:
themeJson
(if passed, use this) >
themeId
> my default theme > fallback
benya-clean
. So pass
themeJson
when previewing a theme being edited
; one default per user, when
isDefault:true
the server transaction resets
isDefault
on other themes of mine.

闭环步骤

Closed-loop Steps

1. 读起点主题并落盘备份(红线 4,照抄不删)

1. Read starting theme and back up to disk (Red Line 4, copy as-is don't delete)

bash
undefined
bash
undefined

a) 读我当前的默认主题(有就在它上面改)并强制落盘备份——主题体在 data.theme.themeJson

a) Read my current default theme (modify on top of it if exists) and force disk backup — theme body is in data.theme.themeJson

BACKUP="/tmp/dby-theme-backup-$(date +%s).json" curl -s -H "$AUTH" "$BASE/api/wechat/theme" | tee "$BACKUP" echo "已备份到 $BACKUP —— 改坏了见 references/deploy-scope.md「回滚」一节"
BACKUP="/tmp/dby-theme-backup-$(date +%s).json" curl -s -H "$AUTH" "$BASE/api/wechat/theme" | tee "$BACKUP" echo "已备份到 $BACKUP —— 改坏了见 references/deploy-scope.md「回滚」一节"

b) 或列出内置主题,挑一个复制起步(推荐 benya-clean——已按红线 1 / 2 改造过)

b) Or list built-in themes, pick one to copy as a starting point (benya-clean recommended — already adapted per Red Lines 1 / 2)

curl -s -H "$AUTH" "$BASE/api/wechat/themes"

> 本 skill 也自带一个 WeChat-safe 起手模板 [`themes/theme.example.json`](./themes/theme.example.json)(暖橘编辑风,复制改配色即可)。
curl -s -H "$AUTH" "$BASE/api/wechat/themes"

> This skill also includes a WeChat-safe starting template [`themes/theme.example.json`](./themes/theme.example.json) (warm orange editing style; just copy and change the palette).

2. 按用户口语描述改 themeJson

2. Modify themeJson according to the user's spoken description

把「换成暖橘色 / 标题要竖条 / 引用做成卡片 / 字再大一点 / 排版长得像某某号」翻译成合法 themeJson。 字段全貌见
references/theme-schema.md
;守住上面四条红线。改配色最省事:只动
palette
八个键
。
Translate "make it warm orange / titles need a vertical bar / quotes as cards / make text bigger / layout looks like XXX account" into a valid themeJson. Field overview in
references/theme-schema.md
; hold the four red lines above. The easiest way to change colors: only touch the eight
palette
keys
.

3. 本地先自检

3. Local self-check first

把生成的主题写成本地文件,过本 skill 自带的零依赖校验器:
bash
node scripts/validate-theme.mjs /tmp/my-theme.json   # 有硬错误按提示改;exit 0 = 合法
Write the generated theme to a local file and run this skill's zero-dependency validator:
bash
node scripts/validate-theme.mjs /tmp/my-theme.json   # follow hints for hard errors; exit 0 = valid

4. 预览(改一版看一版)

4. Preview (preview after each change)

API 渲染 → 存本地 .html 打开:
bash
undefined
API render → save local .html and open:
bash
undefined

用编辑中的 themeJson 渲染样例文章,拿回 HTML 存本地打开肉眼看

Render sample article with the themeJson being edited, get HTML back, save locally and open visually

curl -s -H "$AUTH" -H 'Content-Type: application/json'
-d "$(jq -n --arg md "$(cat sample.md)" --slurpfile t /tmp/my-theme.json
'{markdown:$md, title:"预览", themeJson:$t[0]}')"
"$BASE/api/wechat/render" | jq -r '.data.html' > /tmp/preview.html open /tmp/preview.html # macOS;别的平台用浏览器打开

> `sample.md` 别带 `#` 一级标题:`title` 会被插成 h1,正文再有就显示两个大标题。
> render 返回的 HTML 与最终存进草稿的正文**逐字一致**。`data.warnings` 若有(如未知 `{{token}}`)一并看一眼。

→ 本机已装 `dby-publish` 且想在本机「改文件重跑即换肤」地预览,读 `references/live-preview.md`,
不需要就别读。改 themeJson 本身以上面 API 渲染那条路为准。
curl -s -H "$AUTH" -H 'Content-Type: application/json'
-d "$(jq -n --arg md "$(cat sample.md)" --slurpfile t /tmp/my-theme.json
'{markdown:$md, title:"预览", themeJson:$t[0]}')"
"$BASE/api/wechat/render" | jq -r '.data.html' > /tmp/preview.html open /tmp/preview.html # macOS; use browser on other platforms

> Don't include a `#` first-level heading in `sample.md`: the `title` will be inserted as h1, and having one in the body would show two big headings.
> The HTML returned by render is **character-for-character identical** to the final draft body. Check `data.warnings` (e.g., unknown `{{token}}`) if present.

→ If local `dby-publish` is installed and you want to preview by "modifying file then re-run to switch skins", read `references/live-preview.md`; don't read unless needed. For modifying themeJson itself, the API rendering path above is authoritative.

5. ⚠️ 落地生效范围(两条渲染路的主题源)

5. ⚠️ Scope of effect (theme source for the two rendering paths)

→ 主题调好、要存回去时读
references/deploy-scope.md
——它回答「存了之后哪条路会变、 哪条不会」,并给出存回服务端 / 落本机文件 / 改坏了回滚的完整命令。
存回服务端设为默认是主路(
POST
/
PUT
+
isDefault:true
):doubaoya.com 网页排版 工作室、
POST /api/wechat/render
、以及未钉本机主题的
dby-publish
发文
都读它。 仍需落一份本机 theme 文件的只有三种情形:发文时钉着
--theme
/
config.mdTheme
、 离线或拉取失败要兜底、用的是不带自动拉取的旧版 pipeline。 落完向用户汇报时如实说清各自的生效范围,别混着说。
→ Read
references/deploy-scope.md
when the theme is tuned and ready to save back — it answers "after saving, which path changes and which doesn't", and gives complete commands for saving to server / local file / rollback if broken.
Saving back to server as default is the main path (
POST
/
PUT
+
isDefault:true
): doubaoya.com web layout studio,
POST /api/wechat/render
, and
dby-publish
publishing that does not pin a local theme
all read from it. A local theme file is only needed in three cases: publishing with
--theme
/
config.mdTheme
pinned, offline or fetch failure fallback, or older pipeline without auto-fetch. When reporting to the user, truthfully clarify the scope for each, don't mix them up.

themeJson 结构(速查)

themeJson Structure (Quick Reference)

→ 真要动手写 / 改 themeJson 的字段时读
references/theme-schema.md
(top-level 键、
elements
支持的标签、
{{token}}
插值、
li.marker
/
img.figureStyle
/
hr.html
这些位置), 不需要就别读。只换配色的话改
palette
八个键就够
,其余用
{{token}}
自动跟随。
主题契约以本包
scripts/validate-theme.mjs
为准(跑它,别照文档记); 本 skill 的
references/theme-schema.md
+
themes/theme.example.json
足够独立完成一次改主题。

→ Read
references/theme-schema.md
when actually writing/modifying themeJson fields (top-level keys, supported tags in
elements
,
{{token}}
interpolation, positions like
li.marker
/
img.figureStyle
/
hr.html
); don't read unless needed. For just changing colors, editing the eight
palette
keys is enough
, the rest follows automatically via
{{token}}
.
The theme contract is based on this package's
scripts/validate-theme.mjs
(run it, don't memorize the docs); this skill's
references/theme-schema.md
+
themes/theme.example.json
are sufficient to independently complete a theme modification.

下一步(先看用户要的终态是什么)

Next Steps (First Determine the User's Desired End State)

别默认往下推——用户要的终态停在哪一档,就在哪一档收手:
用户要的终态到这一步够不够下一步
只要把默认排版改成自己想要的样子✅ 够了,到这里就结束(以后每次渲染自动生效)—
还要用这套主题排版某篇正文、存进自己公众号草稿箱不够
dby-publish
(存回服务端的默认排版它会自动拉;本机主题文件用
--theme
指过去)
已有排好版的图文,只想让它进草稿箱不够
dby-publish
正文还没写不够
dby-api
(拉同主题爆文样本再写正文,走 api.gzh.hotArticle)
正文没过违禁词不够
dby-banned-words
(三平台一次比对,出安全改写版)
说不清要到哪一步—
dby
(公众号飞轮的逐跳导航)
Don't default to pushing further — stop at the tier where the user's desired end state lies:
User's Desired End StateIs This Step Enough?Next Step
Only need to change the default layout to what I want✅ Enough, stop here (takes effect automatically for every render from now on)—
Also want to layout a specific article with this theme and save it to my WeChat draft boxNot enough
dby-publish
(it auto-fetches the server-side default layout; point local theme file via
--theme
)
Already have typeset content, just want it into the draft boxNot enough
dby-publish
Content not written yetNot enough
dby-api
(pull same-topic viral samples then write content, via api.gzh.hotArticle)
Content hasn't passed banned-word checkNot enough
dby-banned-words
(cross-check three platforms, produce safe rewrite)
Unclear about the next step—
dby
(hop-by-hop navigation for the WeChat flywheel)