Ghost Doesn't Do Code. Fix That.

Ghost Doesn't Do Code. Fix That.

Ghost is a beautiful publishing platform. But paste a code block into a post and you get monospace text on a grey background — no colors, no language awareness, no way to copy. For a tech blog, that's a problem. Your readers will judge your code examples, and "grey blob of text" doesn't inspire confidence.

The fix: Prism.js for syntax highlighting, a custom copy-to-clipboard button, and Mermaid.js for diagrams. All self-hosted — no CDN dependencies, no third-party requests. Everything served from your own box.

Here's how to set it up in Ghost's Code Injection, without touching your theme files.


What You Get

graph LR
    Ghost[Ghost Post] -->|markdown| CodeBlock[Code Block]
    CodeBlock --> Prism[Prism.js]
    CodeBlock --> CopyBtn[Copy Button]
    CodeBlock --> Mermaid[Mermaid.js]
    Prism -->|colored syntax| Reader((Reader))
    CopyBtn -->|clipboard| Reader
    Mermaid -->|SVG diagram| Reader
FeatureWhat it does
Prism.jsLanguage-aware syntax highlighting
Copy buttonOne-click clipboard copy, hover to reveal
Mermaid.jsRenders diagrams from code blocks
Self-hostedZero external dependencies

Prerequisites

  • A running Ghost blog (see The 10-Minute Website Setup)
  • Shell access to your server
  • A Docker volume mount for custom assets (we'll set that up)

Step 1: Mount a Custom Assets Directory

Ghost themes live inside the Docker container. Modifying them directly means losing changes on every update. Instead, mount a host directory into the theme's asset path.

mkdir -p /srv/ghost-blog/custom-assets

Add the volume to your Ghost service in docker-compose.yml:

volumes:
  - ./custom-assets:/var/lib/ghost/content/themes/YOUR_THEME/assets/custom

Replace YOUR_THEME with your theme name — casperjournal, or whatever you're running. Restart:

docker compose up -d

Verify it works:

echo "test" > /srv/ghost-blog/custom-assets/test.txt
curl -s https://yourblog.example.com/assets/custom/test.txt

If you see test, the mount is live. Remove the test file.


Step 2: Download Prism.js

Self-hosted means downloading the files. Prism is modular — a core plus language components:

cd /srv/ghost-blog/custom-assets

# Core
wget -4 https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css
wget -4 https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js

# Languages — add what you need
wget -4 https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js
wget -4 https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-yaml.min.js
wget -4 https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js
wget -4 https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-nginx.min.js
wget -4 https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-docker.min.js

The -4 flag forces IPv4 — some CDNs have flaky IPv6 SSL on certain servers.

Need more languages? Browse the Prism CDN directory — each language is ~0.3-0.5KB.


Step 3: Download Mermaid.js

wget -4 -O mermaid.min.js https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js

Step 4: Create the Copy Button

This is a small vanilla JS script — no dependencies, no Prism plugin overhead. The button appears on hover, copies the code block contents, and shows a checkmark for feedback.

Create copy-code.js:

/* copy-code.js — copy button for <pre><code> blocks
   Zero dependencies. Hover to reveal, click to copy. */
(function () {
  // SVG: two overlapping rectangles (classic copy icon)
  const ICON_COPY = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`;
  const ICON_CHECK = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;

  // Inject styles
  const css = document.createElement('style');
  css.textContent = `
pre { position: relative; }
pre .copy-btn {
  position: absolute;
  top: 8px;
  right: 8px;
  padding: 4px 6px;
  border: none;
  border-radius: 4px;
  background: rgba(255,255,255,0.1);
  color: rgba(80,80,80,0.6);
  cursor: pointer;
  opacity: 0 !important;
  transition: opacity .2s, background .2s, color .2s;
  line-height: 0;
}
pre:hover .copy-btn { opacity: 1 !important; }
pre .copy-btn:hover { background: rgba(255,255,255,0.2); color: #555; }
pre .copy-btn.copied { color: #4ade80; }
`;
  document.head.appendChild(css);

  // Add buttons
  document.querySelectorAll('pre > code, pre').forEach(function (el) {
    var pre = el.tagName === 'PRE' ? el : el.parentNode;
    if (pre._hasCopyBtn) return;
    pre._hasCopyBtn = true;

    var btn = document.createElement('button');
    btn.className = 'copy-btn';
    btn.innerHTML = ICON_COPY;
    btn.title = 'Copy';
    btn.addEventListener('click', function () {
      var code = pre.querySelector('code') || pre;
      navigator.clipboard.writeText(code.innerText).then(function () {
        btn.innerHTML = ICON_CHECK;
        btn.classList.add('copied');
        setTimeout(function () {
          btn.innerHTML = ICON_COPY;
          btn.classList.remove('copied');
        }, 2000);
      });
    });
    pre.appendChild(btn);
  });
})();

Save it to your custom-assets directory.

Why JS hover events? Safari has issues with pre:hover .copy-btn when Prism restructures the DOM inside code blocks. The CSS rule still works in Chrome and Firefox, but the JS mouseenter/mouseleave ensures cross-browser compatibility.

Why !important? Prism's CSS selectors (pre[class*="language-"]code[class*="language-"]) are more specific than pre .copy-btn and will override your opacity rules without it. Welcome to specificity wars.


Step 5: Wire It Up in Ghost

Ghost Admin → Settings → Code Injection.

Site Header:

<link rel="stylesheet" href="/assets/custom/prism.min.css">
<style>
/* Overrides for Ghost "Journal" theme — adjust colors to match your theme */
pre[class*="language-"],
code[class*="language-"] {
  background: #f6f6f6 !important;
  border-radius: 8px;
  font-size: 0.9em !important;
}
</style>

The style overrides fix two things: Prism forces its own background color (#f4f2f0) and strips border-radius from code blocks. The #f6f6f6 matches the Journal theme — adjust this to your theme's code block background. The font-size is optional but Prism's default feels slightly too large in most Ghost themes.

Site Footer:

<script src="/assets/custom/prism.min.js"></script>
<script src="/assets/custom/prism-bash.min.js"></script>
<script src="/assets/custom/prism-yaml.min.js"></script>
<script src="/assets/custom/prism-json.min.js"></script>
<script src="/assets/custom/prism-nginx.min.js"></script>
<script src="/assets/custom/prism-docker.min.js"></script>
<script src="/assets/custom/mermaid.min.js" defer></script>
<script src="/assets/custom/copy-code.js" defer></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
  document.querySelectorAll('pre code.language-mermaid').forEach(function(code) {
    var pre = code.parentElement;
    var div = document.createElement('div');
    div.className = 'mermaid';
    div.textContent = code.textContent;
    pre.replaceWith(div);
  });
  mermaid.initialize({startOnLoad: true, theme: 'dark'});
});
</script>

The Mermaid script converts fenced code blocks tagged as language-mermaid into rendered SVG diagrams. Prism auto-highlights on load — no initialization call needed.


Writing Posts with Syntax Highlighting

Ghost's markdown editor creates the right HTML when you use fenced code blocks with language tags:

```bash
docker compose up -d
```

```yaml
services:
  myapp:
    image: nginx:alpine
```

```mermaid
graph LR
    A --> B --> C
```

Prism requires these language-xxx classes — without a language tag, you get no highlighting. Ghost adds them automatically from the fenced code block syntax.


The File Layout

After setup, your custom-assets directory looks like this:

/srv/ghost-blog/custom-assets/
├── copy-code.js
├── mermaid.min.js
├── prism.min.css
├── prism.min.js
├── prism-bash.min.js
├── prism-docker.min.js
├── prism-json.min.js
├── prism-nginx.min.js
└── prism-yaml.min.js

Everything self-hosted. No external requests. No CDN outages. No third-party tracking. For a security-focused blog, that matters.


Licenses

Everything used here is MIT or ISC licensed — free for personal and commercial use, no attribution required in production.

ComponentLicense
Prism.jsMIT
Mermaid.jsMIT
Lucide Icons (copy button SVG)ISC
copy-code.jsDo whatever you want

Resources


Previously: Webserver Hardening with CrowdSec — Real-time attack detection and community-powered blocking.