A truly static blogdown website

In a previous post I have briefly described why I switched to Hugo + blogdown package for making a new site and showed the final result. In this post I will explain several crucial steps in details. This blog post is not supposed to be very comprehensive and it is assumed that the readers are comfortable with HTML/CSS, Rmarkdown and either know Hugo internals or at least have saved the browser tab with the Hugo documentation website :)

Several initial steps are summarized very well in an article from Jacqueline Nolis. After reading this post I had a blank theme with a css/js files for Bootstrap 4 framework. The whole fun is about to begin.

In order to make the site at least somewhat functional several more steps are needed:

  • Prepare Hugo layouts
    • For custom pages
    • For blog posts
  • Write Hugo shortcodes and partials
  • Figure out how to write Rmardown documents with plain HTML and Hugo shortcodes
  • Add functionality such as feedback forms
  • Test the website locally
  • Commit and deploy the website

Prepare Hugo layouts

The first Hugo building block is located in the theme subfolder layouts/_default/baseof.html. This layout is included in each and every website page, so it should be universal, but not overwhelmed with CSS files and JS scripts, if they are not used across the whole site. This file is normally available for every Hugo theme and requires very minimal changes.

Next, two more common pieces should be added: {{ partial “header” . }} and {{ partial “footer” . }}. These two Hugo partials are same for the whole site. Footer is just a piece of HTML code, but header is a bit more complex: it builds a Bootstrap navbar (top menu) from a config.yaml file using Hugo syntax. The examples are available on Hugo documentation website.

Also, don’t forget to create a simple {{<rawhtml>}} shortcode that would allow calling custom HTML from Rmd files, this will be needed later.

Custom page layouts

After the header and footer are ready it’s time for making custom page layouts. If you have performed all the previous steps, then you have seen a {{ block “main” . }}{{ end }} piece in the baseof.html file. This piece puts the main content of each layout. There are two ways to create custom pages:

  1. Put the whole HTML code in a single unique template;
  2. Make a very generic template and put the whole content in the Rmd file.

I decided to travel the second route. So, the very simple custompage.html layout was created under layouts/_default/ subfolder. It contained literally three lines of Hugo syntax:

{{ define "main" }}
{{ .Content }}
{{ end }}

After that I was able to create individual .Rmd files for each custom page.

title: "Homepage"
output: html_document
layout: custompage

<script src="/js/typewriter.js"></script>
<div class="container text-center">
<h1>Hello, I’m Eduard</h1>
data-rotate='[ "program in SAS and R", "do biostatistics", "train junior staff", "do statistical consulting" ]'></span> for Clinical Trials

` ` `{r echo=FALSE}
blogdown::shortcode('incpartials', 'servicesoverview.html')
` ` `

<div class="container">

` ` `{r echo=FALSE}
blogdown::shortcode('incpartials', 'expertise.html')
` ` `


<h3>Not decided yet? Here are Frequently Asked Questions</h3>


<a id="contact-form"></a>
### Submit a request and I will reply within 24 hours
If you would like to attach any documents, please send an e-mail to info@stats-consult.com instead.
` ` `{r echo=FALSE}
blogdown::shortcode('incpartials', 'contactform.html')
` ` `



Every static page had a short settings section. After the setting section the main content was placed as raw HTML. In the example above I have deliberately showed several ways how the content can be added:

  1. As raw HTML tags. Please note in the above example that <script> was used only on this page and not in the baseof.html. This was done to prevent unnecessary code from loading on every page of the website.
  2. As a “native” Hugo shortcode calls, such as {{<productcards>}}. This approach is quite limited and works only for direct shortcode calls without extra parameters.
  3. As blogdown::shortcode() function called from R markdown code chunk. This approach allows providing additional parameters to the shortcode.
  4. As a standard Rmarkdown syntax.

With a combination of these techniques one is able to create very complex custom pages. The only exception from this rule is a 404.html layout. This page is not used anywhere and only generated by Hugo during the building stage. I use the resulting html file in the .htaccess and point to a custom error page: ErrorDocument 404 /404.html

Blog posts layouts

Blogdown blog pages have minimum 2 layouts: one for a post overview page and another for a single blog post. For my purposes I have added an extra layout called blog.html.

{{ define "main" }}
  <div class="container">
    <div class="text-center">
      <h1>All posts</h1>
  <div class="container">
    <div class="card-columns">
        {{ range (where .Site.RegularPages "Type" "in" .Site.Params.mainSections) }}
            {{ .Render "summary" }}
        {{ end }}
{{ end }}

This layout is pretty simple and just lists all posts using a summary.html. For each blog post a separate card is created, the layout of the summary.html is the following:

<div class="card">
  <div class="postsummary">
    {{ if isset .Params "images" }}
    <a href="{{ .Permalink }}">
      <img class="card-img-top" src="{{.Permalink}}{{.Params.Images}}" alt="{{ .Title }}">
    {{ end }}
    <div class="card-body">
      <h5 class="card-title"><a href="{{ .Permalink }}">{{ .Title }}</a></h5>
      <p class="card-text">{{ .Summary }}</p>
      <p class="card-text">
        <small class="text-muted">
          <i class="fas fa-calendar-alt"></i> {{ .Date.Format "2006-01-02" }}
            {{ if gt .ReadingTime 1 }}
                {{ .Scratch.Set "readingTime" "mins" }}
            {{ else }}
                {{ .Scratch.Set "readingTime" "min" }}
            {{ end }}
            &#183; {{ .ReadingTime }} {{ .Scratch.Get "readingTime" }}

Here I had to learn Hugo in more details. Each blog post card has several options:

  • URL link (.Permalink variable)
  • cover image (.Params.Images variable)
  • summary text (.Summary variable)
  • date of creation (.Date.Format variable)
  • reading time (.ReadingTime variable)

All these variables are described in Hugo documentation. Some of them are built-in, the others (.Images and .Summary) can be defined in the YAML section of the blog post.

Please note that by default blogdown creates .md files. If you want to work with .Rmd, then one extra option is needed in the .Rprofile: blogdown.ext = ‘.Rmd’

Additional functionality

Hugo websites are static, but it is often useful to have some extra functionality. This can be achieved in several ways:

  1. By using custom JavaScript that is executed in the user’s browser
  2. Via <iframe>. I personally don’t like iframes, but if someone wants to add a Google form on a site this may be the only solution.
  3. Via third-party HTML code. For example, I use a formspree.io feedback form. It looks nice and uses the site’s CSS styling. The downside of this approach is that not all third-party solutions provide HTML tags and not all of them have free tiers.
  4. Using additional server-side technologies (such as PHP code). I don’t demonstrate this approach, because it makes a site no longer “static”.

Testing and building the blogdown website

Now we are almost ready to publish the site, but need a few extra steps.

First one is checking the site via blogdown::check_site() function. Please read the suggested steps carefully and apply the suggested patches.

Once website is checked, a local copy should be created via blogdown::build_site(local = TRUE) function. This command makes Hugo to create a local website. The output is stored in the public subfolder. Please note that a rendered website may not work properly on a local machine because of relative links. During the website development it is more practical to check the site via blogdown::serve_site(), which creates a local Hugo server and properly emulates all the links of the website.

Deploying the website

This step is fairly easy: you can upload the website on your server either manually or utilize some CI/CD tool. While using CI/CD may look like an overkill for this easy task, I can still encourage to learn it exactly because the deployment of Hugo websites is easy. I personally use bitbucket for many years already (long before GitHub allowed creating private repos on a free tier), but since my projects were mainly for data analysis I never went beyond simple git commit.

I consider my latest experience with Hugo as a good opportunity to learn several new things, and one of them is trying CI/CD tool, i.e. bitbucket’s Pipelines. At first Pipelines didn’t want to work. This tool asks for 2FA on the project and it was not clear why it refused to start. I wish bitbucket documentation was a bit clearer on this topic: team workspaces don’t have 2FA on a free tier and hence Pipelines will not work. It is still possible to make a personal private repo which will be accepted by Pipelines settings.

Since I had zero experience with CI/CD before, it was not fully clear which tutorials to use in the beginning. I found a blog post of Chris Frewin extremely useful. At least it provides minimal examples (for some reasons it was not immediately obvious from the official documentation).

Next step is to think about deployment process. Bitbucket has a useful template for FTP upload. My bitbucket-pipelines.yml file now looks like this:

      - step:
          name: Upload public folder via FTP
          - pipe: atlassian/ftp-deploy:0.3.6
              USER: $FTP_USER
              SERVER: $FTP_HOST
              REMOTE_PATH: /
              LOCAL_PATH: public
              DELETE_FLAG: 'true'

Extra tips

  • Don’t forget to include your web server config file. Since Pipelines completely erase website directory, I have to keep .htaccess file in my repo. Of course, you can keep the existing files and not erase them, but I decided to make sure that live website exactly matches the latest build.
  • It may be useful to ignore .Rmd files in the public subfolder. This can be achieved by adding the following two lines into .gitignore file.

I hope this tutorial is helpful and clarifies several important aspects of website development with Hugo, blogdown and R.

Here are some other posts:

Updates in June 2021
Updates in June 2021

Image attribution, parallax effect in blog posts, meta tags for social pages

2021-07-07 · 6 mins

Moving to Hugo
Moving to Hugo

Finally got some free time to learn blogdown package. Here is a story of converting my website from Wordpress to Hugo.

2021-05-29 · 2 mins

Multilanguage Shiny App - Advanced Cases
Multilanguage Shiny App - Advanced Cases

My approach to translating Shiny apps via shiny.i18n package.

2021-03-15 · 5 mins