Choosing a WordPress Code Syntax Highlighting Plugin: Where It Runs Is Everything

Date:

Every comparison of these plugins ranks them on themes, languages and features. None of them checks the one property that decides whether a plugin can work for you at all.

The short answer: ask where the highlighting happens — in the editor when you press save, on the server when the page is requested, or in the reader’s browser. That single answer predicts whether the plugin works with automated publishing, whether it survives being uninstalled, and whether it costs your readers anything. Feature lists do not.
Where the highlighting happens decides everything elseThree architectures. Only one of them survives a post that nobody opened in the editor.
In the editor, on save

Colored markup is written INTO the post. Fastest page, but the color is now your content — and it only ever happens if a person pressed save.

cannot be automated
On the server, per request

The post stores a plain code block; color is added on the way out. Remove the plugin and every block degrades to a valid code block. Cache absorbs it.

In the reader's browser

Content stays plain, but a library ships to every reader on every visit, and the code visibly recolors a moment after the page has already appeared.

Themes and language counts are features and can be changed later. This is an architecture, and changing it means editing every post you have already written.

The three places highlighting can happen

A syntax highlighter turns plain code into colored markup. The interesting question is when.

In the editor, at save time. The plugin runs a highlighter in your browser while you edit and writes the colored markup into the post. The page then just displays what was stored.

On the server, at render time. The post stores a plain code block. The plugin highlights it as the page is assembled, every request.

In the reader’s browser. The post stores plain code, and a JavaScript library — Prism or highlight.js — colors it after the page loads.

Everything else — theme count, language count, line numbers — is a feature. This is an architecture, and it is the thing you cannot change later without editing every post you have written.

Where highlighting happens is an architecture, not a feature, and you can’t change it later.

What each choice actually costs you

Editor-time highlighting produces the fastest page: the work is already done, and nothing runs at render or in the browser. The cost is that the colored markup is now inside your content. Uninstall the plugin and you are left with markup only that plugin understood. And crucially, the highlighting only ever happens if a human opens the editor and saves.

Server-time highlighting keeps your content plain. The stored block is a standard code block; the color is added on the way out. Uninstall the plugin and every block degrades to a plain, valid code block — nothing to clean up. The cost is a little work per request, which caching removes.

Browser-time highlighting keeps content plain too, but ships a library to every reader and colors the code after paint. It is the only option that fails if a reader blocks scripts, and the only one where code visibly changes appearance a moment after the page appears.

The axis nobody tests: does it work when nobody opens the editor?

This is where the choice stops being aesthetic.

If your posts are ever created by anything other than a person typing into the editor — a script, a migration, an import, an automated content pipeline, a headless CMS pushing over an API — then an editor-time highlighter cannot help you. The step that produces the color is the step you skipped.

The failure is silent and specific. The plugin is installed, active, and correctly configured. Its block appears in your post. The code displays. It is simply not colored, and there is no error to tell you why, because nothing went wrong: you never ran the editor that does the highlighting.

An editor-time highlighter can fail silently: nothing is wrong, you simply never opened the editor.

I tested exactly this before choosing. Publishing a code block programmatically to a site running an editor-time highlighter returned markup identical to what was sent — no color, no line numbers, no copy button. Publishing the same code as a plain code block to a server-side highlighter produced 49 highlighted elements and three language labels, with the stored content byte-for-byte unchanged.

That last detail is the one worth keeping. The stored post did not change, so the highlighting was an enhancement of standard markup rather than a rewrite of it.

The uninstall test

Before adopting any of these, ask what your archive looks like if you stop using it.

A plugin whose color lives in your content leaves that content shaped like itself. A plugin that adds color on the way out leaves nothing behind at all. For ten posts this is a curiosity. For several hundred it is the difference between changing a setting and running a migration.

The general form of the test: turn the plugin off and look at a post. If it still reads correctly, the plugin was an enhancement. If it does not, the plugin was a dependency, and you should have known that before you had an archive.

Turn the plugin off and look at a post: if it still reads correctly, it was an enhancement, not a dependency.

Line numbers and a copy button

These are the two features people actually want, and they are worth checking individually rather than trusting a feature matrix.

Line numbers are usually a per-block setting. What matters is whether the plugin exposes it as a standard block attribute, because that is what an automated publisher can set. If it does, turning line numbers on is one field in your script; if it is buried in a plugin-specific interface, it is a manual step forever.

Two judgment calls worth making deliberately once, rather than per post: number blocks only above about three lines, since numbering a two-line block is decoration; and do not number a block that has no declared language, because that is usually a command’s output, and numbering implies it is source meant to be read line by line.

A copy button is more often missing than present, and it is the single most-used control on a technical article — every code block exists to be run somewhere else. It is also the easiest thing to add yourself, and there is a right way to do it: build the button with JavaScript rather than storing it in the content, so a post never depends on it.

// No secure context means no clipboard API. Render nothing rather than a
// button that silently does nothing when pressed.
if ( ! navigator.clipboard || ! window.isSecureContext ) { return; }

document.querySelectorAll( 'pre.wp-block-code' ).forEach( function ( pre ) {
    var code = pre.querySelector( 'code' );
    if ( ! code || pre.querySelector( '.copy-btn' ) ) { return; }

    var button = document.createElement( 'button' );
    button.type = 'button';
    button.className = 'copy-btn';
    button.textContent = 'Copy';
    button.setAttribute( 'aria-label', 'Copy code to clipboard' );

    button.addEventListener( 'click', function () {
        // textContent, not innerText: line numbers are generated in CSS and
        // never enter the text layer, so the clipboard gets only the code.
        navigator.clipboard.writeText( code.textContent ).then( function () {
            button.textContent = 'Copied';
            setTimeout( function () { button.textContent = 'Copy'; }, 1600 );
        } );
    } );

    pre.appendChild( button );
} );
Code language: JavaScript (javascript)

Three details that get missed. The clipboard API needs a secure context, so on plain HTTP the button should not be rendered at all rather than rendered dead. Copy textContent rather than innerText, or generated line numbers end up in the clipboard alongside the code. And whatever CSS you give it, do not reveal it on hover alone — that makes it unreachable for keyboard and touch users, who are precisely the people a copy button helps most.

A copy button is the most-used control on a technical article, since every code block exists to be run elsewhere.

Choosing, in one pass

  1. Do posts ever get created without the editor? If yes, you need server-time or browser-time highlighting. This eliminates most of the field before you read a single feature list.
  2. Turn the candidate off and look at a post. Enhancement or dependency?
  3. Is the language a standard block attribute? If yes, it can be declared rather than detected. Detection is usually right; declared cannot drift.
  4. Are line numbers a block attribute too, or a plugin-specific control?
  5. Copy button — present, or thirty lines you write once?
  6. Then look at themes and language coverage. They matter, and they are the easiest thing to change.

The short version

The comparison you find when you search for these plugins is a feature table, and feature tables are made of the things that are easy to count. The property that decides whether a plugin fits your workflow is not on it.

Ask where the highlighting happens. If your publishing is even partly automated, that question has exactly one acceptable answer, and it eliminates most of the field in a sentence — and before adding any highlighter, it’s worth asking whether the job needs a plugin at all.

Frequently Asked Questions

Does server-side highlighting slow down my site?

It adds work to rendering a page, which any page cache then removes entirely. The comparison that matters is against browser-side highlighting, which ships a library to every reader on every visit — server-side highlighting sends none. The fastest is editor-time, which does the work once, at the cost of putting it in your content.

Can I switch highlighters later without editing every post?

If you are moving from a server-side or browser-side plugin, yes — your content is plain code blocks and nothing about it is plugin-specific. If you are moving from an editor-time plugin, expect a migration, because the colored markup it wrote is in your posts.

Is an editor-time highlighter a bad choice?

No — for hand-written posts it is the fastest option available and a perfectly good one. It becomes the wrong choice at exactly one moment: when something other than a person starts creating your posts. Knowing which situation you are in is the whole decision.

Why declare the language instead of letting it be detected?

Detection is good and usually correct. But it is inference from the code, and inference can change when the detector is updated or when a snippet is short and ambiguous. A declared language is a fact stored with the block. Where declaring is free, prefer the fact.

Should every code block have line numbers?

No. They earn their place on a listing long enough to refer to a line, and they are noise on a two-liner. Numbering a block that holds a command’s output is actively misleading, because it presents the output as source.

What about a plugin that adds highlighting through a shortcode?

Treat it as the strictest case of the uninstall test. Shortcode-wrapped code is content that only that plugin can interpret, so removing it leaves visible shortcode text in every affected post. Block-based or filter-based highlighting does not have this failure mode.

Share post:

Subscribe

Popular

More like this
Related

How to Merge Spreadsheets by URL (and Why Our Tool Reported 5 Misses That Weren’t)

Matching two spreadsheets on a URL column is the same job as matching a stock list to a price list by product code. It goes wrong in the same two places: how you clean the code, and how you count what didn't match.

Many-to-One: Why a Mapping Backlog Outgrows Its Target Taxonomy

You're mapping a messy source vocabulary onto a clean target taxonomy. The target has about 5,600 slots, so the job must be roughly 5,600 decisions — right? The backlog is already past 8,900, and still climbing.

The Fix That Didn’t Take: When a Content-Hash Gate Skips Your Code Change

You shipped a fix to how one field is computed, re-ran the pipeline across everything, and it reported a clean run. Then the consistency check still flags those records as wrong. The fix is in the code, the rebuild "ran" — and the records never changed. Your own optimisation skipped them.

Your Dedup Scan Is Over-Counting — Because It Forgot What You Already Fixed

The report said 80 duplicate groups still to resolve. You'd been resolving them for days. You run it again after a big batch and it *still* says 80-ish. The backlog isn't stubborn — the scan is counting work you already finished.