Switching my website from Hugo to Quarto
Hugo is an open-source static site generator built with Go, which uses markdown files and templates to generate static HTML pages. The R package {blogdown}, makes it easier for R users to create websites using Hugo with pages that contain R code. My personal website was previously built using {blogdown} and the Hugo Apéro theme.
Quarto is an open-source scientific and technical publishing system that lets you create dynamic documents, presentations, websites, and books. It supports multiple languages like R, Python, and Julia, and integrates markdown with code to produce reproducible, high-quality output.
I recently converted my personal website from Hugo to Quarto, and this blog post aims to explain the motivation behind the switch, discuss the things that were tricky to convert, and some tips if you’re thinking about doing the same.
The motivation for switching
Why switch from Hugo?
So if I already have a functioning website built with Hugo, why go through the pain of converting it to Quarto? Well, there are a few reasons!
- Hugo requires files called
index.md
which can be generated from anindex.Rmd
file. But this means you essentially need to keep two copies of the same file (which can end up diverging or having changes overwritten). Quarto just needs oneindex.qmd
filem which is much cleaner. - There were a lot of features that weren’t natively available in the Hugo theme I was using such as adding LaTeX equations, combining HTML and Markdown formatting, client-side search, Lightbox styling for iages, and a sticky table of contents for pages. In Hugo, I ended up building all of these features in myself, by writing new templates, shortcodes, and some JavaScript. Although that was a useful experience in itself, Quarto has all of those features (and others) already built-in. This should mean less website maintenance time, and more time for just writing blog posts.
- Using {blogdown} meant my website, although built with Hugo, was essentially tied to an R package and R itself. Although I’m definitely not abandoning R, I do write more blog posts about Python and D3 than I used to, so it’s easier to do that in Quarto which has native support for them.
Why switch to Quarto?
The issues I was having with Hugo can be solved with Quarto quite easily, but there were a few other reasons why Quarto was also a good choice:
- I already use Quarto for other documents, including slide decks, training course websites, and single page websites. Using the same software for my personal website as well, makes it easier to keep things looking consistent - especially with Quarto’s multi-format branding abilities.
- Quarto has a nice, user-friendly implementation of responsive grid layouts which makes it really easy to build pages that look good on both desktop and mobile.
What needed changing?
In terms of the technical aspect of actually switching from one to the other, besides copying folders into the right place, there are a few specific things that need changing.
YAML options
The YAML options for Hugo index.md
files and Quarto index.qmd
files are slightly different, and need to be changed. For example, the old Hugo YAML options might have looked like this:
---
title: "How to choose an IDE"
author: "Nicola Rennie"
date: "2025-07-11"
categories:
- R
- Python
slug: "how-choose-ide"
excerpt: "Thinking about switching to a different IDE? Instead of telling you which IDE to pick, this blog helps you ask the right questions to decide for yourself."
subtitle: "Thinking about switching to a different IDE? Instead of telling you which IDE to pick, this blog helps you ask the right questions to decide for yourself."
image: "featured.png"
---
and the updated Quarto YAML looks like this:
---
title: "How to choose an IDE"
date: "2025-07-11"
description: "Thinking about switching to a different IDE? Instead of telling you which IDE to pick, this blog helps you ask the right questions to decide for yourself."
image: "featured.png"
categories:
- R
- Python
---
You can see that many of the options are similar. The main differences are the removal of the slug
option (see Website structure below), and change to the descripiton. In Hugo, there are two separate YAML options for the page subtitle summary, and the summary that appears in the listing page. In Quarto, these are the same thing - which saves copying and pasting it to two options! I’ve also removed the author
option here, and set it globally for all blog posts - which, to be fair, I could also have done in Hugo!
Shortcodes
All of the old Hugo shortcodes needed updating to the Quarto equivalents, because the Hugo shortcodes won’t be recognised by Quarto and will cause an error during rendering. For example, in my old Hugo site, I had a custom shortcode that applied Lightbox image styling:
{{< img-lightbox "images/featured.png" "alt text" "100%" >}}
For this example, in Quarto it’s as simple as adding lightbox: true
to the YAML or code block options.
Deployment
Both the old Hugo website and new Quarto website are deployed to GitHub Pages using GitHub Actions and this is an approach I’d recommend because it’s one less thing for you to do, and it makes it easier to make quick updates when you’re away from your laptop. Essentially, it gives you the ability to easily fix a typo from your phone.
There were two aspects to changing the deployment:
- Update the GitHub Actions script to the Quarto GitHub Pages workflow
- In GitHub, change the pages setting to deploy from the
gh-pages
branch
Not particularly tricky, but you do need to remember to manually publish to GitHub Pages using quarto publish gh-pages
once before the Actions workflow will work.
What was difficult?
Although there are many good things about switching to Quarto and the process was overall relatively easy, there were still a few things that were tricky when converting from Hugo to Quarto.
Website structure
One of the things I found the most annoying was the fact that the way that the GitHub repository structure relates website structure can different between Hugo and Quarto. In Hugo, a YAML parameter call slug
exists, where you can specify what the URL of a specific page is. If you don’t specify the slug
, then it defaults to the directory name. In my old Hugo website, all of my directories were prefixed with the date of the blog post to make it nicely ordered, but the published URL didn’t have the date prefix. For example, the folder might be named blog/2025-07-11/how-choose-ide
, and the slug could be specified as:
slug: "how-choose-ide"
to deploy the post at nrennie.rbind.io/blog/how-choose-ide. In Quarto, slug
doesn’t exist as a YAML parameter. To keep this structure, and make sure that links to existing blog posts don’t break, I needed to find a way to change the URL. Danielle Navarro also follows a similar structure for her blog posts, and described how she tackled this problem using Netlify redirects in a blog post about Porting a distill blog to quarto.
I decided to use a slightly different approach, that works regardless of where you deploy your site and also works when rendering locally. All of the folders are named in a specific way using YYYY-MM-DD-blog-post-url
, and so Quarto will render this to a folder called YYYY-MM-DD-blog-post-url
. All I really need to do is rename this folder after it’s been rendered to remove the date prefix. We’re going to write a script that does that automatically when rendering!
The following is a bash script (called postprocess.sh
and saved in the root directory of the project) which renames the subfolders within the blog
and talks
directories. It also goes through each file, and the listing page, to fix any links that exist within it.
#!/bin/bash
set -e
echo "Starting post-processing for blog/ and talks/..."
SECTIONS=( "blog" "talks" )
# === Part 1: Fix dated URLs in listing and site files ===
for section in "${SECTIONS[@]}"; do
FILES=( "$section/index.html" "$section/index.xml" "$section/index-r.xml" "listings.json" "search.json" "sitemap.xml" )
for f in "${FILES[@]}"; do
full_path="docs/$f"
if [ -f "$full_path" ]; then
echo "Fixing dated URLs in $full_path..."
# Replace dated folder prefixes anywhere (including image URLs)
sed -i -E "s#(/$section/)[0-9]{4}-[0-9]{2}-[0-9]{2}-(.+?)/#\1\2/#g" "$full_path"
# Also fix relative URLs with ../ prefix anywhere
sed -i -E "s#(\.\./$section/)[0-9]{4}-[0-9]{2}-[0-9]{2}-(.+?)/#\1\2/#g" "$full_path"
fi
done
done
# === Part 2: Fix all dated URLs inside listing HTML files ===
for section in "${SECTIONS[@]}"; do
FILE="docs/$section/index.html"
if [ -f "$FILE" ]; then
echo "Fixing all dated URLs in $FILE ..."
sed -i -E "s#(\.\./$section/)[0-9]{4}-[0-9]{2}-[0-9]{2}-(.+?)(/index\.html|/featured\.png|/)#\1\2\3#g" "$FILE"
fi
done
# === Part 3: Fix dated URLs inside each post/talk's own index.html ===
for section in "${SECTIONS[@]}"; do
for d in docs/$section/20*-*-*/; do
[ -d "$d" ] || continue
dated_slug=$(basename "$d")
slug_only=$(echo "$dated_slug" | sed -E 's/^[0-9]{4}-[0-9]{2}-[0-9]{2}-//')
f="${d}index.html"
if [ -f "$f" ]; then
echo "Fixing paths in $f..."
sed -i -E "s#([\"'/])$section/${dated_slug}#\1$section/${slug_only}#g" "$f"
sed -i -E "s#https?://[^\"' >]*/$section/${dated_slug}#/$section/${slug_only}#g" "$f"
fi
done
done
# === Part 4: Rename dated folders to slug-only folders ===
for section in "${SECTIONS[@]}"; do
for d in docs/$section/20*-*-*/; do
[ -d "$d" ] || continue
dated_slug=$(basename "$d")
slug_only=$(echo "$dated_slug" | sed -E 's/^[0-9]{4}-[0-9]{2}-[0-9]{2}-//')
target="docs/$section/${slug_only}"
if [ ! -d "$target" ]; then
echo "Renaming $d -> $target"
mv "$d" "$target"
fi
done
done
echo "Post-processing complete."
To get this script to run automatically after the website is rendered, you can edit the post-render
option in the _quarto.yml
file:
project:
type: website
output-dir: docs
post-render: bash ./postprocess.sh
Volume of pages
The reason that the conversion from Hugo to Quarto took me so long was the sheer volume of pages that needed converting - there were 57 blog posts and 64 talk pages to update alongside the main pages. Although I could have probably written a script to convert the YAML from Hugo to Quarto in an automated way, I did it manually for a few reasons:
- the YAML options in some of my older blog posts definitely weren’t written in a consistent way
- files often had some custom Hugo shortcodes, and I want to make sure that no were accidentally left in
- I wanted to inspect some of the older posts to make sure they all had alt text and would still work
- I needed to check each page correctly rendered any way
To render a single page (without formatting) of a website, rename it from index.html
to _index.html
. The prefix of the underscore tells Quarto it’s not part of the main site. This is useful when you’re drafting one page and want to check your computations all work before you render it within the rest of the site.
Home page layout
Although there are some built in options for home and about pages with Quarto, the options are quite limited and none of them were quite what I was looking for. They’re also not easy to change. For my home page, I ended up just having it as a normal Quarto page and building the structure with the grid
layout.
How do you make it easier?
If you’re thinking about switching to Quarto for your personal website, whether from Hugo or something else, here are a few things I’d recommend doing:
Start with a very minimal version of your website, and get it up and running before you move all of your blog posts over. To do this, I re-built the website in a separate GitHub repository and then copied it over to my main website repository which, although not exactly Git best practice, made it easier to have the two sites deployed in different ways at the same time to compare the two.
Use
freeze: auto
when you’re building and tweaking parts of the website. This means a page will only be re-rendered if you edit it. When you’re totally happy with a blog post, change it tofreeze: true
to make sure it never gets re-rendered. This makes rendering your site faster and ensures that older blog posts won’t break when packages get updated.Take your time! You don’t need to convert it all in one day - you can slowly convert a few pages at a time when you have the space to work on it.
Useful resources
A few resources I found especially helpful:
- The Quarto documentation about websites
- Danielle Navarro’s blog post about porting a distill blog to quarto.
- Ella Kaye’s blog post about inspiring and useful resources when building a new Quarto site
- Silvia Canelón’s blog post about porting a Website from Hugo Apéro to Quarto
If you do find a broken link on this site following the conversion from Hugo to Quarto, please let me know what page you were trying to access and if there was a link to it by raising a GitHub issue at github.com/nrennie/nrennie.github.io-comments.
Reuse
Citation
@online{rennie2025,
author = {Rennie, Nicola},
title = {Switching My Website from {Hugo} to {Quarto}},
date = {2025-07-29},
url = {https://nrennie.rbind.io/blog/hugo-quarto-website/},
langid = {en}
}