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://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css" />

<style>
/* --- 1. 颜色变量:自适应主题 data-theme 属性 --- */
:root {
    --c-bg: #f6f8fa;       /* 亮色背景 */
    --c-text: #24292e;     /* 亮色文字 */
    --c-border: #e1e4e8;   /* 亮色边框 */
    --btn-bg: #d1d5da;
    --btn-text: #24292e;
    --tok-keyword: #d73a49;
    --tok-func: #6f42c1;
    --tok-str: #032f62;
    --tok-comment: #6a737d;
}

/* 当主题切换为深色时 (监听 data-theme 属性) */
html[data-theme="dark"] {
    --c-bg: #1e1e1e;       /* 暗色背景 */
    --c-text: #c9d1d9;     /* 暗色文字 */
    --c-border: #30363d;   /* 暗色边框 */
    --btn-bg: #4b5263;
    --btn-text: #ffffff;
    --tok-keyword: #ff7b72;
    --tok-func: #d2a8ff;
    --tok-str: #a5d6ff;
    --tok-comment: #8b949e;
}

/* --- 2. 代码框主体样式 --- */
div.code-toolbar > pre, 
.gh-content pre {
    background-color: var(--c-bg) !important;
    color: var(--c-text) !important;
    border: 1px solid var(--c-border) !important;
    border-radius: 6px !important;
    padding: 1.5em !important;
    margin: 1.5em 0 !important;
    font-family: 'Fira Code', Consolas, monospace !important;
    line-height: 1.6 !important;
    transition: background-color 0.2s ease, color 0.2s ease;
}

div.code-toolbar > pre code {
    background: transparent !important;
    color: inherit !important;
}

/* --- 3. 语法高亮映射 --- */
.token.comment { color: var(--tok-comment) !important; font-style: italic; }
.token.keyword, .token.operator { color: var(--tok-keyword) !important; }
.token.function { color: var(--tok-func) !important; }
.token.string { color: var(--tok-str) !important; }
.token.punctuation { color: var(--c-text) !important; opacity: 0.7; }

/* --- 4. 复制按钮:强制常驻右上角 --- */
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: 10;
}

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

/* --- 5. 彻底隐藏页脚 --- */
.gh-foot, 
.site-footer, 
footer, 
.gh-copyright, 
.gh-powered-by { 
    display: none !important; 
}
</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://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/toolbar/prism-toolbar.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/contrib/auto-render.min.js"></script>

<script>
document.addEventListener("DOMContentLoaded", function() {

    // --- 模块 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'); // 兼容部分第三方主题
                html.setAttribute('class', html.getAttribute('class') + ' dark-mode'); // 暴力追加
            } else {
                // 系统是亮色:强制移除暗色类名
                html.classList.remove('dark-mode');
                html.setAttribute('data-theme', 'light');
                // 暴力清理 class 字符串中的 dark-mode
                html.setAttribute('class', html.getAttribute('class').replace('dark-mode', '').trim());
            }
        };

        // 1. 初始化执行
        applyTheme(mediaQuery);
        
        // 2. 监听系统设置变化 (无需刷新页面,即刻生效)
        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
            
            // 解码 HTML 实体
            const textArea = document.createElement('textarea');
            textArea.innerHTML = clean;
            return `$$ ${textArea.value} $$`;
        });

        content.innerHTML = html;

        setTimeout(function() {
            if (typeof Prism !== 'undefined') 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