Code Blocks


Syntax highlighting, line numbers, and the copy button

Beautiful Hugo supports two syntax highlighting engines: Hugo’s built-in Chroma (default) and client-side Highlight.js. This page demonstrates Chroma, which is fast and built when the page renders.

Required Config

Beautiful Hugo supports two syntax highlighting engines: Hugo’s built-in Chroma (default) and client-side Highlight.js. See Configuration — Syntax Highlighting for the required hugo.toml settings for each engine.

Fenced Code Blocks

Use triple backticks with a language identifier, like this:

{
  "example": "code"
}
# python
def fibonacci(n):
    """Return the nth Fibonacci number."""
    if n <= 1:
        return n
    a, b = 0, 1
    for _ in range(2, n + 1):
        a, b = b, a + b
    return b
// go
package main

import "fmt"

func main() {
    ch := make(chan string)
    go func() { ch <- "hello" }()
    fmt.Println(<-ch)
}
// javascript
const greet = (name) => {
  console.log(`Hello, ${name}!`);
};

greet("World");
# toml
[Params]
  subtitle = "My Site"
  colorScheme = "auto"
  selfHosted = false

Line Numbers

Use Hugo’s highlight shortcode with linenos to add line numbers:

{{< highlight python "linenos=table" >}}
def quicksort(arr):
    ...
{{< /highlight >}}
1
2
3
4
5
6
7
8
def quicksort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) // 2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quicksort(left) + middle + quicksort(right)

Inline line numbers:

{{< highlight go "linenos=inline" >}}
func reverse(s string) string {
    ...
{{< /highlight >}}
1func reverse(s string) string {
2    runes := []rune(s)
3    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
4        runes[i], runes[j] = runes[j], runes[i]
5    }
6    return string(runes)
7}

Line Highlighting

Use hl_lines to emphasize specific lines:

{{< highlight python "linenos=table, hl_lines=3 4" >}}
def process(data):
    result = []
    for item in data:       # highlighted
        result.append(item)  # highlighted
    return result
{{< /highlight >}}
1
2
3
4
5
def process(data):
    result = []
    for item in data:       # highlighted
        result.append(item)  # highlighted
    return result

Copy Button

Every code block automatically gets a copy-to-clipboard button in the top-right corner. Hover over any code block on this page to see it. The button strips line numbers before copying.

Including External Files

The include-code shortcode reads a source file from your project and renders it with syntax highlighting. It respects the same Chroma/HLJS setting as fenced code blocks.

With explicit language and line options (Chroma only):

Live example

This page includes the example site’s own hugo.toml:

# This gets overridden when building for production
baseurl = "https://sbackp.com"
DefaultContentLanguage = "en"
title = "SBACKP"
enableGitInfo = true

[[module.imports]]
   path = "github.com/halogenica/beautifulhugo/v5"

[pagination]
  pagerSize = 5

# With enableGitInfo, ":git" makes .Lastmod the commit date of the file.
# Hugo does not merge [frontmatter] from a theme, so it must stay here.
[frontmatter]
  lastmod = [":git", "lastmod", "date"]

[Services]
  [Services.Disqus]
    # Shortname = "XXX"
  [Services.googleAnalytics]
    id = "G-9J06S5426H"

[taxonomies]
  category = "categories"
  tag = "tags"

# Ranking configuration for showRelatedPosts (Hugo's related-content engine)
[related]
  includeNewer = true
  threshold = 80
  toLower = false
  [[related.indices]]
    name = "tags"
    weight = 100
  [[related.indices]]
    name = "keywords"
    weight = 80
  [[related.indices]]
    name = "date"
    weight = 10

# With Params.selfHosted = true the theme fetches Bootstrap, KaTeX, PhotoSwipe,
# Highlight.js and Fuse.js at build time (resources.GetRemote) and serves them
# from the site. Hugo keeps the downloads in the getresource file cache, so
# only the first build needs network access. The CDN URLs are pinned and
# immutable, so revalidation stays off; enable [HTTPCache] to honour the
# Cache-Control headers instead. Hugo does not merge these sections from a
# theme, so they belong in the site configuration.
# [caches.getresource]
#   dir = ":cacheDir/:project"
#   maxAge = -1
# [HTTPCache]
#   [HTTPCache.cache.for]
#     includes = ["**"]

[Params]
  subtitle = "Half thought-out solutions to made-up problems"
  mainSections = ["post", "recipe"]
  logo = "/img/avator-favicon-stag.png" # Expecting square dimensions
  favicon = "/img/favicon.ico"
  # share_img = "/img/share.jpg" # Default social image, used instead of logo
  # mobileMenuPosition = "left" # Side of the hamburger menu on narrow screens ("right" by default)
  dateFormat = ":date_long"
  commit = false
  rss = false
  readingTime = true
  wordCount = false
  colorScheme = "dark" # "auto", "dark", or "light"
  # mathEngine = "katex"  # "katex" (default), "mathjax" (always CDN, even with selfHosted), or "none"
  mathRender = "server"  # render KaTeX at build time; "client" (default) renders in the browser
  socialShare = false
  delayDisqus = true
  showRelatedPosts = true
  disableFigureOverride = true
  headerImgStyle = "narrow"
  showSource = false
  sourceRepo = "https://github.com/halogenica/beautifulhugo/blob/master/exampleSite/"
  since = 2023
  showPostNav = true
  toc = false
  hideAuthor = true

  [Params.search]
    provider = "fuse"

[[Params.bigimg]]
  src = "/img/triangle.jpg"
  desc = "Triangle"
[[Params.bigimg]]
  src = "/img/sphere.jpg"
  desc = "Sphere"
  position = "center top"
[[Params.bigimg]]
  src = "/img/hexagon.jpg"
  desc = "Hexagon"

# For AI search summary limits, see [Params.seo] in the SEO & i18n docs.
[Params.seo.robots]
  ai-summary-limit = "nosnippet"
  noindex = true
  nofollow = true
  
[Params.author]
  name = "SBACKP"
  website = "https://sbackp.com"
  #email = "sbackp@gmail.com"
  #github = "sbackp"
  #twitter = "sbackp"
  #linkedin = "sbackp" # company/username

# Any platform the built-in registry does not cover. "icon" is a CSS class,
# so a site can also load its own icon font from head_custom.html.
#[[Params.author.custom]]
#  href = "https://matrix.to/#/@username:example.org"
#  title = "Matrix"
#  icon = "fas fa-comments"

[markup.highlight]
  noClasses = false

[markup.goldmark.parser]
  wrapStandAloneImageWithinParagraph = false # lets Markdown attributes reach the image render hook

[markup.goldmark.parser.attribute]
  block = true

[markup.goldmark.renderer]
  unsafe = true

# Extra inline markup: ==mark==, ~~delete~~, ^sup^, ~sub~, ++insert++
# GFM strikethrough also matches a single tilde, so it must go off for ~sub~.
[markup.goldmark.extensions]
  strikethrough = false

[markup.goldmark.extensions.extras.delete]
  enable = true
[markup.goldmark.extensions.extras.insert]
  enable = true
[markup.goldmark.extensions.extras.mark]
  enable = true
[markup.goldmark.extensions.extras.subscript]
  enable = true
# Safe with math because the passthrough extension below keeps LaTeX ^ away
# from Goldmark.
[markup.goldmark.extensions.extras.superscript]
  enable = true

# Headings collected for the table-of-contents panel
[markup.tableOfContents]
  startLevel = 2
  endLevel = 4
  ordered = false

# Keeps LaTeX out of Goldmark, so the browser gets the delimiters unchanged.
# Hugo does not merge [markup] from a theme, so this must stay in the site
# configuration. The delimiters are the KaTeX auto-render defaults; single $
# is not an inline delimiter, because prose often contains a stray $.
[markup.goldmark.extensions.passthrough]
  enable = true
  [markup.goldmark.extensions.passthrough.delimiters]
    block = [['\[', '\]'], ['$$', '$$']]
    inline = [['\(', '\)']]

[[menu.main]]
    identifier = "setup"
    name = "Setup"
    weight = 1

[[menu.main]]
    parent = "setup"
    name = "Configuration"
    url = "page/configuration/"
    weight = 1

[[menu.main]]
    parent = "setup"
    name = "Pages & Layouts"
    url = "page/pages-and-layouts/"
    weight = 2

[[menu.main]]
    parent = "setup"
    name = "Theming"
    url = "page/theming/"
    weight = 3

[[menu.main]]
    parent = "setup"
    name = "SEO & i18n"
    url = "page/seo-and-i18n/"
    weight = 4

[[menu.main]]
    parent = "setup"
    name = "Comments & Social"
    url = "page/comments-and-social/"
    weight = 5

[[menu.main]]
    identifier = "features"
    name = "Features"
    weight = 2

[[menu.main]]
    parent = "features"
    name = "Shortcodes"
    url = "page/shortcodes/"
    weight = 1

[[menu.main]]
    parent = "features"
    name = "Code Blocks"
    url = "page/code-blocks/"
    weight = 2

[[menu.main]]
    parent = "features"
    name = "Markdown Extensions"
    url = "page/markdown-extensions/"
    weight = 3

[[menu.main]]
    parent = "features"
    name = "Figures & Galleries"
    url = "page/figures-and-galleries/"
    weight = 4

[[menu.main]]
    parent = "features"
    name = "Math & Diagrams"
    url = "page/math-and-diagrams/"
    weight = 5

[[menu.main]]
    parent = "features"
    name = "Publications"
    url = "publication/"
    weight = 6

[[menu.main]]
    identifier = "blog"
    name = "Blog"
    weight = 3

[[menu.main]]
    parent = "blog"
    name = "Posts"
    url = "post/"
    weight = 1

[[menu.main]]
    parent = "blog"
    name = "Tags"
    url = "tags/"
    weight = 2

[[menu.main]]
    parent = "blog"
    name = "Categories"
    url = "categories/"
    weight = 3

[[menu.main]]
    parent = "blog"
    name = "Archives"
    url = "archives/"
    weight = 4

[[menu.main]]
    name = "About"
    url = "page/about/"
    weight = 4

[outputs]
home = ["HTML", "RSS", "JSON"]
section = ["HTML", "RSS"]
page = ["HTML"]

With line numbers and highlighted lines:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# This gets overridden when building for production
baseurl = "https://sbackp.com"
DefaultContentLanguage = "en"
title = "SBACKP"
enableGitInfo = true

[[module.imports]]
   path = "github.com/halogenica/beautifulhugo/v5"

[pagination]
  pagerSize = 5

# With enableGitInfo, ":git" makes .Lastmod the commit date of the file.
# Hugo does not merge [frontmatter] from a theme, so it must stay here.
[frontmatter]
  lastmod = [":git", "lastmod", "date"]

[Services]
  [Services.Disqus]
    # Shortname = "XXX"
  [Services.googleAnalytics]
    id = "G-9J06S5426H"

[taxonomies]
  category = "categories"
  tag = "tags"

# Ranking configuration for showRelatedPosts (Hugo's related-content engine)
[related]
  includeNewer = true
  threshold = 80
  toLower = false
  [[related.indices]]
    name = "tags"
    weight = 100
  [[related.indices]]
    name = "keywords"
    weight = 80
  [[related.indices]]
    name = "date"
    weight = 10

# With Params.selfHosted = true the theme fetches Bootstrap, KaTeX, PhotoSwipe,
# Highlight.js and Fuse.js at build time (resources.GetRemote) and serves them
# from the site. Hugo keeps the downloads in the getresource file cache, so
# only the first build needs network access. The CDN URLs are pinned and
# immutable, so revalidation stays off; enable [HTTPCache] to honour the
# Cache-Control headers instead. Hugo does not merge these sections from a
# theme, so they belong in the site configuration.
# [caches.getresource]
#   dir = ":cacheDir/:project"
#   maxAge = -1
# [HTTPCache]
#   [HTTPCache.cache.for]
#     includes = ["**"]

[Params]
  subtitle = "Half thought-out solutions to made-up problems"
  mainSections = ["post", "recipe"]
  logo = "/img/avator-favicon-stag.png" # Expecting square dimensions
  favicon = "/img/favicon.ico"
  # share_img = "/img/share.jpg" # Default social image, used instead of logo
  # mobileMenuPosition = "left" # Side of the hamburger menu on narrow screens ("right" by default)
  dateFormat = ":date_long"
  commit = false
  rss = false
  readingTime = true
  wordCount = false
  colorScheme = "dark" # "auto", "dark", or "light"
  # mathEngine = "katex"  # "katex" (default), "mathjax" (always CDN, even with selfHosted), or "none"
  mathRender = "server"  # render KaTeX at build time; "client" (default) renders in the browser
  socialShare = false
  delayDisqus = true
  showRelatedPosts = true
  disableFigureOverride = true
  headerImgStyle = "narrow"
  showSource = false
  sourceRepo = "https://github.com/halogenica/beautifulhugo/blob/master/exampleSite/"
  since = 2023
  showPostNav = true
  toc = false
  hideAuthor = true

  [Params.search]
    provider = "fuse"

[[Params.bigimg]]
  src = "/img/triangle.jpg"
  desc = "Triangle"
[[Params.bigimg]]
  src = "/img/sphere.jpg"
  desc = "Sphere"
  position = "center top"
[[Params.bigimg]]
  src = "/img/hexagon.jpg"
  desc = "Hexagon"

# For AI search summary limits, see [Params.seo] in the SEO & i18n docs.
[Params.seo.robots]
  ai-summary-limit = "nosnippet"
  noindex = true
  nofollow = true
  
[Params.author]
  name = "SBACKP"
  website = "https://sbackp.com"
  #email = "sbackp@gmail.com"
  #github = "sbackp"
  #twitter = "sbackp"
  #linkedin = "sbackp" # company/username

# Any platform the built-in registry does not cover. "icon" is a CSS class,
# so a site can also load its own icon font from head_custom.html.
#[[Params.author.custom]]
#  href = "https://matrix.to/#/@username:example.org"
#  title = "Matrix"
#  icon = "fas fa-comments"

[markup.highlight]
  noClasses = false

[markup.goldmark.parser]
  wrapStandAloneImageWithinParagraph = false # lets Markdown attributes reach the image render hook

[markup.goldmark.parser.attribute]
  block = true

[markup.goldmark.renderer]
  unsafe = true

# Extra inline markup: ==mark==, ~~delete~~, ^sup^, ~sub~, ++insert++
# GFM strikethrough also matches a single tilde, so it must go off for ~sub~.
[markup.goldmark.extensions]
  strikethrough = false

[markup.goldmark.extensions.extras.delete]
  enable = true
[markup.goldmark.extensions.extras.insert]
  enable = true
[markup.goldmark.extensions.extras.mark]
  enable = true
[markup.goldmark.extensions.extras.subscript]
  enable = true
# Safe with math because the passthrough extension below keeps LaTeX ^ away
# from Goldmark.
[markup.goldmark.extensions.extras.superscript]
  enable = true

# Headings collected for the table-of-contents panel
[markup.tableOfContents]
  startLevel = 2
  endLevel = 4
  ordered = false

# Keeps LaTeX out of Goldmark, so the browser gets the delimiters unchanged.
# Hugo does not merge [markup] from a theme, so this must stay in the site
# configuration. The delimiters are the KaTeX auto-render defaults; single $
# is not an inline delimiter, because prose often contains a stray $.
[markup.goldmark.extensions.passthrough]
  enable = true
  [markup.goldmark.extensions.passthrough.delimiters]
    block = [['\[', '\]'], ['$$', '$$']]
    inline = [['\(', '\)']]

[[menu.main]]
    identifier = "setup"
    name = "Setup"
    weight = 1

[[menu.main]]
    parent = "setup"
    name = "Configuration"
    url = "page/configuration/"
    weight = 1

[[menu.main]]
    parent = "setup"
    name = "Pages & Layouts"
    url = "page/pages-and-layouts/"
    weight = 2

[[menu.main]]
    parent = "setup"
    name = "Theming"
    url = "page/theming/"
    weight = 3

[[menu.main]]
    parent = "setup"
    name = "SEO & i18n"
    url = "page/seo-and-i18n/"
    weight = 4

[[menu.main]]
    parent = "setup"
    name = "Comments & Social"
    url = "page/comments-and-social/"
    weight = 5

[[menu.main]]
    identifier = "features"
    name = "Features"
    weight = 2

[[menu.main]]
    parent = "features"
    name = "Shortcodes"
    url = "page/shortcodes/"
    weight = 1

[[menu.main]]
    parent = "features"
    name = "Code Blocks"
    url = "page/code-blocks/"
    weight = 2

[[menu.main]]
    parent = "features"
    name = "Markdown Extensions"
    url = "page/markdown-extensions/"
    weight = 3

[[menu.main]]
    parent = "features"
    name = "Figures & Galleries"
    url = "page/figures-and-galleries/"
    weight = 4

[[menu.main]]
    parent = "features"
    name = "Math & Diagrams"
    url = "page/math-and-diagrams/"
    weight = 5

[[menu.main]]
    parent = "features"
    name = "Publications"
    url = "publication/"
    weight = 6

[[menu.main]]
    identifier = "blog"
    name = "Blog"
    weight = 3

[[menu.main]]
    parent = "blog"
    name = "Posts"
    url = "post/"
    weight = 1

[[menu.main]]
    parent = "blog"
    name = "Tags"
    url = "tags/"
    weight = 2

[[menu.main]]
    parent = "blog"
    name = "Categories"
    url = "categories/"
    weight = 3

[[menu.main]]
    parent = "blog"
    name = "Archives"
    url = "archives/"
    weight = 4

[[menu.main]]
    name = "About"
    url = "page/about/"
    weight = 4

[outputs]
home = ["HTML", "RSS", "JSON"]
section = ["HTML", "RSS"]
page = ["HTML"]

The file path is relative to the Hugo project root. If language is omitted, it is auto-detected from the file extension. See Configuration for the full parameter reference.

Inline Code

Use backticks for inline code: hugo serve -D or console.log("hello").