Add Syntax Highlighting to Ghost (with Prism.js + CDN)

Add Syntax Highlighting to Ghost (with Prism.js + CDN)

Most guides out there show you the “basic way” to add Prism.js, but I wanted something that feels smooth and doesn’t drag down performance too much. Sure, loading from an external CDN always adds a little cost, but honestly—it’s not big enough to notice. I still thought hard about how to cut down on any slowdown, and this is the final setup I ended up using.

Let’s jump right in.


Header (insert in Code Injection → Site Header)

<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>

That’s it. This one line simply tells the browser, “hey, get ready to grab stuff from jsDelivr.” It makes loading a bit faster.


<script>
(function () {
  // Skip everything if no code blocks
  var hasCode = !!document.querySelector('pre code');
  if (!hasCode) return;

  // Auto-add "line-numbers" class to all <pre><code>
  document.querySelectorAll('pre code').forEach(function (block) {
    if (block.parentElement) block.parentElement.classList.add('line-numbers');
  });

  // Simple dynamic CSS/JS loader
  function loadCSS(href) {
    return new Promise(function (resolve, reject) {
      var l = document.createElement('link');
      l.rel = 'stylesheet';
      l.href = href;
      l.onload = resolve;
      l.onerror = reject;
      document.head.appendChild(l);
    });
  }
  function loadScript(src) {
    return new Promise(function (resolve, reject) {
      var s = document.createElement('script');
      s.src = src;
      s.defer = true;
      s.onload = resolve;
      s.onerror = reject;
      document.body.appendChild(s);
    });
  }

  // 1) Prism theme + line numbers CSS
  loadCSS('https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css')
  .then(function () {
    return loadCSS('https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/line-numbers/prism-line-numbers.min.css');
  })

  // 1.5) Insert override CSS *after* theme (to force line-wrap)
  .then(function () {
    var style = document.createElement('style');
    style.textContent = `
pre[class*="language-"],
code[class*="language-"] {
  white-space: pre-wrap !important;      /* allow wrapping */
  overflow-wrap: anywhere !important;    /* break anywhere */
  word-break: break-word !important;     /* break long tokens */
}
pre.line-numbers { padding-left: 3.8em; position: relative; }
pre.line-numbers > code { white-space: inherit !important; }`;
    document.head.appendChild(style);
  })

  // 2) Prism core + Autoloader + Line Numbers JS
  .then(function () { return loadScript('https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js'); })
  .then(function () { return loadScript('https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js'); })
  .then(function () { return loadScript('https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/line-numbers/prism-line-numbers.min.js'); })

  // 3) Set autoloader path + highlight all code
  .then(function () {
    if (window.Prism && Prism.plugins && Prism.plugins.autoloader) {
      Prism.plugins.autoloader.languages_path =
        'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/';
    }
    if (window.Prism && Prism.highlightAll) Prism.highlightAll();
  })
  .catch(function (e) { console.warn('[Prism] load error:', e); });
})();
</script>

Why does this look different from other tutorials?

Most “how to” pages show you a bunch of <link> and <script> tags pasted directly into the header/footer. That works, but it loads everything whether you need it or not.

Here, the script first checks: “Do we even have code blocks on the page?” If not, nothing loads at all—zero overhead. If yes, then it loads CSS and JS in order, so nothing breaks.

For the look, I went with Prism’s Twilight theme—to me, it’s the one that feels the cleanest and nicest on my eyes. After that, I made sure to add Prism’s Line Numbers plugin (so every code block gets clear line numbers) and a custom line-wrapping override. This way, even very long lines don’t blow past the edge of the screen—they wrap neatly and stay aligned with the numbers.

The autoloader plugin means you don’t need to worry about which languages to include. Prism will fetch them on demand from the CDN. Line numbers are applied automatically to every block, no extra HTML required.


(modified at 18/09/25 Below)

In this version I split things into Script (keeps the same position as before) and a separate Style block I recommend adding at the very bottom of Code Injection → Site Footer so it reliably overrides Prism’s theme CSS.

<script>
(function () {
  // Fast exit: if the page has no code blocks, skip all loading
  if (!document.querySelector('pre code')) return;

  // Pre-prime each <pre><code> BEFORE Prism runs:
  // - add line-numbers class for the Line Numbers plugin
  // - set copy-to-clipboard labels for the Copy plugin
  function primePre(root){
    (root || document).querySelectorAll('pre code').forEach(function (code) {
      var pre = code.parentElement;
      if (!pre) return;
      pre.classList.add('line-numbers');
      pre.setAttribute('data-prismjs-copy', 'Copy');
      pre.setAttribute('data-prismjs-copy-success', 'Copied!');
      pre.setAttribute('data-prismjs-copy-error', 'Failed');
    });
  }
  primePre(document);

  // Tiny loaders (Promise-based) to keep order strict and readable
  function loadCSS(href){
    return new Promise(function(res,rej){
      var l=document.createElement('link');
      l.rel='stylesheet'; l.href=href;
      l.onload=res; l.onerror=rej;
      document.head.appendChild(l);
    });
  }
  function loadScript(src){
    return new Promise(function(res,rej){
      var s=document.createElement('script');
      s.src=src; s.defer=true;        // defer keeps execution order while not blocking parsing
      s.onload=res; s.onerror=rej;
      document.body.appendChild(s);
    });
  }

  // Load order matters:
  // 1) CSS (theme + plugins)
  // 2) Core JS + plugin JS (in dependency order)
  loadCSS('https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css')
  .then(function(){ return loadCSS('https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/line-numbers/prism-line-numbers.min.css'); })
  .then(function(){ return loadCSS('https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/toolbar/prism-toolbar.min.css'); })
  .then(function(){ return loadScript('https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js'); })
  .then(function(){ return loadScript('https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js'); })
  .then(function(){ return loadScript('https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/line-numbers/prism-line-numbers.min.js'); })
  .then(function(){ return loadScript('https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/toolbar/prism-toolbar.min.js'); })
  .then(function(){ return loadScript('https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js'); })
  .then(function(){
    // Configure Autoloader to fetch languages on demand from CDN
    if (window.Prism && Prism.plugins && Prism.plugins.autoloader) {
      Prism.plugins.autoloader.languages_path =
        'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/';
    }

    // Register a "show language" badge for the toolbar BEFORE highlighting
    if (Prism.plugins && Prism.plugins.toolbar) {
      Prism.plugins.toolbar.registerButton('lang-badge', function (env) {
        var pre = env.element.closest('pre');
        // Allow overriding the label via data attributes if desired
        var dataLabel = pre ? (pre.getAttribute('data-lang-label') || pre.getAttribute('data-lang')) : '';
        // Fallback to class-based language (language-xxx)
        var classLang = Array.from(env.element.classList).find(function(c){ return c.indexOf('language-')===0; });
        var lang = dataLabel || (classLang ? classLang.replace('language-','') : 'text');
        var badge = document.createElement('span');
        badge.className = 'prism-lang-badge';
        badge.textContent = String(lang).toUpperCase();
        return badge; // Prism Toolbar will insert this next to the Copy button
      });
    }

    // Initial highlight pass (also attaches toolbar, copy, and line numbers)
    if (Prism.highlightAll) Prism.highlightAll();

    // Handle dynamically added code blocks (SPA / infinite scroll)
    function highlightUnder(root){
      primePre(root); // ensure line-numbers + copy labels are set prior to highlighting
      root.querySelectorAll('pre code').forEach(function(el){
        if (Prism.highlightElement) Prism.highlightElement(el);
      });
    }

    // Observe the whole document for newly inserted nodes
    var mo = new MutationObserver(function(muts){
      muts.forEach(function(m){
        m.addedNodes && m.addedNodes.forEach(function(n){
          if (!(n instanceof Element)) return;
          // Single <pre><code> node directly added
          if (n.matches && n.matches('pre code')) {
            highlightUnder(n.parentElement || n);
          }
          // Or a container that contains one/more code blocks
          else if (n.querySelectorAll) {
            var nodes = n.querySelectorAll('pre code');
            if (nodes.length) highlightUnder(n);
          }
        });
      });
    });
    mo.observe(document.body, { childList: true, subtree: true });
  })
  .catch(function(e){ console.warn('[Prism] load error:', e); });
})();
</script>
<style>
/* Global tokens */
:root{
  --prism-radius:12px;
  --prism-pill-h:28px;
  --prism-pill-px:12px;
  --prism-toolbar-top:14px;
  --prism-toolbar-right:12px;
}

/* Code block layout */
pre[class*="language-"],
code[class*="language-"]{white-space:pre-wrap!important;overflow-wrap:anywhere!important;word-break:break-word!important;}
pre.line-numbers{padding-left:3.8em;position:relative;}
pre.line-numbers>code{white-space:inherit!important;}
/* Extra top padding to account for lowered toolbar */
pre[class*="language-"]{padding-top:2.6em!important;}

/* Rounded corners + clipping (applies to toolbar/line numbers too) */
.code-toolbar{border-radius:var(--prism-radius) !important;overflow:hidden!important;}
.code-toolbar > pre[class*="language-"],
.code-toolbar pre[class*="language-"]{border-radius:var(--prism-radius)!important;}

/* Toolbar container */
.code-toolbar .toolbar{
  opacity:1!important;visibility:visible!important;transform:none!important;transition:none!important;z-index:3;
  display:flex;align-items:center;gap:8px;
}
/* Position: define it in one place only */
.code-toolbar > .toolbar{position:absolute;top:var(--prism-toolbar-top)!important;right:var(--prism-toolbar-right)!important;left:auto;}

/* Toolbar item wrapper: unify vertical centering (fix language-badge baseline) */
.code-toolbar > .toolbar .toolbar-item{
  display:flex!important;align-items:center!important;height:var(--prism-pill-h)!important;
}

/* Unified pill styling (language badge + Copy) */
.code-toolbar > .toolbar .toolbar-item > button,
.code-toolbar > .toolbar .toolbar-item > span.prism-lang-badge{
  display:inline-flex!important;align-items:center!important;justify-content:center!important;gap:6px!important;
  box-sizing:border-box!important;height:100% !important;padding:0 var(--prism-pill-px)!important;margin:0!important;
  border-radius:999px!important;border:none!important;box-shadow:none!important;
  -webkit-appearance:none!important;appearance:none!important;font:inherit!important;font-size:13px!important;font-weight:400!important;line-height:1!important;
}

/* Light/Dark color schemes */
@media (prefers-color-scheme: dark){
  .code-toolbar > .toolbar .toolbar-item > button,
  .code-toolbar > .toolbar .toolbar-item > span.prism-lang-badge{
    background:rgba(255,255,255,.06)!important;color:rgba(255,255,255,.86)!important;
  }
  .code-toolbar > .toolbar .toolbar-item > button:hover,
  .code-toolbar > .toolbar .toolbar-item > span.prism-lang-badge:hover{
    background:rgba(255,255,255,.10)!important;
  }
}
@media (prefers-color-scheme: light){
  .code-toolbar > .toolbar .toolbar-item > button,
  .code-toolbar > .toolbar .toolbar-item > span.prism-lang-badge{
    background:rgba(0,0,0,.04)!important;color:rgba(0,0,0,.86)!important;
  }
  .code-toolbar > .toolbar .toolbar-item > button:hover,
  .code-toolbar > .toolbar .toolbar-item > span.prism-lang-badge:hover{
    background:rgba(0,0,0,.07)!important;
  }
}

/* Language badge: uppercase only */
.code-toolbar > .toolbar .toolbar-item > span.prism-lang-badge{text-transform:uppercase!important;}

/* Copy icon (left of text) */
.code-toolbar > .toolbar .toolbar-item > button[data-copy-state]::before{
  content:""!important;display:inline-block!important;width:14px!important;height:14px!important;margin-right:6px!important;
  background-repeat:no-repeat!important;background-size:14px 14px!important;
}
@media (prefers-color-scheme: dark){
  .code-toolbar > .toolbar .toolbar-item > button[data-copy-state]::before{
    background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='rgba(255,255,255,0.85)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><rect x='9' y='9' width='13' height='13' rx='2' ry='2'/><path d='M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1'/></svg>")!important;
  }
}
@media (prefers-color-scheme: light){
  .code-toolbar > .toolbar .toolbar-item > button[data-copy-state]::before{
    background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='rgba(0,0,0,0.85)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><rect x='9' y='9' width='13' height='13' rx='2' ry='2'/><path d='M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1'/></svg>")!important;
  }
}
</style>

What’s new in v2

  • Split setup: The script still lives in the same place (Site Footer). The style is now a dedicated block placed at the very bottom of the footer so it overrides Prism’s theme cleanly.
  • Language badge: Every code block now shows a small language pill (e.g., BASH, YAML, JS) next to the copy button, so readers instantly know which syntax they’re looking at.
  • Rounded corners: Code blocks use soft rounded corners for a friendlier look that matches modern UI.
  • Everything else remains fast: It still only loads when a page actually contains code; the Autoloader fetches languages on demand; line numbers and wrapping remain enabled.