AO3 HTML 排版指南:哪些标签能用,为什么有些会失效

如果你在 AO3 发过文,可能遇到过这些情况:本地看起来好好的排版,贴到 AO3 后突然变样,颜色没生效;聊天体在手机上挤成一团;预览时 AO3 还自动给你补了一堆 <p><br>

AO3 并不是一个“随便贴网页代码”的编辑器。它允许作者用一部分 HTML 和 CSS 做排版,但会先把内容送进 parser 和 sanitizer:parser 尽量补齐、整理不完整的代码,sanitizer 会移除不允许或有风险的部分。AO3 官方 HTML FAQ 已经直接说明了这一点。1

这篇不是开发教程,只是想解决一个作者经常会遇到的问题:为什么有些排版会失效;如果你希望作品可下载、能在不同设备上打开,排版时最好把力气花在哪里。正文会参考 AO3 官方 FAQ,也会提到 AO3 公开源码里能看到的一些实现细节,但重点还是作者写文时怎么判断。

AO3 为什么要清洗 HTML?

原因很简单:AO3 的作品页不是作者自己的私人网页,而是读者会直接打开的公共档案页面。

如果 AO3 允许任意 HTML、CSS 和 JavaScript,每一篇作品都可能影响整页:追踪读者、插入外部脚本、遮挡界面,甚至破坏站点结构。对一个长期保存用户投稿的档案馆来说,这个风险太高。

所以 AO3 用的是“允许名单”逻辑。不是“除了危险的都可以”,而是“只有名单里的才可以”。官方 HTML FAQ 列出了允许的 tags 和 attributes;源码里的 sanitizer_config.rb 也能看到类似配置:ablockquotebrdivemimgolpspantablestrongul 等元素可以保留,但属性和链接协议都有范围。12

很多排版失效并不是因为你“写错了”,而是 AO3 从一开始就不允许那段代码进入最终页面。

哪些 HTML 更稳?

从作者角度看,最稳的还是基础语义标签。

比如:

  • <p>:段落
  • <br>:换行
  • <strong> / <b>:加粗
  • <em> / <i>:斜体
  • <blockquote>:引用
  • <hr>:分隔线
  • <ul><ol><li>:列表
  • <a href="...">:链接
  • <img src="...">:图片
  • <ruby><rt><rp>:注音 / ruby 标注
  • <table><tr><td><th>:表格

这些标签在官方 FAQ 和源码允许名单里都能看到。它们不一定让页面看起来很“高级”,但胜在耐用:移动端能读,下载成 EPUB 后不太容易崩,读者换皮肤或关闭作者样式时,内容本身还在。

对中文作者来说,ruby 值得多看一眼。如果你要写汉字注音、日语假名、术语读音,AO3 允许 <ruby><rt><rp> 这一组标签,比把括号硬塞进正文里更干净。

为什么 script、iframe、style 会消失?

因为它们不在普通作品 HTML 的安全范围里。

在 AO3 源码的 sanitizer 配置里,scriptiframestylesvgmathnoscript 等会被列入 remove_contents。也就是说,它们不只是标签会被删,里面的内容也可能一起被移除。2

这会影响很多人常见的写法。

如果你把下面这种东西贴进作品正文:

1
2
3
4
5
<script>alert("hi")</script>
<iframe src="..."></iframe>
<style>
  .chat { color: red; }
</style>

它不会像普通网页那样运行。AO3 会把这些东西清掉。想做样式,就不要把 <style> 写进正文,而应该用 Work Skin;想插入外部网页,也不能靠 iframe 嵌进去。

这是安全边界。读者打开作品页时,不应该执行陌生作者塞进来的脚本。

为什么 <3 有时也会出问题?

在 HTML 里,< 会被当成标签开始。比如 <em> 是斜体标签,<p> 是段落标签。那作者在正文里写 <3 呢?

AO3 源码的 HtmlCleaner 里有一行专门处理这个问题:它会把 <3 转成 &lt;33

也就是说,AO3 知道很多作者会写这个小心心。它没有简单粗暴地把所有 < 都当成错误,而是在清洗前先保护了一些常见输入。

不过这也提醒我们:如果你要在正文里显示真正的小于号,最好写成 &lt;;显示大于号,可以写成 &gt;。官方 HTML FAQ 也建议这样做,因为 < 可能会让 parser 以为你开始写 HTML 了。1

Work Skin 能做什么?

如果基础 HTML 不够用,就轮到 Work Skin。

AO3 官方 Skins FAQ 说,skin 是用 CSS 改变 AO3 或单篇作品显示方式的 stylesheet;Work Skin 负责改变某一篇作品的呈现。创建 Work Skin 后,作者还要在作品里加对应的 HTML class,CSS 才知道该作用到哪里。4

这就是为什么很多聊天体、短信体、论坛体会长这样:

1
2
3
4
<div class="phone">
  <p class="message left">你到了吗?</p>
  <p class="message right">还在路上。</p>
</div>

然后在 Work Skin 里写:

1
2
3
4
5
6
7
8
.phone {
  max-width: 30em;
}

.message {
  padding: 0.5em;
  border-radius: 0.5em;
}

普通 HTML 负责“这是什么内容”,Work Skin 负责“它看起来像什么”。

为什么 Work Skin 也不能随便写 CSS?

Work Skin 不是无限制 CSS。

AO3 官方 Skins FAQ 说明,自定义 skin 只能使用有限的一组 CSS properties 和 values。源码里的 CssCleaner 也是这个思路:解析 CSS,检查 property 是否支持,再检查 value 是否符合允许的颜色、数字、单位、函数、URL 等格式。不符合要求的属性或值会被清掉,并给出错误。45

WorkSkin 模型里还有一个很关键的细节:保存 Work Skin 时,AO3 会给选择器加上 #workskin 前缀。6

这对作者很重要。你的 Work Skin 默认只能影响作品正文区域,而不是整个 AO3 页面。你写 .message,AO3 实际会让它变成类似 #workskin .message 的作用范围。这样 CSS 就不会跑出去改导航、评论区、kudos 按钮或整个站点界面。

源码里还限制了几种容易出事的 CSS:Work Skin 不允许 CSS custom properties,也就是 --name 这种变量定义;不允许 var();也禁止 position: fixed6

AO3 没有给每个作者发一个完整网页沙盒,它只开放一小部分样式,用来改变作品正文的呈现。

为什么有些排版预览正常,发布后仍然不稳?

因为读者的阅读环境不一样。

Work Skin 默认会展示给读者,但读者可以选择隐藏作者样式。AO3 官方 Skins FAQ 说明,读者可以在单篇作品页面点击 “Hide Creator’s Style”,也可以在 Preferences 里勾选 “Hide work skins”,让所有作品隐藏自定义 Work Skin。4

所以有一个很实用的原则:不要把关键信息只放在样式里。

比如,下面这种写法就不太稳:

1
2
<p class="red">这句话代表 A 角色。</p>
<p class="blue">这句话代表 B 角色。</p>

读者一旦关闭 Work Skin,颜色没了,A/B 就分不出来了。

更稳的写法,是把身份写进文本结构里:

1
2
<p><strong>A:</strong>你到了吗?</p>
<p><strong>B:</strong>还在路上。</p>

聊天体、论坛体尤其需要注意这一点。好看的排版可以加分,但作品的基本信息最好不要只靠颜色、位置、背景图或浮动来传达。

为什么复杂排版下载后会变样?

AO3 的作品不只在网页里被阅读。很多读者会下载 EPUB、MOBI、PDF 或 HTML,也会用手机阅读器、电子墨水屏、浏览器阅读模式打开。

复杂 Work Skin 在网页上可能很好看,但下载文件未必会完整保留同样的 CSS 效果。就算保留了,阅读器也可能不支持某些样式。

如果你的目标是“作品长期可读”,排版可以分成两层:

第一层是内容结构。用段落、引用、列表、加粗、角色名,把文字本身写清楚。

第二层才是视觉皮肤。用 Work Skin 增加聊天气泡、论坛框、颜色和间距。

这样即使第二层失效,第一层仍然能读。

对作者来说,最容易踩的坑

第一,把富文本编辑器里的“脏” HTML 直接贴进 AO3。

从 Word、飞书、微信公众号后台复制出来的内容,常常带着一堆 AO3 不需要的样式和属性。AO3 会清洗它们,但清洗后的结果不一定是你想要的。更稳的做法是:先贴纯文本,或者在本地整理成简单 HTML,再放进 AO3 预览。

第二,把 CSS 写进作品正文。

正文里应该放 HTML;CSS 应该放 Work Skin。把 <style> 放进正文,通常会被清掉。

第三,用颜色区分所有信息。

聊天体很容易靠颜色、左右位置、头像来区分角色。但读者关闭 Work Skin、下载 EPUB,或者使用不同站点皮肤时,这些区别可能消失。最好在文本里保留角色名、时间、场景提示。

第四,不预览。

AO3 官方 FAQ 也提醒,发布前最好预览 HTML。parser 可能会帮你补闭合标签、调整结构;如果标签嵌套有问题,预览时通常能看出来。1

一个实际例子:更稳的聊天体

如果你想写聊天体,最容易踩的坑是:只用颜色、左右位置或气泡样式区分角色。这样的排版在网页上可能很好看,但读者一旦隐藏 Creator’s Style,或者下载成 EPUB,角色关系就可能变得很难读。

更稳的做法,是先让 HTML 本身能读,再用 Work Skin 美化。

作品正文里可以这样写。注意每条消息本身都带着角色名,left / right 只是给 Work Skin 用来控制显示方向:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<div class="chatlog">
  <div class="chat-screen">
    <div class="chat-header">
      <div class="chat-title">群聊</div>
    </div>
    <div class="chat-body">
      <p class="chat-time">23:12</p>
      <p class="msg left"><strong class="speaker"><span class="colon"></span></strong><span class="bubble">你到了吗?</span></p>
      <p class="msg right"><strong class="speaker"><span class="colon"></span></strong><span class="bubble">还在路上。</span></p>
      <p class="msg left"><strong class="speaker"><span class="colon"></span></strong><span class="bubble">我在门口等你。</span></p>
    </div>
  </div>
</div>

Work Skin 里可以这样写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
.chatlog {
  max-width: 32em;
  margin: 1em auto;
  font-family: Arial, "Microsoft YaHei", sans-serif;
  color: #111;
}

.chat-screen {
  overflow: hidden;
  border: 1px solid #d2d2d2;
  background: #eaeaea;
  box-shadow: -0.35em 0.35em 0 #f4f4f4;
}

.chat-header {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 3.2em;
  background: #f8f8f8;
  border-bottom: 1px solid #d8d8d8;
}

.chat-title {
  font-weight: 700;
}

.chat-body {
  min-height: 15em;
  padding: 1em;
  background: #eaeaea;
}

.chat-time {
  text-align: center;
  color: #777;
  font-weight: 700;
}

.speaker {
  color: #5f5f5f;
  font-size: 0.9em;
  line-height: 1.2;
  margin: 0 0.35em 0.25em;
  white-space: nowrap;
}

.bubble {
  display: inline-block;
  max-width: 72%;
  padding: 0.58em 0.78em;
  border-radius: 0.35em;
  background: #fff;
  box-shadow: 0 0.12em 0.16em rgba(0, 0, 0, 0.16);
}

.msg.right .bubble {
  background: #9eea6a;
}

.msg {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  margin: 0.9em 0;
}

.msg.right {
  align-items: flex-end;
}

.msg .speaker {
  font-weight: 400;
}

.colon {
  display: none;
}

.msg .bubble {
  margin-top: 0.25em;
}

这个例子不追求“最像手机截图”。它的重点是:关掉 Work Skin 后,每条消息仍然是 林:你到了吗?周:还在路上。 这样的完整文本。气泡、左右位置和背景都只是第二层;冒号也保留在 HTML 里,只是在样式开启时隐藏。

notfun做了一个轻量预览页,可以用来练习这种基础 HTML + Work Skin 写法:AO3 HTML / Work Skin 预览器。聊天体模板和上面的结构是同一个思路:先保证关掉 Work Skin 也能读,再让样式负责气泡和位置。它不是 AO3 官方 sanitizer,也不能保证和 AO3 最终渲染完全一致;它只是帮你在发布前先看一眼:开关 Work Skin 以后,作品还能不能读。

这些规则对作者意味着什么?

从官方 FAQ 和 AO3 公开源码中的实现细节看,AO3 大概按这些原则处理作者输入。

比如:

  • HTML 不是黑名单,而是允许名单。
  • 普通字段不允许 inline CSS。
  • Work Skin 里的 CSS 会被清洗。
  • Work Skin 选择器会被限制在 #workskin 之内。
  • 不安全或难以清洗的标签会被移除。
  • sanitizer 版本变化后,AO3 还有批量重新清洗旧字段的任务。7

但这些实现细节也不能告诉我们一切。对作者来说,最实用的结论不是背允许名单,而是理解 AO3 的取舍:

它允许你排版,但不允许你把作品页变成一整个自定义网页。
它允许你美化作品,但读者可以关闭作者样式。
它允许你做复杂格式,但长期保存最依赖的仍然是清楚的文本结构。

如果你想做聊天体、论坛体,可以用 Work Skin,但不要让关键信息只存在于颜色和位置里。

如果你并不追求复杂样式,只是想做普通的加粗、斜体、链接、列表或分隔线,AO3 自带的富文本编辑器可能是更好的选择。它少了很多手写 HTML 的维护成本,也不太容易因为一个标签没闭合就把后面的段落带歪。

如果你希望作品多年后还能被人顺利读到,最稳的排版往往不是最炫的,而是样式被清掉以后,文本仍然成立。


  1. AO3 官方 FAQ “Formatting content on AO3 with HTML” 说明,AO3 会用 parser 和 sanitizer 处理作者输入,并列出允许的 HTML tags 和 attributes。https://archive.transformativeworks.org/faq/formatting-content-on-ao3-with-html?language_id=en ↩︎ ↩︎ ↩︎ ↩︎

  2. AO3 开源仓库 sanitizer_config.rb 中定义了 Archive 使用的 HTML 允许名单、属性、协议,以及会被移除内容的元素。https://github.com/otwcode/otwarchive/blob/master/config/initializers/gem-plugin_config/sanitizer_config.rb ↩︎ ↩︎

  3. AO3 开源仓库 html_cleaner.rb 中的 fix_bad_characterssanitize_valueadd_paragraphs_to_text 展示了 AO3 如何在保存/显示前处理文本、HTML 字段和段落。https://github.com/otwcode/otwarchive/blob/master/lib/html_cleaner.rb ↩︎

  4. AO3 官方 “Skins and Archive Interface FAQ” 说明,skins 是用 CSS 改变 AO3 或单篇作品呈现的 stylesheet;Work Skin 需要和作品中的 HTML class 配合使用,读者也可以隐藏 creator’s style。https://archive.transformativeworks.org/faq/skins-and-archive-interface?language_id=en ↩︎ ↩︎ ↩︎

  5. AO3 开源仓库 css_cleaner.rb 展示了 Work Skin / Skin CSS 的解析、属性和值清洗逻辑,包括对支持属性、支持值、URL、字体和颜色的检查。https://github.com/otwcode/otwarchive/blob/master/lib/css_cleaner.rb ↩︎

  6. AO3 开源仓库 work_skin.rb 中,Work Skin 保存时会调用 clean_css_code 并添加 #workskin 前缀,同时禁止 CSS custom properties、var()position: fixedhttps://github.com/otwcode/otwarchive/blob/master/app/models/work_skin.rb ↩︎ ↩︎

  7. AO3 开源仓库 resanitize_batch_job.rb 展示了当 sanitizer 版本更新时,AO3 可以对允许 HTML 的字段进行批量重新清洗。https://github.com/otwcode/otwarchive/blob/master/app/jobs/resanitize_batch_job.rb ↩︎

0%