ile
May 19, 2025, 2:26am
1
I have trouble getting syntax highlighting to work.
In the front matter I have markup: html
In the index.md’s content section I have:
<pre><code class="language-javascript">// Set these!
const API_KEY = '';
const FORM_ENDPOINT = '';</code></pre>
In config.yml I have:
theme: hugo-PaperMod
pygmentsUseClasses: false
params:
assets:
disableHLJS: true
markup:
goldmark:
renderer:
unsafe: true
highlight:
noClasses: false
style: monokai # Choose a Chroma style (e.g., monokai, dracula, github)
codeFences: true
The final output:
<pre><code class="language-javascript">// Set these!
const API_KEY = '';
const FORM_ENDPOINT = '';</code><button class="copy-code">copy</button></pre>
So it adds the copy button at least.
I’m a bit of a new user, just throwing things at the wall here (what Grok suggests).
ile
May 19, 2025, 5:35am
2
Grok and me came up with this, which seems to do the job:
<!DOCTYPE html>
<html>
<head>
<title>{{ .Title }}</title>
</head>
<body>
<h1>{{ .Title }}</h1>
{{ $content := .Content }}
{{ $matches := findRE `<pre><code[^>]*language-([a-zA-Z0-9]+)[^>]*>([\s\S]*?)</code></pre>` $content }}
{{ if $matches }}
{{ range $index, $match := $matches }}
{{ $langMatch := findRE `language-([a-zA-Z0-9]+)` $match 1 }}
{{ $lang := "javascript" }} <!-- Fallback -->
{{ if $langMatch }}
{{ $lang = replaceRE `language-([a-zA-Z0-9]+)` "$1" (index $langMatch 0) }}
{{ end }}
{{ $code := replaceRE `<pre><code[^>]*language-[a-zA-Z0-9]+[^>]*>([\s\S]*?)</code></pre>` "$1" $match }}
{{ $trimmedCode := trim $code "\n \t" }}
{{ if and $lang $trimmedCode }}
{{ $highlighted := highlight $trimmedCode $lang "" }}
{{ $replacement := printf `<pre class="chroma"><code class="language-%s">%s</code></pre>` $lang $highlighted }}
{{ $content = replace $content $match $replacement }}
{{ end }}
{{ end }}
{{ end }}
{{ $content | safeHTML }}
</body>
</html>
ile
May 19, 2025, 5:57am
4
My source content is HTML, not markdown.
I got that in the meantime. Then you have no choice, and whatever you set for the Goldmark renderer is irrelevant. Those settings are only for MD files that are rendered to HTML.
With MD, you’d simply use fenced or other code blocks.
ile
May 19, 2025, 7:11am
6
Ok, this was a bit of work, but it seems to work mostly now.
themes/hugo-PaperMod/layouts/partials/code_highlight.html:
{{ $content := .content }}
{{ if .bypass }}
{{ $content | safeHTML }}
{{ else }}
<!-- Process only raw HTML <pre><code> blocks -->
{{ $matches := findRE `<pre><code( class="language-[^"]*")?>[\s\S]*?</code></pre>` $content }}
{{ if $matches }}
{{ range $match := $matches }}
{{ $lang := "plaintext" }}
{{ $langMatch := findRE `language-([a-zA-Z0-9]+)` $match 1 }}
{{ if $langMatch }}
{{ $lang = replaceRE `language-([a-zA-Z0-9]+)` "$1" (index $langMatch 0) }}
{{ end }}
{{ $code := replaceRE `<pre><code[^>]*>([\s\S]*?)</code></pre>` "$1" $match }}
{{ $trimmedCode := trim $code "\n \t" }}
{{ if $trimmedCode }}
<!-- Decode HTML entities before highlighting -->
{{ $decodedCode := $trimmedCode | htmlUnescape }}
{{ $highlighted := highlight $decodedCode $lang (dict "noClasses" false) }}
{{ $innerCode := replaceRE `<div class="highlight"><pre[^>]*><code[^>]*>([\s\S]*?)</code></pre></div>` "$1" $highlighted }}
<!-- Remove highlighting from HTML entities -->
{{ $cleanedCode := replaceRE `<span class="ni"><</span>` "<" $innerCode }}
{{ $cleanedCode = replaceRE `<span class="ni">></span>` ">" $cleanedCode }}
{{ $cleanedCode = replaceRE `<span class="ni">&</span>` "&" $cleanedCode }}
{{ $replacement := printf `<div class="code-wrapper"><pre class="chroma"><code class="language-%s">%s</code></pre><button class="copy-code">copy</button></div>` $lang $cleanedCode }}
{{ $content = replace $content $match $replacement }}
{{ end }}
{{ end }}
{{ end }}
{{ $content | safeHTML }}
{{ end }}
themes/hugo-PaperMod/layouts/_default/single.html:
{{- define "main" }}
<article class="post-single">
<header class="post-header">
{{ partial "breadcrumbs.html" . }}
<h1 class="post-title entry-hint-parent">
{{ .Title }}
{{- if .Draft }}
<span class="entry-hint" title="Draft">
<svg xmlns="http://www.w3.org/2000/svg" height="35" viewBox="0 -960 960 960" fill="currentColor">
<path
d="M160-410v-60h300v60H160Zm0-165v-60h470v60H160Zm0-165v-60h470v60H160Zm360 580v-123l221-220q9-9 20-13t22-4q12 0 23 4.5t20 13.5l37 37q9 9 13 20t4 22q0 11-4.5 22.5T862.09-380L643-160H520Zm300-263-37-37 37 37ZM580-220h38l121-122-18-19-19-18-122 121v38Zm141-141-19-18 37 37-18-19Z" />
</svg>
</span>
{{- end }}
</h1>
{{- if .Description }}
<div class="post-description">
{{ .Description }}
</div>
{{- end }}
{{- if not (.Param "hideMeta") }}
<div class="post-meta">
{{- partial "post_meta.html" . -}}
{{- partial "translation_list.html" . -}}
{{- partial "edit_post.html" . -}}
{{- partial "post_canonical.html" . -}}
</div>
{{- end }}
</header>
{{- $isHidden := (.Param "cover.hiddenInSingle") | default (.Param "cover.hidden") | default false }}
{{- partial "cover.html" (dict "cxt" . "IsSingle" true "isHidden" $isHidden) }}
{{- if (.Param "ShowToc") }}
{{- partial "toc.html" . }}
{{- end }}
{{- if .Content }}
<div class="post-content">
{{- if not (.Param "disableAnchoredHeadings") }}
{{- $anchoredContent := partial "anchored_headings.html" .Content }}
{{- partial "code_highlight.html" (dict "content" $anchoredContent "bypass" false) }}
{{- else }}
{{- partial "code_highlight.html" (dict "content" .Content "bypass" false) }}
{{- end }}
</div>
{{- end }}
<footer class="post-footer">
{{- $tags := .Language.Params.Taxonomies.tag | default "tags" }}
<ul class="post-tags">
{{- range ($.GetTerms $tags) }}
<li><a href="{{ .Permalink }}">{{ .LinkTitle }}</a></li>
{{- end }}
</ul>
{{- if (.Param "ShowPostNavLinks") }}
{{- partial "post_nav_links.html" . }}
{{- end }}
{{- if (and site.Params.ShowShareButtons (ne .Params.disableShare true)) }}
{{- partial "share_icons.html" . -}}
{{- end }}
</footer>
{{- if (.Param "comments") }}
{{- partial "comments.html" . }}
{{- end }}
</article>
{{- end }}{{/* end main */}}
Changing the files of your theme might not be such a good idea, since your changes might be overwritten by a new version of the theme.
You might be better off if you copy the files to the corresponding directory under /layouts
and modify them there.
ile
May 19, 2025, 8:44am
8
Thanks. I think I will fork/rename the theme at this point – I have made too many changes already for it to be updateable any more.
Good point if someone is going re-use the code.
Is there some reason that you can’t just use the highlight shortcode ?
1 Like
ile
May 19, 2025, 1:46pm
10
I’m writing the content with TipTap (web app), then save it to the database (the same web app). Then I create the blog with Hugo on my development machine, using that database as a “headless CMS” through GraphQL.
So, the source content is HTML and that’s the way I have to keep it.
There is no way for shortcodes to enter the equation.
Well, at least the workflow’s not convoluted, so you’ve got that going for you.
1 Like
ile
May 19, 2025, 4:02pm
12
It seems that Hugo likes markdown more than HTML.
There might be an opportunity to handle HTML more ”natively”.
I think image processing is another thing, in addition to syntax highlighting.
I don’t know what that means. We pass it through.
ile
May 19, 2025, 4:11pm
14
Well, in markdown the images are processed, a srcset is created. Not sure if this is by a plugin or themes, but that seems to happen.
Similar things for HTML?
That’s theme-specific.
If you’re just dumping HTML from A to B, you need to either change A or process it somehow when it gets to B.
ile
May 19, 2025, 4:14pm
16
I can create that processing in the theme. That was my plan earlier, wasn’t just sure where it happens.
After all, syntax highlighting seems to be more Hugo specific.
Hugo provides native syntax highlighting via a shortcode, fenced code blocks (Markdown), or the transform.Highlight
function.
But you can implement your own JS approach instead (e.g., https://highlightjs.org ).