Background
I’ve created a few different versions of my blog using blogdown. Creating new blog with blogdown is so simple, but customizing can be finicky. Now blogdown 1.0, but was still sitting in a stack of posts that never made their way to the web when I started writing this 😳 Now a year later I’m hoping to use blogdown internally at my new job to disseminate analysis more easily and I could really use that post as a guide 😱 So, for posterity I’m going to document the process as I go through it. If you’d like to learn how to create blogdown site or learn how to customize on your existing blogdown site, read on.
What is blogdown?
blogdown is an R package witten by Yihui Xie that wraps the Hugo web development framework. Blogdown provides a tool for R developers to build their own blogs and portfolio sites from Hugo templates. There are several other R libraries to consider for publishing including distill, bookdown, and hugodown. We’ll cover blogdown only in this post.
Learning blogdown
If you’re new to blogdown I recommend this article from Alison Presmanes Hill Hill. Alison spent a few years working on the RStudio team and her blog is loaded with blogdown walk-throughs and gotcha’s. I highly recommend looking through her blog for other content. Additionally, Yihui wrote a blogdown book that I always keep handy. Finally, I occasionally look through Yihui’s stack overflow answers just to find tidbits of info about how rmarkdown, knitr, blogdown, etc. really work because they are so complicated and black-box like… It’s not specifically a resource for blogdown, but blogdown relies on the knitr and rmarkdown infrastructure so it helps to understand those packages behind the package. Aside from that, blogdown relies on rmarkdown. If you’ve not used rmarkdown before I recommend at least going through a bit of this guide from RStudio.
What is Hugo?
Hugo is a web development framework used to build static websites. It’s is one of the most popular static site generators available at this time. If you’re looking to use blogdown, you do not necessarily need to know almost anything about Hugo to succeed. However, the more you understand Hugo the easier it becomes to customize your site, debug, etc.
Learning Hugo
I’ll cover some of the basics in the next section, but I’ll barely scratch the surface on Hugo. If you want to learn more, the Hugo documentation and a youtube playlist Hugo provides are both good resources. I also found this tutorial by Adi Purdila useful. I haven’t gone through and built my own Hugo site outside blogdown, but just watching the tutorial helped me understand how Hugo works for customization. This is probably a great starting point if you want to learn how to build your own template, but I’m not going there just yet…
Prerequisites
If you’ve never done web development at all there are some basic concepts and tools that are important to understand, but given that most of us have been on the internet for a few decades the learning curve isn’t too bad. I am not a web developer and have no business teaching web development so please take all this with a grain of salt. The reason I decided to spend time on it is simply that building and customizing a blogdown site requires very basic knowledge of web development. As I was writing the walk through I kept coming back to these concepts and it felt disingenous to write the post without making some attempt to cover them - knowing and acknowledging that my attempt will probably be clumsy at best 😬
HTML (HyperText Markup Language)
HTML is the primary building block of web pages. There’s a ton to learn about HTML. I recommend w3school’s HTML tutorial if you want to get a broad understanding. The two things I’d definitely recommend reviewing are HTML elements (aka HTML tags) and HTML attributes. I always keep these lists of HTML elements and HTML attributes handy.
CSS (Cascading Style Sheets)
CSS is code used to describe how html elements are to be displayed on a webpage, CSS “styles” a webpage. I borrowed the below image from a css-tricks post, but the image on the right shows what Wikipedia’s homepage would look like without CSS.
Again, I recommend w3school’s CSS tutorial to get a broad understanding of CSS. If you want to do something to style your site search it and don’t be afraid to post on stackoverflow. I find a lot of really specific CSS implementations on there that I’ve tried to jam into my personal site over time. Time more you do it the easier it gets. What you need to know in CSS can be especially specific to your goal, but I’d recommend reviewing CSS syntax and CSS selectors regardless because they’re very useful and fundamental.
Browser Developer Tools
The browser web developer tools that come in your standard issue web browser are critical to working on web development. The most commonly used tool within the set is the element inspector which usually is represented by a little square with an arrow in the corner. This video shows a few ways you can use the element inspector.
Using the element inspector allows you to find code related to an element so you can understand the code and make changes. We can try it on David Robinson’s website by opening web developer tools, selecting the image of David, and changing the image link to another image on the web.

Changing the content can be useful for obvious reasons, but we can also modify the CSS around objects. We might not want the radial border around the image. We can find that CSS property if we select the image and look in the styles tab. Finding the right CSS property can require some knowledge, but it’s usually not too hard to figure looking at the different names and tinkering. David’s picture is inside an html img
tag which contains a class class="bio-photo
. The styles tab has a CSS selector called .article-author-side .bio-photo
. That CSS selector has a property called border-radius
. Un-check that property and see what the site would look like without it. A note, many of the bigger sites use system generated class names and things of that nature which can make this approach break down quickly.

We can also add new properties to see what a change might look like. Knowing all the styling options you can use is overwhelming, but just search by the html tag name, in this case img
, and “CSS properties” (i.e. search img CSS properties) and you’ll usually get useful results. On this one I ended up back at w3schools CSS tutorial.

We could probably take this pretty far, but I’ll cover one more CSS change. We can change the fonts of text in the site using the same basic approach. Fonts have several CSS properties, but the property that is similar to “font” inside of a word processing software is font family. Just like most areas in technology, fonts can get very “deep.” To my understanding, no fonts are perfectly web safe, meaning they may not work as intended in some browsers or contexts. These fonts are considered the safest, but they’re also a bit bland which might not be the best choice for all sites. Regardless of what font you choose, always have a few fallback fonts. Meaning, instead of deleting the font family listed, we can just add our new font to the front. There are obvious style considerations like this new font looking way different than the fallback, but that’s a different discussion. When setting fonts in the CSS, include a fallback that you intend to be the fallback rather than relying on the browser’s default. If you want to find reasonably web safe fonts, Google fonts has a lot of options.

Testing
While we’re building our site we’ll want to test that it works. RStudio has a viewer pane that’s closest to development, which I love. However, there are always little differences that can pop up between what we see in RStudio and what shows up in the browser when it gets there. I recommend testing in a browser often. I use Brave as my primary web browser, but I also use Chrome and Safari to test my pages during web development because most users will likely be in one of the two.

New Project
Beyond that, I also like to do a little bit of device testing. I don’t expect I have a ton of mobile users, but I still don’t want to have an awful mobile experience if I can help it. I use a tool called LT Browser to quickly see how the site will look on various devices.
Hugo Basics
As noted, I’ve never built a Hugo site without blogdown so take all my Hugo advice and instruction with a block of salt. That said, there are a few Hugo concepts that you do have to understand if you want to build a site with blogdown and / or customize the site. I’d recommend reading Alison’s article How much hugo do I need to know?. This will help you decide what parts you want to spend time trying to understand. I’ll be referencing parts of this article later as well, so it provides helpful context. Aside from that, here’s my high-level understanding of these concepts gives you more than enough context for the walk-through.
Themes
Hugo sites are built off of themes, which I think of as a website template. Hugo provides mechanics to use a single template for many purposes by changing parameters before rendering the site. Hugo provides a list of themes or you can build your own. When selecting a theme to use, I recommend going to the github page and looking at the commit history, the contributors, and the documentation to assess how much you can rely on the theme. It’s obviously just a guess regardless, but I think it’s a good idea. Regardless, once you select a theme, test it with blogdown before you do anything else. Many themes don’t work with blogdown out of the box. I honestly don’t know exactly why, but I’d guess reasons vary from theme to theme.
Config file
Hugo uses a config file with a list of variables that allow users to easily customize their site. For example, the variable title
is used to set the title of your site that shows up in search engines and on the browser tab. Themes generally come with a config file, but the variables included vary from theme to theme. The config variables are provided by Hugo so they can be added, but themes can also provide custom variables called params. Params can be in the config file or in a separate file in the config directory. Configs generally contain a menu which is how the site menu (i.e. navigation bar) is configured. The menu also has variables provided by Hugo and can be broken into a separate file in the config directory. This site is small so I don’t expect the config file to get too big, but for larger sites the config can be broken into several files.
Static Files
Static files get served as-is on the site root. This is useful for storing resources in the site’s root. The resources can be images, JS, CSS, etc. (e.g. an image on the home page).
Content
The content is the substance of the site and Hugo assumes so organizational structure of the site which is explain here. This part seems complicated, but in my experience this is the easiest part to understand when using blogdown.
Templating
Templating is basically Hugo’s engine. It seems very intimidating when you start using blogdown, but it’s honestly not - we’re just not comfortable enough (or at least I’m not) with all the HTML, CSS, & JS to fully understand how simple Hugo is. Hugo uses templated HTML in combination with other resources in order to dynamically build webpages when the site gets built. Sounds kind of crazy, but if you use glue or jinja it’s pretty familiar. Template variables and functions are accessed within {{ }}
.
Build a site from scratch
Alright I’ve laid out a lot of background info that most of you probably skipped over (no judgement) so let’s get right into it. I’m building this site using hugo-lithium because it’s minimalist and supported by RStudio.
Project setup
In Rstudio, go to File -> New Project and there is a simple template. Alternatively, we can use the function blogdown::new_site()
. Once the site is built, check that the template is working properly by running blogdown::serve_site()
. For more details refer to the book.

Blogdown will automagically setup a repo structure (create folders and files) for the site, similarly to how devtools::create_package()
does for R package development if that’s a familiar process. Since blogdown is a wrapper around Hugo, the repo has resources for both blogdown and Hugo. I’ll get into more detail as we customize certain parts of the site, but Alison provided an easy cheat sheet for the folder structure in the below tweet.
An #rstats #blogdown file hierarchy cheatsheet:
— Alison Presmanes Hill (@apreshill) December 28, 2018
├─ archetypes <- edit me!
├─ config.toml <- edit me!
├─ content <- edit me!
├─ data <- edit me!
├─ layouts <- edit me!
├─ public <- ignore me!
├─ static <- use me! (png/pdf/csv/xls)
├─ themes <- don't touch! pic.twitter.com/gvVA703Lwa
Content
The navbar spans across all the pages, but the content is each of the pages under the navbar. Site content goes in, you guessed it, ~/content/
. The site will have an about page and posts by default, but you can add as many pages directly under ~/content/
as you’d like. Just don’t forget to add them to the menu in your config.yaml
so the user can navigate to them. Beyond pages directly under ~/content/
, the Hugo theme will generally have some default folders including ~/post/
, which is the default location for blog posts.
New Posts
To create posts, I typically copy old posts because I customize my yaml header a bit. However, for a new site you can either copy one of the demo posts included or just use the RStudio add-in (shown below).

Permalinks
Each post’s url is generated by by Hugo using the the config.yaml
and the document slug from the post’s yaml header. So if our site was www.mark-druffel.com
and we used the configuration below, a post url would be www.mark-druffel.com/2021/10/15/this-posts-title
. I personally find that to be overkill because I don’t post multiple times a day and my titles aren’t likely to be the same so I just use post: /:year/:slug/
for my blog.
permalinks:
post: /:year/:month/:day/:slug/
Styles
There are a number of changes that can be made to improve the look and feel of all pages under ~/content/
, but for this site I’m sticking very close to the theme. Just know, you can change any of the CSS styles the same way we did with the navbar to apply CSS properties across all the content on the site! The one thing we will change for this site is fonts.
Fonts
We already changed fonts in the navbar so I won’t walk back through the entire process, but here’s the CSS code I used to modify fonts throughout my content. I just repeated the some process of finding parts of the page I wanted to modify, finding the CSS selectors, importing the font I wanted, and applying it to the CSS selector.
.nav-links li {
display: inline-block;
margin: 0 0 0 15px;
font-family: Rubik, Overpass;
font-size: 1.25rem;
}
h1, h2, .article-title {
font-family: Roboto, Merriweather;
font-size: 1.5rem;
font-weight: bold;
}
h3, h4, .article-duration, .archive-item-link {
font-family: Roboto, Merriweather;
font-size: 1.3em;
font-weight: bold;
}
h5, h6, .footer {
font-family: Roboto, Merriweather;
font-size: 1rem;
font-weight: bold;
}
.article-date {
font-family: Roboto, Merriweather;
font-size: .9rem;
}
body {
font-family: Roboto, Merriweather;
font-size: 1rem;
}
code {
font-family: Fira Code, Source Code Pro, monospace;
font-weight: 300;
}
Now we have a blog with a custom navbar and fonts throughout all of the content. However, you might notice some content doesn’t pick up our CSS configurations.

Style Raster Graphics
When we publish blog posts we’ll render all sorts of content using R libraries. Unfortunately, the outputs from our R code may not easily integrate to our site and this can be one of the trickier parts of maintaining a nice looking blog, especially if you choose a highly styled theme 😬 In this case, the image a raster graphic. Raster graphics are image files (pixels), which have no use for CSS code. We need to apply the same properties to our R graphics to match the style of the site. This applies to any plotting using the graphics device including ggplot2 😲

We can resolve this problem a few different ways, but the first and most obvious approach is to customize the ggplot2 theme. The theme() function in ggplot2 provides a huge number of settings that can be used to customize the output including color, font, background, etc. This really becomes more of a ggplot2 tutorial, so I’m going to skip a bit of the details but the aforementioned resources should provide more than enough to customize your ggplot2 output for your blog. I try to be DRY (do not repeat yourself) when possible so I created a few themes for my blog for different visual types (e.g. one for standard visuals, one for faceted plots, one for non-grid based plots like network diagrams, etc.). Below is a gist of my standard theme if it’s a helpful starting point. You can choose existing themes and add onto them as show below (i.e. theme_minimal() + theme(...)
).
blogdown::shortcode("gist", "mark-druffel", "83b839abe61d056a2b4cef81d09f7bcc")
Depending how you style your blog, I think you could also use thematic and bslib, but since the blog doesn’t use bootstrap by default I personally haven’t tried to do that… Aside from the theme, some other helpful resources when using ggplot2 in blogdown include ggthemes, ragg, showtext, and ggtext.
ggthemes
Library with complete ggplot2 themes including pre-built color scale functions. I don’t use ggthemes on my blog, but you could adopt a theme from there if any fit the style of your site which would simplify things tremendously.
ragg
The ragg library will simply improve R’s raster graphics rendering performance and quality. It will make ggplot2 output look sharper.
showtext
The showtext library makes it easier to use non-standard fonts with the R graphics device. Again, our site’s CSS is not available to ggplot2 when we render plots. Therefore, we’ll have to either install fonts on our machine (similar to adding them to a folder for the site) or load them from an API (similar to importing them in our CSS from the fonts API). Then we need to make them available to the R session. You can see an example in the package vignettes. Instructions for installing fonts on a machine will vary by OS, but it’s very simple to do on Linux, Mac, or Windows if you just use a search engine.
ggtext
The ggtext library allows the use of markdown syntax inside ggplot2. This does not change your theme, it’s just an easy way to bold titles and things like that on an individual plot.
Ok let’s look apply this stuff to our aforementioned plot from the demo post. Open ~/content/post/<date>-r-markdown/index.Rmd
. Load the necessary libraries, load the fonts, run showtext_auto()
to make the fonts available, and create a theme function you’ll use on your plot. I used a theme based on theme_void()
since the demo is a pie chart.
library(ragg)
library(showtext)
library(tidyverse)
library(ggrepel)
# Tell knitr to use ragg for graphics
knitr::opts_chunk$set(dev = "ragg_png")
# font_add_google is a function in sysfonts which is loaded by showtext
font_add_google("Rubik", "rubik")
font_add_google("Roboto", "roboto")
font_add_google("Fira Code", "firaCode")
# Adding a noticeable font just to make sure everything is working
font_add_google("Homemade Apple", "homemadeApple")
showtext_auto()
theme_void_blog <- function(...){
ggplot2::theme_void() +
ggplot2::theme(
text = ggplot2::element_text(
family = "roboto",
size = 11,
inherit.blank = FALSE),
title = ggplot2::element_text(
family = "homemadeApple",
face = "bold",
size = 18,
inherit.blank = FALSE,
hjust = .5),
plot.subtitle = ggplot2::element_text(
family = "homemadeApple",
size = 14,
inherit.blank = FALSE),
legend.title = ggplot2::element_text(
family = "rubik",
size = 12,
face = "bold",
inherit.blank = FALSE,
hjust = .5),
legend.text = ggplot2::element_text(
family = "roboto",
color = "#33334D",
size = 10, hjust = 0)
) +
ggplot2::theme(...)
}
This demo file uses the pie()
function from base R. Nothing wrong with the function, but I’m not as familiar with the base R plotting tools so I’m going to use ggplot2 instead. The labels actually aren’t as simple with ggplot, but I just used ggrepel to make the labeling simpler. The new fonts should load and mostly the same visualization should render with your new fonts.
tibble::tibble(
#value = c(280, 60, 20),
value = c(280, 60, 20),
name = c('Sky', 'Shady side of pyramid', 'Sunny side of pyramid'),
color = c('#0292D8', '#F7EA39', '#C4B632')
) |>
ggplot(aes(x = "", y = value, label = name, fill = color)) +
geom_col() +
coord_polar(theta = "y", start = pi / 1.275, direction = -1) +
scale_fill_identity() +
geom_text_repel(family = "roboto", size = 5, nudge_x = .6, show.legend = FALSE, segment.color = "transparent") +
labs(title = "To make sure showtext is working") +
theme_void_blog()
Style Vector Graphics
R libraries don’t just produce raster graphics like plots from ggplot2, many libraries use the htmlwidgets framework which uses HTML, CSS, and JavaScript to produce interactive visualizations / widgets. Depending on exactly how the widget is set up, it will inherit CSS properties from your site. However, you may want to create new CSS properties or apply existing properties to specific parts of the widget.
If you add the code below to a new block inside ~/content/post/<date>-r-markdown/index.Rmd
and run you’ll create a d3.js scatter plot from the metricsgraphics library.
library(metricsgraphics)
mtcars %>%
mjs_plot(x=wt, y=mpg, width=600, height=500) %>%
mjs_point(color_accessor=carb, size_accessor=carb) %>%
mjs_labs(x="Weight of Car", y="Miles per Gallon")
If you inspect the CSS carefully you can see the CSS properties we added in the apply new fonts section.

That’s likely the behavior we’d want. That said, if we wanted to modify the font of the axises, for instance, we could add CSS properties to the classes of the htmlwidget like so. Many htmlwidget libraries provide tools to do this directly, for instance metricsgraphics::mjs_add_css_rule()
, but it’s good to know how to do it directly in the Rmarkdown document as well because sometimes that’s necessary or easier.
.mg-x-axis {
font-family: Homemade Apple;
}
.mg-y-axis {
font-family: Homemade Apple;
}

CSS Conflicts
Some htmlwidgets have generic CSS selectors that are in main.css
, but no way to modify them through the library. An example I’ve bumped into in the past was the library skimr. These selectors were probably chosen intentionally to try to easily match site styles, but sometimes it doesn’t look great. To avoid those conflicts, throughout your entire main.css
file you can use a prefix for the site. For example, you could change .header
to .site-header
.
# Find and replace ".header {" to ".site-header {"
usethis::edit_file(main_css)
If you do that, you’ll have to find references of that class in your html partials. First, move the partial from the theme as we’ve mentioned before. WARNING, WE MODIFY THIS FILE SEVERAL PLACES. DO NOT RUN THIS CODE IF YOU ALREADY MOVED THIS FILE INTO YOUR PARTIALS FOLDER, IT WILL ERASE YOUR WORK!
fs::file_copy(paste0(here::here(),'/themes/hugo-lithium/layouts/partials/header.html'), paste0(here::here(), '/layouts/partials/header.html'))
You’d need to modify header.html
to the code below by changing the <header class="header">
to <header class="site-header">
.
<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode }}">
<head>
{{ partial "head.html" . }}
</head>
<body>
<div class="wrapper">
<header class="site-header">
{{ partial "nav.html" . }}
</header>
Add Multimedia
Now, let’s move onto the really hard hitting content like emojis! 😂 As you’ve seen throughout this post there’s a lot of multimedia you can use on a page. Hugo provides support for a number of things that blogdown wraps directly like emojis, tweets, youtube, etc. Beyond Hugo and blogdown, there are several R libraries that can provide additional content such as fontawesome.
I’m going to modify the about page to demonstrate a few of these tools, content/about.md
. Notice the about page is a .md
file not an .Rmd
file. Vanilla .md
files can be used for simple pages with just text, images, emojis, but they will not have access to R. I generally use .Rmd
files for everything, but .md
are less complex and render much more quickly so keep that in mind.
To demonstrate all the multimedia functionality that comes to mind, I’ll need an .Rmd
file so delete the .md
version (optional) and create about.Rmd
. In about.Rmd
, first we setup our yaml header. There are a number of options that can be set in the yaml header, refer to the rmarkdown cookbook for more details - these yaml headers provide a lot of customization features out of the box.
---
title: "About"
---
Under our header we can start to create our about page. We can use Hugo’s built in emojis in our sentences to insert emojis that are words wrapped in colons anywhere in the document. For these to work, we need to add a line to config.yaml
.

The list of emojis supported by Hugo is small, but we can use the fontawesome package to insert fontawesome emojis. To find new fontawesome icons search here or use fontawesome::fa_metadata()
. Note the heart in the code below is an emoji in the Hugo supported syntax, since blogdown parses the entire document it will add emojis in code blocks as well.
This is a blog I have created to share analytics with my team members. Although we have and ❤️ Apache Superset & shiny, sometimes I just want to quickly share analysis I perform in `fontawesome::fa("r-project", fill = "steelblue")`.
Hugo has a shortcode feature that allows users to pull in content from a number of platforms. The blogdown::shortcode()
function is a wrapper to use it. It can be used to embed tweets, figures, youtube videos, gists, etc.
blogdown::shortcode("youtube", "2xkNJL4gJ9E")
It can also be used for syntax highlighting languages which may not be supported by knitr.
blogdown::shortcode("highlight", "bash", .content = "echo Thanks for visiting our new site!;")
echo Thanks for visiting our new site!;
You might be thinking, shortcode doesn’t support GIFs!?!@ Agreed, but don’t worrying GIFs are simple to add without using shortcode. They are an image and therefore work exactly like image files do. Add, {<add CSS properties}
, to a new line in your document - not in a code block. In the CSS properties you can add anything you want, but I typically use it to specify width like {width=500px}
. If you want to center the image, one simple way is to just wrap the line in <center> </center>
tags. You can save the giph in a folder in the post folder, or just link to a direct url. Giphy provides GIF Links if you hit the share button.

Markdown Formatting
There are so many guides on markdown formatting and I’m guessing most users will already know a little so I’m going to cut this section short. If you’re new to markdown, checkout this guide. If you’re like me, you probably know markdown pretty well but always bump into weird pesky things. I’m going to cover a few little tricks I’ve picked up that I find myself looking up a lot.
Section Links
Sometimes you want to refer a user to a prior section. Do this the same way you add a hyperlink, but replace spaces in the section name with hyphens. For example, to refer someone back to the beginning of this entire section I would use [this](#build-a-site-from-scratch)
. which does this.
Bullets
I often add bullets and the style looks terrible because markdown isn’t actually recognizing them as bullets. This is caused by two mistakes I commonly make:
- Keep one line between the paragraph above the bullets and the bullets themselves.
- Make sure you end the above paragraph and each bullet with a linebreak.
Linebreaks
According the markdown documentation:
CommonMark and a few other lightweight markup languages let you type a backslash () at the end of the line, but not all Markdown applications support this, so it isn’t a great option from a compatibility perspective.
The markdown docs instead recommend using two spaces (which is impossible to see in the IDE) or
at the end of a line. I’ve gotten into the habit of using backslash, but
seems like a good option as well.
Other Styling & Components
Browser Tab Title & Image
You may notice when you preview the site there’s a little Li
logo and a title A Hugo Website
.

We can change the logo by creating a favicon logo and adding it. This site can convert images to icon files. Save the image in /static
and add the appropriate reference to the config file. Also, change the text in the config file title
.
title: DocStation Analytics
params:
favicon: images/favicon.ico
Code Folding
This section modifies the Hugo theme substantially more so than the other sections up to this point. It seems like a lot more work, but it’s very simple. Just make sure to follow along and do all of the steps unless I specifically say they’re optional because, if you miss any, the code show/hide button might not show up at all which makes it harder to troubleshoot. I never would have figured out how to do this without Sebastien Rochette’s work he published on his blog, so thanks so much to him! I also want to thank James Pustejovsky who also used Sebastian’s post and did his own incredibly helpful post.
High Level Overview
The codefoling is made possible by a JS function, we’ll call it codefolding.js
, which searches each page for code based on the HTML classes assigned by blogdown. When it finds code, it wraps it in an HTML div and adds a button. The JS function collapses the div when the button is clicked. That JavaScript function needs to be available to the pages, which means it needs to be in the header or footer of each HTML page. Further, that function relies on other code from bootstrap and custom CSS which need to be loaded first.
Add Dependencies
Sebastian wrote his codefolding blog post using Bootstrap 3 relying on collapse.js
, dropdown.js
, and transition.js
, the last of which was deprecated in Bootstrap 4. I’m not enough of a Bootstrap guru to know where that functionality lives in Bootstrap 4 and frankly didn’t want to bother working through that for this one feature on my site - so I just stuck with Bootstrap 3, it works just fine. Those bootstrap functions rely on jquery so you need to download that as well. Finally, we’ll need popper.js to allow scrolling within our code boxes, but James’ post provided an API so we’ll just add it in our partials. For posterity, you can just download the bootstrap.min.js file, but I stuck with the three functions only since that’s all I needed.
- Bootstrap 3 Latest Version
- Jquery - I used 3.6.1
Add the JS files to the static/js
folder. As we learned when changing the home button, the static/js
files will override the files inside ~/themes/hugo-lithium/static
. I this case, those files aren’t in ~/themes/hugo-lithium/static/images/logo.png
so they’re simply additive. Again, you only need the three specific files or bootstrap.bundle.min.js
, not all four as shown below.

Aside from JS, we’ll also need some CSS to format the div around the code block and the button that controls it. Ideally, we’d just use a recent Bootstrap distribution like this which does work, but it conflicts with other parts of the theme - for me it knocked things out of place in the navbar. We’d need to modify our main.css
to manage the conflicts between that file and the boostrap file which could be a lot more work than just creating a custom CSS file for the code folding, which is what I decided to do. Copy the code below and save it at ~/static/css/codefolding.css
. If you want to make your button look different (e.g. different color, hovering behavior, etc.), this is the place to do that.
#code-folding-buttons {float: right;}
#code-folding-buttons .pull-right > .dropdown-menu {
right: 0;
left: auto;
}
#code-folding-buttons .btn.btn-default.btn-xs.dropdown-toggle {
float: right;
}
#code-folding-buttons.open > .dropdown-menu {
display: block;
}
#code-folding-buttons .dropdown-menu li {
padding: 5px;
}
/* Each code block button */
.code-folding-btn {
margin-right: 10px;
margin-bottom: 4px;
width: 125%;
background: #fff;
border-top: #252525 1px solid;
border-bottom: #252525 1px solid;
border-left: #252525 2px solid;
border-right: #252525 2px solid;
border-radius: 10px;
text-align: center;
color: black;
padding: 6px;
float: right;
font-family: Rubik;
font-size: .75rem;
font-weight: 400;
}
.code-folding-btn:hover {
background: #252525;
border-top: #fff 1px solid;
border-bottom: #fff 1px solid;
border-left: #fff 2px solid;
border-right: #fff 2px solid;
color: white;
font-family: Rubik;
font-size: .75rem;
font-weight: 600;
}
.row {
display: flex;
border-bottom: solid 1px #d7d7d7;
}
.col-md-12 {
margin: 0 0 0 auto;
}
.collapse {
display: none;
}
.in {
display: block ;
border: solid 1px #d7d7d7;
border-radius: 5px;
}
/* Main show/hide button */
#code-folding-buttons {float: right;}
#code-folding-buttons .dropdown-menu {
min-width: 80px;
padding: 5px 0;
margin: 2px 0 0;
font-size: 14px;
}
Add Modify Partials
Now that we have dependencies in place, we need to add the necessary HTML partials so that the dependencies are added when the site is rendered by Hugo. You can structure your partials however you want, but each theme generally has things structured a certain way that might be easy to piggy back off of. Below is some R code to copy files from the existing theme (hugo-lithium specific) and create new files we need.
fs::file_touch(paste0(here::here(), '/layouts/partials/header_maincodefolding.html'))
fs::file_copy(paste0(here::here(),'/themes/hugo-lithium/layouts/partials/head_custom.html'), paste0(here::here(), '/layouts/partials/head_custom.html'))
fs::file_copy(paste0(here::here(),'/themes/hugo-lithium/layouts/partials/header.html'), paste0(here::here(), '/layouts/partials/header.html'))
Notice partials have if statements in them, such as {{ if eq .RelPermalink "/" }} ... {{ else }}{{ if .Description }} ... {{ end }}{{ end }}
. These if statements reference variables in config.yaml
, it’s how the config.yaml
is able to parameterize a Hugo site. When the site is rendered, Hugo reads the yaml and includes HTML partials based on the if statements. I actually don’t use parameters for my codefolding, but Sebastien set them up in his post and I left them in here for people who may want them - and to just better illustrate how Hugo works. You can completely remove all the if statements in the partials we create and leave the parameters off of the config file if you like.
head_custom.html
The lithium template has a head_custom.html
file by default, which is empty. It’s already loaded into the site so we don’t have to modify / create as many partials to implement our code folding by using it. We can use this to load dependencies into the site’s header. First, we’ll load the jquery, bootrap, and popper.js dependencies. Next, we load codefolding.js
and codefolding.css
. Finally, we can run codefolding.js
(i.e. within $(document).ready
). If you don’t plan to use config.yaml
to disable code folding on certain posts you can omit the if statements.
{{ if not .Site.Params.disable_codefolding }}
<script src="{{ "js/jquery-3.6.0.min.js" | relURL }}"></script>
<script src="{{ "js/transition.js" | relURL }}"></script>
<script src="{{ "js/collapse.js" | relURL }}"></script>
<script src="{{ "js/dropdown.js" | relURL }}"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"></script>
{{ end }}
{{ if and (not .Site.Params.disable_codefolding) (not .Params.disable_codefolding) (in (string .Content) "</pre>") }}
<script src="{{ "js/codefolding.js" | relURL }}"></script>
<link rel="stylesheet" href="{{ "css/codefolding.css" | relURL }}" />
<script>
$(document).ready(function () {
window.initializeCodeFolding("show" === {{ if isset .Params "codefolding_show" }}{{ .Params.codefolding_show }}{{ else }}{{ default ("hide") .Site.Params.codefolding_show }}{{ end }});
});
</script>
{{ end }}
The head_custom.html
partial is pulled into the ~/themes/hugo-lithium/layouts/partials/head.html
partial, which we don’t need to modify or move out of themes. It’s loaded on the last line below with no if statement (i.e. it’s always loaded).

header.html
The aforementioned head.html
, and by extension head_custom.html
, are loaded into header.html
(these names are a bit tough to keep straight). There’s no need to make changes to header.html
unless you wanted to add the button to collapse and show all code, which is included below.
<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode }}">
<head>
{{ partial "head.html" . }}
</head>
<body>
<div class="wrapper">
<header class="header">
<!-- Add this if you want the butto to callapse and show all code on a page -->
{{ partial "header_alldoc_codefolding_button.html" . }}
{{ partial "nav.html" . }}
</header>
Setup Config
If you included the if statements in the partials, add these parameters to config.yaml
at level 1 (i.e. not nested underneath anything in the file).
# Set to true to disable code folding
disable_codefolding: false
# Set to "hide" or "show" all codes by default
codefolding_show: hide
codefolding_nobutton: true
And voila, you have code folding!

Pages with code should now have a button beside the code which will expand and collapse the code.

Misc Configs
There are so many configurations you can use in the lithium theme and even more throughout Hugo.
disqusShortname
Disqus is a simple way to add a comments box on your site. Disqus has helped me engage with other data scientists through their sites which is a super simple way to connect with other people in the profession. It’s so simple, I highly recommend it. To use Disqus on your site, you need to create an account and then create a new site on that account. When you create a site, you give it a shortname which you can then add to the config.yaml
.
baseurl: /
languageCode: en-us
title: DocStation Analytics
theme: hugo-lithium
googleAnalytics: ''
disqusShortname: 'add the name here'
canonifyURLs
Tells Hugo to use canonical urls.
baseurl: /
languageCode: en-us
title: DocStation Analytics
theme: hugo-lithium
googleAnalytics: ''
canonifyURLs: true
enableRobotsTXT
Tells Hugo to use a custom robots.txt file, which tells bots how to interact with your site. Learn more here. To modify the robots.txt file, add the below code to the config.yaml
.
baseurl: /
languageCode: en-us
title: DocStation Analytics
theme: hugo-lithium
googleAnalytics: ''
enableRobotsTXT: true
Then create a new robots.txt file at ~/layouts/robots.txt
.
Deploy w/ Hosting Service
You can deploy a blogdown site to an endless number of services including Netlify, Github Pages, Cloudflare, AWS S3, etc. I’m not going to document deployment because it’s so well documented elsewhere, but explain the first two options below.
Netlify
I get the impression that Netlify is the most popular place to host a blogdown. It’s to setup CI/CD with Github and Netlify has domain services so you can register your own custom domain in the same place. It’s what I use for my personal site. That said, if you want to set your site up with Netlify I’m going to hand you off to Alison because her up & running with blogdown in 2021 post provides a flawless step-by-step. There’s simply no reason to replicate here.
Github Pages
Github pages seems to be a close second in popularity from what I’ve seen. It’s even simpler than netlify, but you cannot use a custom domain without a premium account. Again, I’m going to outsource the guide on this approach because it’s so simple and already well documented in the blogdown book.
Deploy Internally w/ Docker
As I mentioned, I host my personal site on Netlify - it’s been very cheap and easy to use. However, I want to publish an internal blog at work that needs to be secured. Netlify requires an enterprise account to use oauth, and their new pricing model would be cost prohibitive for a small startup. Further, because we work with health data we use a service called Aptible that makes HIPPA compliance and auditing much simpler. Given that, I need to host my blogdown site in a Docker container. Given the length of this post and the specialized circumstances of wanting to host a blogdown site on Docker, I decided to leave that for a separate post which I’ll link to here when it’s up.