Ghost适配高级Markdown样式

建站运维 Feb 15, 2026

Ghost默认是不支持公式的代码块之类的高级markdown样式的。它仅支持markdown-it插件能渲染的样式。

我们需要自定义样式的加载。

Ghost 作为一个 Headless CMS,其设计哲学是“极致的精简与轻量”。但这对于技术博主来说往往意味着阵痛:默认主题通常缺乏对复杂 Markdown 语法的支持。

本文复盘在 Ghost (v5.x) 中实现以下功能的完整技术路径:

  1. 表格:修复主题缺失的边框与排版。
  2. 代码块:实现 Prism.js 高亮、亮/暗色自适应、以及汉化的常驻复制按钮。
  3. 数学公式:解决 Ghost 后端解析器“污染” LaTeX 语法的核心难题。

核心思路:Code Injection

Ghost 官方不支持类似 WordPress 的“插件”上传。所有的前端样式修正,都必须通过 Settings -> Code Injection 中的 Site Header (CSS) 和 Site Footer (JS) 来实现。

一、 表格不渲染问题

在 Ghost 编辑器中,表格上方必须保留一个物理空行。如果表格紧挨着上一段文字,Ghost 的解析器会将其识别为普通文本。

二、 代码高亮与交互体验

Ghost 自带的高亮非常基础。为了实现“生产力级”的代码阅读体验,我们选择 Prism.js 方案,但需要解决三个问题。

1. 静态资源的 SRI 报错

直接引用 CDN (cdnjs) 时,如果携带 integrity 校验码,可能会因为节点文件差异导致浏览器拦截资源,表现为“样式全无”。

  • 对策:在 Code Injection 中移除 <link><script> 标签中的 integrity 属性。

2. 复制按钮

原生 Prism 插件的按钮通常需要 Hover 触发。

  • 修改 CSS opacity: 1 强制按钮常驻,提升移动端和非鼠标操作的体验。

三、复杂数学公式

这是排查过程中耗时最长的部分。Ghost 的后端解析器(Koenig)为了提升阅读兼容性,会抢在前端渲染引擎(KaTeX/MathJax)介入前,将 Markdown 特殊字符转义为 HTML 标签。

问题的本质:
大模型(如 ChatGPT、DeepSeek)输出的块级公式标准格式通常包含换行。Ghost 会将这些换行处理为 <br> 标签。更严重的是,它会将 LaTeX 中的指数符号 ^ 识别为 HTML 的上标语法。

  • 输入e^{-x^2}
  • Ghost 解析后e<sup>{-x</sup>2}
  • 结果:渲染器由于无法在 DOM 中找到合法的 LaTeX 字符串,导致渲染崩溃或产生排版错误。

解决方案:正则表达式外科手术
我们通过在 Site Footer 注入一段脚本,在页面加载的一瞬间(渲染引擎启动前),对内容区域进行“逆向还原”:

  1. 精准锁定:利用正则 /\$\$([\s\S]*?)\$\$/gm 仅处理块级公式,避免误伤正文。
  2. 暴力清洗
  • <sup></sup> 强制替换回 ^
  • <br> 还原为换行符。
  • 还原被转义的 &#36; (即 $)。
  1. 时序同步:使用 setTimeout(..., 50) 确保 DOM 树重绘完成后,再调用 renderMathInElement 进行渲染。

四、 亮暗模式

在处理代码块配色时,最初的方案是使用 CSS 媒体查询 @media (prefers-color-scheme: dark)。但在实际应用中,这种方案存在缺陷:它只能响应操作系统的设置,无法感知用户在博客页面上点击的主题切换按钮。

优化路径:
通过观察,我们确定 Ghost 主题(如 Casper、Edition)在切换亮暗模式时,会动态修改 <html> 标签的 data-theme 属性或 class

  • 对策:将 CSS 变量(--c-bg 等)直接绑定在 html[data-theme="dark"] 选择器下。
  • 效果:实现代码块背景色、高亮配色与主题状态的毫秒级同步,而非死板地跟随系统。

五、 审美上的收尾:常驻按钮与页脚阻断

  1. 常驻复制按钮
    为了提升交互感,我们将按钮从 opacity: 0 (hover触发) 修改为常驻显示,并固定在 div.code-toolbar 的右上角。这在移动端及平板设备上尤为重要。
  2. 页脚清理
    如果你追求极致的纯粹感,不希望显示默认的 "Published with Ghost" 字样,可以通过 CSS 暴力阻断。这种方法比修改主题 .hbs 模板更稳健,不会在更新主题时丢失配置。

六、 最终代码配置汇总

直接粘贴到你的代码注入对应位置即可。

1. Site Header


<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.9/katex.min.css" />

<style>
/* =========================================================
   1) 主题变量:默认亮色,data-theme="dark" 时切换暗色
   ========================================================= */
:root {
    --c-bg: #f6f8fa;
    --c-text: #24292e;
    --c-border: #e1e4e8;

    --btn-bg: #d1d5da;
    --btn-text: #24292e;

    --tok-comment: #6a737d;
    --tok-keyword: #d73a49;
    --tok-string: #032f62;
    --tok-number: #005cc5;
    --tok-boolean: #005cc5;
    --tok-function: #6f42c1;
    --tok-class: #e36209;
    --tok-builtin: #005cc5;
    --tok-variable: #e36209;
    --tok-property: #005cc5;
    --tok-operator: #d73a49;
    --tok-punctuation: #586069;
    --tok-regex: #22863a;
    --tok-constant: #005cc5;
    --tok-namespace: #6f42c1;
    --tok-decorator: #6f42c1;
}

html[data-theme="dark"] {
    --c-bg: #0d1117;
    --c-text: #c9d1d9;
    --c-border: #30363d;

    --btn-bg: #30363d;
    --btn-text: #f0f6fc;

    --tok-comment: #8b949e;
    --tok-keyword: #ff7b72;
    --tok-string: #a5d6ff;
    --tok-number: #79c0ff;
    --tok-boolean: #79c0ff;
    --tok-function: #d2a8ff;
    --tok-class: #ffa657;
    --tok-builtin: #79c0ff;
    --tok-variable: #ffa657;
    --tok-property: #79c0ff;
    --tok-operator: #ff7b72;
    --tok-punctuation: #c9d1d9;
    --tok-regex: #7ee787;
    --tok-constant: #79c0ff;
    --tok-namespace: #d2a8ff;
    --tok-decorator: #d2a8ff;
}

/* =========================================================
   2) 块级代码:只让 pre 做外壳,避免双层框
   ========================================================= */
div.code-toolbar > pre[class*="language-"],
.gh-content pre[class*="language-"],
pre[class*="language-"] {
    background-color: var(--c-bg) !important;
    color: var(--c-text) !important;
    border: 1px solid var(--c-border) !important;
    border-radius: 8px !important;
    padding: 1.25em 1.35em !important;
    margin: 1.5em 0 !important;
    font-family: "Fira Code", Consolas, Monaco, monospace !important;
    font-size: 0.95rem !important;
    line-height: 1.65 !important;
    overflow: auto !important;
    box-shadow: none !important;
    text-shadow: none !important;
    transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
    tab-size: 4 !important;
}

/* 内层 code 完全透明,避免第二层框 */
div.code-toolbar > pre[class*="language-"] > code[class*="language-"],
.gh-content pre[class*="language-"] > code[class*="language-"],
pre[class*="language-"] > code[class*="language-"] {
    display: block !important;
    background: transparent !important;
    color: inherit !important;
    border: 0 !important;
    border-radius: 0 !important;
    padding: 0 !important;
    margin: 0 !important;
    box-shadow: none !important;
    text-shadow: none !important;
    font: inherit !important;
    white-space: pre !important;
}

/* 避免主题对 pre code 追加块级底色 */
.gh-content pre code {
    background: transparent !important;
    border: 0 !important;
    padding: 0 !important;
    margin: 0 !important;
    box-shadow: none !important;
}

/* =========================================================
   3) 行内 code
   ========================================================= */
.gh-content :not(pre) > code,
.gh-content :not(pre) > code[class*="language-"] {
    background: rgba(127, 127, 127, 0.12) !important;
    color: var(--c-text) !important;
    border: 1px solid var(--c-border) !important;
    border-radius: 4px !important;
    padding: 0.15em 0.35em !important;
    font-family: "Fira Code", Consolas, Monaco, monospace !important;
    font-size: 0.92em !important;
    white-space: normal !important;
}

/* =========================================================
   4) token 配色:覆盖 Prism 大部分常见 token
   ========================================================= */
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
    color: var(--tok-comment) !important;
    font-style: italic;
}

.token.punctuation {
    color: var(--tok-punctuation) !important;
    opacity: 0.9;
}

.token.namespace {
    color: var(--tok-namespace) !important;
    opacity: 0.95;
}

.token.property,
.token.tag,
.token.constant,
.token.symbol,
.token.deleted {
    color: var(--tok-property) !important;
}

.token.boolean {
    color: var(--tok-boolean) !important;
}

.token.number {
    color: var(--tok-number) !important;
}

.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.inserted {
    color: var(--tok-string) !important;
}

.token.operator,
.token.entity,
.token.url {
    color: var(--tok-operator) !important;
}

.token.atrule,
.token.attr-value,
.token.function {
    color: var(--tok-function) !important;
}

.token.keyword {
    color: var(--tok-keyword) !important;
}

.token.class-name {
    color: var(--tok-class) !important;
}

.token.regex,
.token.important {
    color: var(--tok-regex) !important;
}

.token.builtin {
    color: var(--tok-builtin) !important;
}

.token.variable {
    color: var(--tok-variable) !important;
}

.token.annotation,
.token.decorator {
    color: var(--tok-decorator) !important;
}

.token.important,
.token.bold {
    font-weight: 700 !important;
}

.token.italic {
    font-style: italic !important;
}

.token.entity {
    cursor: help !important;
}

/* =========================================================
   5) Toolbar / Copy
   ========================================================= */
div.code-toolbar {
    position: relative !important;
}

div.code-toolbar > .toolbar {
    position: absolute !important;
    top: 10px !important;
    right: 10px !important;
    opacity: 1 !important;
    visibility: visible !important;
    z-index: 20 !important;
}

div.code-toolbar > .toolbar .toolbar-item {
    display: inline-flex !important;
}

div.code-toolbar > .toolbar button,
div.code-toolbar > .toolbar a,
div.code-toolbar > .toolbar span {
    background: var(--btn-bg) !important;
    color: var(--btn-text) !important;
    border: none !important;
    border-radius: 5px !important;
    padding: 4px 10px !important;
    font-size: 12px !important;
    font-weight: 600 !important;
    line-height: 1.4 !important;
    cursor: pointer !important;
    box-shadow: 0 1px 2px rgba(0,0,0,0.12) !important;
    text-decoration: none !important;
}

/* 复制按钮 hover */
div.code-toolbar > .toolbar button:hover,
div.code-toolbar > .toolbar a:hover {
    filter: brightness(0.96);
}

/* =========================================================
   6) 代码滚动条(可选)
   ========================================================= */
pre[class*="language-"]::-webkit-scrollbar {
    height: 10px;
    width: 10px;
}
pre[class*="language-"]::-webkit-scrollbar-thumb {
    background: rgba(127,127,127,0.35);
    border-radius: 999px;
}
pre[class*="language-"]::-webkit-scrollbar-track {
    background: transparent;
}

</style>

<script>
window.Prism = window.Prism || {};
window.Prism.manual = true;
window.Prism.plugins = window.Prism.plugins || {};
window.Prism.plugins.toolbar = window.Prism.plugins.toolbar || {};
window.Prism.plugins.toolbar.copy = '复制';
</script>

<script src="https://cdn.bootcdn.net/ajax/libs/prism/1.29.0/prism.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/prism/1.29.0/plugins/toolbar/prism-toolbar.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/prism/1.29.0/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js"></script>
<script defer src="https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.9/katex.min.js"></script>
<script defer src="https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.9/contrib/auto-render.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
    if (window.Prism && Prism.plugins && Prism.plugins.autoloader) {
        Prism.plugins.autoloader.languages_path =
            "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/";
        Prism.plugins.autoloader.use_minified = true;
    }
    // --- 模块 1: 强制主题跟随系统 (Auto-Dark Mode) ---
    function autoSyncTheme() {
        const html = document.documentElement;
        const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

        const applyTheme = (e) => {
            if (e.matches) {
                // 系统是暗色
                html.classList.add('dark-mode');
                html.setAttribute('data-theme', 'dark'); 
            } else {
                // 系统是亮色
                html.classList.remove('dark-mode');
                html.setAttribute('data-theme', 'light');
            }
        };

        // 初始化执行
        applyTheme(mediaQuery);
        // 监听系统设置变化
        mediaQuery.addEventListener('change', applyTheme);
    }
    
    autoSyncTheme();

    // --- 模块 2: DOM 深度清洗与渲染 (修复公式与高亮) ---
    function cleanAndRender() {
        const content = document.querySelector('.gh-content, .post-content, .post-full-content, article, .c-content');
        if (!content) return;

        let html = content.innerHTML;
        html = html.replace(/&#36;/g, '$'); // 解码转义的 $

        // 仅处理 $$ ... $$ 块,保护行内公式
        html = html.replace(/\$\$([\s\S]*?)\$\$/gm, function(match, inner) {
            let clean = inner.replace(/<br\s*\/?>/gi, '\n')  // 还原换行
                             .replace(/<\/?p[^>]*>/gi, '')   // 去除 P 标签
                             .replace(/<\/?sup[^>]*>/gi, '^') // 修复上标 e^{-x^2}
                             .replace(/<\/?sub[^>]*>/gi, '_') // 修复下标
                             .replace(/<\/?em[^>]*>/gi, '_')  // 修复错判的斜体
                             .replace(/<\/?span[^>]*>/gi, ''); // 去除 span
            
            const textArea = document.createElement('textarea');
            textArea.innerHTML = clean;
            return `$$ ${textArea.value} $$`;
        });

        content.innerHTML = html;

        setTimeout(function() {
            // 确保 Prism 对象存在再执行高亮
            if (window.Prism) {
                window.Prism.highlightAll();
            }
            if (typeof renderMathInElement !== 'undefined') {
                renderMathInElement(document.body, {
                    delimiters: [
                        {left: '$$', right: '$$', display: true},
                        {left: '\\[', right: '\\]', display: true},
                        {left: '$', right: '$', display: false},
                        {left: '\\(', right: '\\)', display: false}
                    ],
                    throwOnError : false,
                    ignoredTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code']
                });
            }
        }, 50);
    }

    cleanAndRender();
});
</script>

Tags