Image: Google Gemini

Self-Hosted Syntax Highlighting in a Roq Blog with Prism.js

A code-heavy blog without syntax highlighting reads like a wall. We wanted colours, and we wanted them self-hosted — no third-party CDN, no extra DNS lookup, no privacy footnote.

It turns out the pieces are all already in place.

What Roq Already Gives You

Roq's markdown plugin renders fenced code blocks like this:

<pre><code class="language-java">…</code></pre>

The language-X class convention was popularised by Prism.js, so the markup is already exactly the shape Prism expects. We don't need to touch any template.

What the Web Bundler Gives You

Roq ships with the Quarkus Web Bundler, which can pull npm packages straight from Maven Central via mvnpm.org. That means a JS dependency looks just like any other Maven dependency:

<dependency>
    <groupId>org.mvnpm</groupId>
    <artifactId>prismjs</artifactId>
    <version>1.30.0</version>
    <scope>provided</scope>
</dependency>

provided because the artifact is only needed at build time — the JS gets baked into the bundle output.

The Entry Point

Web-bundler treats src/main/resources/web/app/app.js as the entry. Anything imported from there ends up in /static/bundle/app.js (or .css). We import the languages we actually use, plus the Prism theme:

import "./style.css";
import "prismjs/themes/prism.css";
import Prism from "prismjs";

import "prismjs/components/prism-java";
import "prismjs/components/prism-bash";
import "prismjs/components/prism-properties";
import "prismjs/components/prism-yaml";
import "prismjs/components/prism-markup-templating";

if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", () => Prism.highlightAll());
} else {
    Prism.highlightAll();
}

Adding a new language later is one line.

How It Lands in the Page

The base layout already calls {#bundle /} in the <head>. That tag is provided by the Web Bundler and emits the right <link> and <script> for every bundled asset:

<link rel="stylesheet" href="/static/bundle/app.css" />
<script type="module" src="/static/bundle/app.js"></script>

Both are content-hashed under the hood, so a deploy invalidates the browser cache automatically.

The Numbers

The whole bundle — Prism core, five language grammars, our init code, the theme CSS, and our existing site stylesheet — comes to 48 kB. Gzipped over the wire that's roughly 17 kB, served from the same origin as the rest of the site.