【Bug已解决】CI is failing for pages build and deployment: Liquid syntax error: ‘if‘ tag was never closed

📅 2026/7/21 22:01:56
【Bug已解决】CI is failing for pages build and deployment: Liquid syntax error: ‘if‘ tag was never closed
【Bug已解决】CI is failing for pages build and deployment: Liquid syntax error: if tag was never closed in source/chat_templates.md 解决方案一、现象长什么样仓库用 GitHub PagesJekyll 构建托管文档其中source/chat_templates.md是一篇讲解聊天模板的文档。某次提交后Pages 的 CIbuild-and-deploy直接红了Liquid syntax error: if tag was never closed in source/chat_templates.md更具体时还会看到Error: The if tag was never closed. Expected {% endif %} but found EOF (or another block).现象特征只影响 Pages 构建不影响 pytest / 单元测试——所以代码 CI 全绿只有部署 CI 红文档里我们确实展示了聊天模板的源码片段里面包含{% if ... %}这样的控制结构因为 chat template 本身就是 Jinja/Liquid 风格模板我们以为md 里的代码块会被原样保留但 Jekyll 在构建时会先把整篇 md 当Liquid 模板预解析{% if %}被当成真正的 Liquid 指令去执行找不到{% endif %}就报错。这是典型的文档里展示模板语法却没转义被构建器当成真指令的坑。二、背景GitHub Pages 用 Jekyll 把 Markdown 渲染成静态站。Jekyll 在渲染前会先用Liquid模板引擎处理整篇文档凡是{{ }}输出和{% %}标签/控制流都会被 Liquid 解析执行。问题在于我们写技术文档时经常要在代码块里展示模板自身的语法比如 chat template 里的{% if message.role user %} {{ message.content }} {% endif %}在普通 Markdown 渲染器里这只是一个代码块原样显示。但在 Jekyll/Liquid 眼里整篇文档都是 Liquid 输入{% if %}被当成真的要执行的条件分支于是它去找{% endif %}——如果文档里只展示了{% if %}开头或代码片段里 endif 被截断/没配对Liquid 就报if tag was never closed。更坑的是即使你写了{% endif %}如果代码块里还有{{ variable }}Liquid 会尝试去渲染这个变量变量不存在就渲染成空或报错文档展示就失真。三、根因根因一句话文档chat_templates.md中的代码块包含了 Liquid 语法的{% if %}/{{ }}但没用{% raw %}...{% endraw %}包裹Jekyll 构建时把它们当成真正的 Liquid 指令解析因{% if %}未闭合或变量未定义而报错导致 Pages 部署 CI 失败。具体未有转义展示模板语法的代码块直接写{% if %}没包{% raw %}Liquid 预解析Jekyll 把全文档当 Liquid 输入遇到{% if %}进入条件分支模式未闭合代码片段里{% if %}后没有成对的{% endif %}文档只摘录了一部分或 endif 在别处Liquid 走到 EOF 仍开着 if → 报错只在 Pages 构建暴露pytest 不碰 md 渲染所以代码 CI 绿、部署 CI 红容易漏。本质是文档内容与构建器模板语言撞车——你展示的语法恰好是构建器要执行的语法。四、最小可运行复现下面用纯 Python 模拟未转义的 Liquid 标签导致解析失败的机制用简单状态机判断 if/endif 配对import re def simulate_liquid(tokens): 极简 Liquid 解析跟踪 {% if %} / {% endif %} 配对。 depth 0 for t in tokens: if t {% if %}: depth 1 elif t {% endif %}: depth - 1 if depth 0: raise SyntaxError(endif 多于 if) if depth ! 0: raise SyntaxError(if tag was never closed) def tokenize(text): return re.findall(r\{\% if \%\}|\{\% endif \%\}|[^{}], text) def demo(): # 文档里只展示了 if 开头没有 endif - 解析失败 bad 示例: {% if message.role user %} {{ message.content }} try: simulate_liquid(tokenize(bad)) except SyntaxError as e: print(Pages 构建报错, e) # 用 {% raw %} 包裹后Liquid 不再解析内部 - 安全 good {% raw %}{% if message.role user %} {{ message.content }}{% endraw %} # raw 块内部被当作纯文本不进入 if/endif 计数 print(raw 包裹后内部不被 Liquid 解析构建通过) if __name__ __main__: demo()输出Pages 构建报错 if tag was never closed raw 包裹后内部不被 Liquid 解析构建通过第一行精确复现了线上的 Liquid 报错第二行说明解决方向——用{% raw %}把展示用的模板语法包起来Liquid 就当它纯文本不再尝试执行。五、解决方案第一层用{% raw %}包裹含 Liquid 语法的代码块第一层最直接在文档里凡是展示{% %}/{{ }}的代码都用{% raw %}...{% endraw %}包一层正确的写法chat_templates.md {% raw %} jinja {% if message.role user %} {{ message.content }} {% endif %}{% endraw %}注意顺序{% raw %} 本身也是 Liquid 标签但它告诉解析器接下来的内容原样输出别解析里面的 {% %}/{{ }}。这样 - {% if %} / {% endif %} 在 raw 块内Liquid 不执行、不要求配对 - {{ message.content }} 也原样显示不会被当成变量渲染 - Pages 构建不再报 if tag was never closed。 修复后重新触发 Pages CI 应当绿。 ## 六、解决方案第二层把模板示例放进独立文件并用 include/highlight 第一层修好了单处但文档里可能多处展示模板语法容易漏包。第二层从结构上规避把要展示的模板源码放**独立文件**如 examples/chat_template.jinja在文档里用 Jekyll 的 {% highlight %} 或 {% include %} 引用且对 include 内容也加 raw 保护 markdown 在 chat_templates.md 里 {% raw %} {% highlight jinja %} {% include_relative examples/chat_template.jinja %} {% endhighlight %} {% endraw %}这样模板源码集中在examples/下不在 md 正文里裸写{% if %}引用时仍用{% raw %}包裹确保 include 的内容不被二次解析编辑模板示例时只改examples/文件md 不再直接含未转义标签漏包风险大降。如果某些静态站点生成器支持{% raw %}嵌套易错也可以改用代码块语言标注 front matter 关闭 Liquid见第三层。七、解决方案第三层在 front matter 关闭 Liquid / 加 CI 预检第三层从构建配置和 CI 双保险杜绝这类问题回潮在 md 顶部 front matter 关掉 LiquidJekyll 支持liquid: false--- title: Chat Templates liquid: false --- 正文里可以随意写 {% if %} / {{ x }}Jekyll 不再解析liquid: false让整篇文档跳过 Liquid 预解析最适合满篇都是模板语法的参考文档。CI 预检在 Pages 构建前加一步扫描文档里未配对的{% if %或裸{{且不在 raw 块内提前失败并给出文件行号import re, sys, pathlib def check_raw_balance(path: str) - bool: text pathlib.Path(path).read_text(encodingutf-8) # 去掉 {% raw %}...{% endraw %} 块其内部不需配对 text re.sub(r\{% raw %\}.*?\{% endraw %\}, , text, flagsre.DOTALL) opens len(re.findall(r\{% if , text)) closes len(re.findall(r\{% endif %\}, text)) if opens ! closes: print(f[FAIL] {path}: {opens} 个 if 开, {closes} 个 endif 闭 - 未闭合) return False return True def demo(): ok check_raw_balance(source/chat_templates.md) sys.exit(0 if ok else 1) if __name__ __main__: demo()CI 里跑python check_liquid.py把未闭合 if在部署前就拦下而不是等 Jekyll 构建时才红。八、落地建议如果你在 Pages 文档里展示模板语法建议短期把所有含{% %}/{{ }}的代码块用{% raw %}...{% endraw %}包裹。中期把模板示例抽到独立.jinja文件文档用 include raw 引用。长期最适合参考文档在 front matter 加liquid: false整篇跳过 Liquid。防护CI 加check_raw_balance预检未闭合 if 提前失败。验证本地bundle exec jekyll build跑一遍确认不再报 Liquid 错。九、排查清单如果 Pages 构建报 Liquid syntax error: if tag was never closed按顺序查定位文件行号CI 日志会给出具体 md 文件和大致位置。搜未配对的{% if %文档里展示的模板片段是否只有 if 没有 endif。确认是否有{% raw %}包裹展示{% %/{{ }的代码块是否转义。考虑liquid: false若文档满篇模板语法直接在 front matter 关掉 Liquid。抽独立文件把模板示例放.jinja用 include raw 引用。加 CI 预检check_raw_balance扫描未闭合 if部署前拦截。本地 build 验证bundle exec jekyll build确认绿。十、小结Pages 部署 CI 报Liquid syntax error: if tag was never closed根因是文档chat_templates.md在代码块里直接展示了 Liquid/Jinja 风格的模板语法{% if %}/{{ }}却没有用{% raw %}转义Jekyll 构建时把整篇文档当 Liquid 模板预解析遇到未闭合的{% if %}就报错。它只在 Pages 构建暴露、不影响单元测试所以容易漏到部署阶段才爆。修复分三层第一层用{% raw %}...{% endraw %}包裹所有含 Liquid 语法的展示代码让解析器原样输出第二层把模板示例抽到独立.jinja文件、用 include raw 引用从结构上降低漏包风险第三层在 front matter 加liquid: false跳过整篇 Liquid 解析并加check_raw_balanceCI 预检把未闭合 if在部署前拦截。核心心法是文档里要展示模板语言本身时必须让构建器知道这部分是内容、不是指令——用 raw 转义或关闭 Liquid否则你展示的语法会被当成要执行的指令构建必然失败。