A retro-looking Hugo theme inspired by gruvbox to build secure, fast, and SEO-ready websites.
This theme is easily customizable with features that any coder loves.
I took a lot of inspiration from the Hello Friend and Doks Hugo themes.
This theme is still in early development. Check out the issues to see what's still missing.
- Code highlighting with Prism
- Full-text search with Flex Search
- Display your CV using structured JSON Resume data
- Integrated image optimization with next-gen image formats and lazy loading
- Dark mode that also changes Prism themes
- Dynamic color choices from the Gruvbox color palette
- Extensible to make it suit your needs
- Responsive, mobile-first design
- Beautiful SVG icons with Tabler Icons
A big thank you to the authors of the software that make this theme possible! ❤️
The theme requires extended Hugo because it uses Sass/SCSS. You'll also have to install Go because the theme uses Go modules.
git clone
the repository andcd
into it- Run
npm ci
to install the dependencies - Run
hugo server
Create a new Hugo website:
hugo new site example.com
cd example.com/
Initialize the site as Hugo module
hugo mod init example.com
Add the following to the config.toml
file:
[markup]
# (Optional) To be able to use all Prism plugins, the theme enables unsafe
# rendering by default
#_merge = "deep"
[build]
# Merge build config of the theme
_merge = "deep"
# This hopefully will be simpler in the future.
# See: https://github.com/schnerring/hugo-theme-gruvbox/issues/16
[module]
[[module.imports]]
path = "github.com/schnerring/hugo-theme-gruvbox"
[[module.imports]]
path = "github.com/schnerring/hugo-mod-json-resume"
[[module.imports.mounts]]
source = "data"
target = "data"
[[module.imports.mounts]]
source = "layouts"
target = "layouts"
[[module.imports.mounts]]
source = "assets/css/json-resume.css"
target = "assets/css/critical/44-json-resume.css"
[[module.mounts]]
# required by hugo-mod-json-resume
source = "node_modules/simple-icons/icons"
target = "assets/simple-icons"
[[module.mounts]]
source = "assets"
target = "assets"
[[module.mounts]]
source = "layouts"
target = "layouts"
[[module.mounts]]
source = "static"
target = "static"
[[module.mounts]]
source = "node_modules/prismjs"
target = "assets/prismjs"
[[module.mounts]]
source = "node_modules/prism-themes/themes"
target = "assets/prism-themes"
[[module.mounts]]
source = "node_modules/typeface-fira-code/files"
target = "static/fonts"
[[module.mounts]]
source = "node_modules/typeface-roboto-slab/files"
target = "static/fonts"
[[module.mounts]]
source = "node_modules/@tabler/icons/icons"
target = "assets/tabler-icons"
[[module.mounts]]
# Add hugo_stats.json to Hugo's server watcher
source = "hugo_stats.json"
target = "assets/watching/hugo_stats.json"
Install the theme:
hugo mod get
Initialize the NPM package.json
and install the dependencies:
hugo mod npm pack
npm install
Run Hugo:
hugo server
Update the Hugo modules:
hugo mod get -u
hugo mod tidy
Update the NPM dependencies:
hugo mod npm pack
npm install
Two options are available to configure the theme colors:
defaultTheme
:dark
orlight
(defaults tolight
)
Default theme color for when a user visits the site for the first time. OS or user preference override this setting. See this comment for more details.themeColor
:gray
,red
,green
,yellow
,blue
,purple
,aqua
, ororange
(defaults toblue
)
Theme color for things such as links, headings etc.themeContrast
:soft
,medium
, orhard
(defaults tomedium
)
Theme background color
The theme allows customization of Prism via
config.toml
parameters:
[params]
[params.prism]
languages = [
"markup",
"css",
"clike",
"javascript"
]
plugins = [
"normalize-whitespace",
"toolbar",
"copy-to-clipboard"
]
In my opinion, this is the coolest feature of the theme. Other Hugo themes usually include a pre-configured version of Prism, which complicates updates and change tracking, and clutters the theme's code base with third-party JavaScript.
The Prism theme is not configurable because of the integration with the dark
mode functionality. Toggling between color modes swaps the Prism theme between
gruvbox-dark
and
gruvbox-light
from github.com/PrismJS/prism-themes.
Check out the Prism showcase on the Demo site for examples
After running npm install
, explore Prism features like this:
# Languages
ls node_modules/prismjs/components
# Plugins
ls node_modules/prismjs/plugins
Images are optimized by default without requiring shortcodes. A custom render hook does all the heavy lifting (see render-image.html).
By default, the theme creates resized versions of images ranging from 300 to 700 pixels wide in increments of 100 pixels.
If the image format is not WebP, the image is converted. The original file format will serve as a fallback for browsers that don't support the WebP format.
Note that only images that are part of the
page bundle are processed.
If served from the static/
directory or external sources, the image will be
displayed but not be processed.
Additionally, all images are lazily loaded to save the bandwidth of your users.
The default quality is 75%. See the
official Image Processing Config Hugo docs.
Change it by adding the following to the config.toml
file:
[imaging]
quality = 75
Change the resize behavior:
[params]
[params.imageResize]
min = 300
max = 700
increment = 100
![Alt text](image-url.jpg "Caption with **markdown support**")
The demo site features examples you can look at. I also use the theme for my website.
Add blog post covers by defining them in the front matter of your posts:
---
cover:
src: my-blog-cover.jpg
alt: A beautiful image containing interesting things
caption: [Source](https://www.flickr.com/)
---
Use the video shortcode to embed your video files from Page Resources.
With a page bundle looking like the following:
embed-videos/
|-- index.md
|-- my-video.jpg
|-- my-video.mp4
|-- my-video.webm
You can embed my-video
like this:
{{< video src="my-video" autoplay="true" controls="false" loop="true" >}}
The shortcode looks for media files matching the filename my-video*
. For each
video
MIME type file, a <source>
element is added. The first image
MIME
type file is used as poster
(thumbnail). It will render the following HTML:
<video
autoplay
loop
poster="/blog/embed-videos/my-video.jpg"
width="100%"
playsinline
>
<source src="/blog/embed-videos/my-video.mp4" type="video/mp4" />
<source src="/blog/embed-videos/my-video.webm" type="video/webm" />
</video>
You can set a Markdown caption
, wrapping the <video>
inside a <figure
>.
Additionally, the shortcode allows you to set the following attributes:
Attribute | Default |
---|---|
autoplay | false |
controls | true |
height | |
loop | false |
muted | true |
preload | |
width | 100% |
playsinline | true |
Learn more about the <video>
attributes here.
Due to the European Copyright Directive it is required to opt into displaying snippets in search engine results.
By default, every page (except 404) includes the
index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1
robots meta value, opting into all snippet features.
You can override the robots meta value in the front matter of your pages:
---
robots: noindex, nofollow
---
Configure social share links in the Hugo config like this:
[params]
[[params.socialShare]]
iconSuite = "simple-icon"
iconName = "facebook"
formatString = "https://www.facebook.com/sharer.php?u={url}"
[[params.socialShare]]
iconSuite = "simple-icon"
iconName = "reddit"
formatString = "https://reddit.com/submit?url={url}&title={title}"
[[params.socialShare]]
iconSuite = "tabler-icon"
iconName = "mail"
formatString = "mailto:?subject={title}&body={url}"
Use the iconSuite
option to specify the icon suite used for the social share
link: simple-icon
or tabler-icon
. Select an icon from the suite with the
iconName
option.
The formatString
supports the following placeholders:
{url}
is replaced with the.Permalink
of the post{title}
is replaced with the.Title
of the post
To enable social share links, set the following in the post's front matter:
---
socialShare: true
---
Check out the Social Share URLs repo on GitHub for more format strings.
The favicons and corresponding markup were generated with the free RealFaviconGenerator.net.
The easiest way to replace the default favicons is to generate them using
RealFaviconGenerator.net and put the generated files into the static/
directory.
You can extend the theme by overriding the following partials in the
layouts/partials
directory which by default are empty placeholder files:
head/head_start.html
Custom HTML at the start of<head>
head/head_end.html
Custom HTML at the end of<head>
footer_end.html
Custom HTML at the end of<body>
comments.html
Comments at the end of posts
KaTeX is a fast, easy-to-use JavaScript library for TeX
math rendering on the web. Let's add it to the theme via npm
. First, add the
following to the package.hugo.json
file:
"dependencies": {
"katex": "^0.16.8"
}
Then run hugo mod npm pack
to sync the package.hugo.json
dependencies with
package.json
. Run npm install
after. We then need to mount the
node_modules/katex
folder into Hugo's virtual filesystem by adding the
following to the config/_default/module.toml
file:
[[mounts]]
source = "node_modules/katex"
target = "assets/katex"
We can then add the following to layouts/partials/head/head_end.html
:
{{ if .Params.katex }}
{{ $katexCSS := resources.Get "katex/dist/katex.min.css" }}
<link
rel="stylesheet"
href="{{ $katexCSS }}"
{{ if hugo.IsProduction }}
integrity="{{ $katexCSS.Data.Integrity }}"
{{ end }}
crossorigin="anonymous"
/>
{{ $katexJS := resources.Get "katex/dist/katex.min.js" }}
<script
defer
src="{{ $katexJS.RelPermalink }}"
{{ if hugo.IsProduction }}
integrity="{{ $katexJS.Data.Integrity }}"
{{ end }}
crossorigin="anonymous"
></script>
{{ $autoRender := resources.Get "katex/dist/contrib/auto-render.min.js" }}
<script
defer
src="{{ $autoRender.RelPermalink }}"
{{ if hugo.IsProduction }}
integrity="{{ $autoRender.Data.Integrity }}"
{{ end }}
crossorigin="anonymous"
onload="renderMathInElement(document.body);"
></script>
{{ end }}
The only thing left is enabling KaTeX in the front matter of our content:
---
title: "Hello World"
description: "The first post of this blog"
date: 2021-03-14T15:00:21+01:00
draft: false
katex: true
---
I'm a .NET developer by trade, so let's say hello in C#!
The theme comes with a tag cloud partial. It is included in the sidebar, but it
is disabled by default. If you wish to configure it, add the following to the
[params]
section in the config.toml
file:
[params.tagCloud]
enable = true
minFontSizeRem = 0.8
maxFontSizeRem = 2.0
If you want to get rid of the sidebar, add an empty data/json_resume/en.json
file with the following content:
{
"$schema": "https://raw.githubusercontent.com/jsonresume/resume-schema/v1.0.0/schema.json",
"basics": {},
"work": [],
"volunteer": [],
"education": [],
"awards": [],
"certificates": [],
"publications": [],
"skills": [],
"languages": [],
"interests": [],
"references": [],
"projects": [],
"meta": {
"canonical": "https://raw.githubusercontent.com/jsonresume/resume-schema/master/resume.json",
"version": "v1.0.0",
"lastModified": "2017-12-24T15:53:00"
}
}
The theme uses PostCSS with following plugins:
Additionally the following plugins are used if building the site with
hugo -e production
:
- postcss-preset-env
- cssnano for minification
- @fullhuman/postcss-purgecss
Inside the assets/css
two folders exist, critical
and non-critical
. Files
inside critical
are concatenated during build time and inlined into the
<head>
element. The styles target mostly
above the fold content.
Try to keep inline CSS to a minimum because it can't be cached and will be
inlined into every single page. Files inside non-critical
are concatenated
into a single file and included as <style>
. Most of the styles are in there.
Files are concatenated in lexicographic order of their file names. File names
start with two digits and a hyphen: NN-
. The order of files might differ
between Linux and Windows, so using this convention improves cross-platform
compatibility.
You might know this approach if you're familiar with Xorg.
You can add new CSS files to the PostCSS pipeline like this:
critical/50-foo.css
non-critical/05-bar.css
non-critical/99-last.css