Migrating my Academic (Hugo Wowchemy) website to Quarto

Authors

Juan-Carlos Castillo

Gabriel Cortés

Published

March 25, 2025

All started with the Academic Hugo template. As many academics, when I discovered about 6 years ago I though it was the best way to build a nice website for displaying my academic work with an easy markdown base. I particularly liked the way in which it showed the publications, an essential part of our work. But … maybe too good to be true. Along the last years it became increasingly intrincated when undertaken by Wowchemy, and lately by Hugoblox. From my point of view, with Wowchemy and Hugoblox the website was becoming more and more difficult to maintain, and I was not able to make it work as I wanted. Besides, they started charging for it. It was time to move to something else, and by that time everyone was talking about Quarto.

For a while I started following academic websites built with Quarto, and I was amazed by the possibilities it offered. I was particularly impressed by the Quarto website itself, which is built with Quarto. I also found a lot of interesting websites built with Quarto, such as Yan Holtz’s and Garrick Aden-Buie’s. So I decided to give it a try.

I started by building a new website from scratch, but I quickly realized that I wanted to keep my old content, the closest possible to Academic Hugo. So I decided to migrate my old website to Quarto. This article is a summary of the steps I took to migrate my website from Hugo Academic to Quarto. This was my original website: jc-castillo.github.io. And this is my new website: jc-quarto.github.io.

This article is not a tutorial on how to build a website with Quarto. There are already plenty of resources on this topic, and I will not repeat them here. You can find a lot of information in the Quarto documentation, which is very well written and easy to follow. I will also not go into detail about the differences between Hugo and Quarto, as there are already many articles on this topic. You can find a good summary of the differences between Hugo and Quarto in this article.

Although you can use this as a guide to building a Quarto site from scratch, most of this article is actually devoted to explaining how to migrate a site already built on Hugo Academic to Quarto. If you are building a site from scratch, the Quarto original documentation is a good place to start. Even though this is a newer tool, there are already a number of Academic sites built on Quarto, including the Quarto official website itself, that you can check out for ideas.

I had the idea, set up the project, generated the basic structure, and the migration was mostly carried on by my research assistant Gabriel Cortés. He did a great job, so if you are interested in something like this, he is the guy.

Project Structure

One of the advantages of building a website in Quarto is that the project structure is much simpler than in Hugo. In fact, it is very similar to any other Quarto project. This is a basic Quarto website project.

.
├── _quarto.yml
├── docs
│   ├── content
│      └── content.html
│   ├── more_content.html
├── index.qmd
├── content
│   ├── posts
│   └── content.qmd
├── resources
│   ├── documents
│   └── images
├── styles.css
├── _extensions
├── README.md
├── project.Rproj

Website basic configuration

First, you will have a _quarto.yml file, which contains the basic configuration of the website. Here you can define the site title, the navbar configuration and some style settings. If you have an external .css file and want to apply it to the whole site, you need to specify it here.

Output directory

By default the website will be rendered into a _site folder. However, we recommend that you create a docs folder as the output directory, as this makes it easier to deploy the website in Github Pages or Netlify.

To change your output directory you need to specify it with output-dir in your _quarto.yml:

project:
  type: website
  output-dir: "docs"

Content

If migrating to a new site meant starting from scratch and losing all your previous content, you might not be too excited about trying something new. Fortunately, migrating content from Hugo to Quarto is fairly straightforward.

The first thing you need to do is change all your old .rmd and .md files to .qmd. We managed to do this manually, but if you have tons of entries you may want to explore some automation. In that case, you should thank Nick Tierney for tweaking this very simple function to convert .rmd to .qmd.

Next, you need to adapt your brand new `.qmd’ YAMLs. This is not as straightforward as the previous step, but we have the advantage that Quarto YAMLs are much simpler than Markdown’s. For example, this is my old Docencia Markdown’s YAML.

# An instance of the Pages widget.
# Documentation: https://wowchemy.com/docs/page-builder/
widget: portfolio

# This file represents a page section.
headless: true

# Order that this section appears on the page.
weight: 45

title: Docencia
subtitle:

content:
  # Page type to display. E.g. post, talk, publication...
  page_type: docencia
  # Choose how many pages you would like to display (0 = all pages)
  count: 5
  # Filter on criteria
  filters:
    author: ""
    category: ""
    tag: ""
    exclude_featured: false
    exclude_future: false
    exclude_past: false
    publication_type: ""
  # Choose how many pages you would like to offset by
  offset: 2
  # Page order: descending (desc) or ascending (asc) date.
  order: desc

design:
  columns: '3'

  # Choose a view for the listings:
  #   1 = List
  #   2 = Compact
  #   3 = Card
  #   4 = Citation (publication only)
  view: 3

Compared to my new Docencia Quarto’s YAML:

title: "Docencia"
page-layout: full
listing:
  contents: posts
  sort: "date desc"
  type: grid
  categories: false
  fields: [image, title, description, date]
  image-align: left
  filter-ui: [title, date]
title-block-banner: true

So you only need to write this very concise chunk of YAML once, and then just copy and paste it to every page you need.

If you have a page that has no other subdirectory, you can just leave this `.qmd’ in your root directory and be done with it! Just like the links page in this site.

However, if you have many entries, you may need to follow a different structure. Here is the one we use:

.
├── docencia
│   ├── docencia.qmd
│   ├── posts
│      └── post1
│         └── index.qmd

Now you can just copy and paste your old content into the posts folder. As you could see in the docencia yaml before, this post folder is indicated in the content option of the listing. Listings are the main tool for creating a blog on Quarto, as they allow you to automatically generate the content of a page (or a section of a page) from a list of Quarto documents.

Then you have to convert the .rmd and .md files into .qmd files and adapt your YAML’s as before. Again, the YAML structure in Quarto is much simpler, as in this example.

title: Talleres y seminarios
categories: [cursos]
# Date published
date: "2018-05-25"  #year-
draft: false # Is this an unpublished draft?
about:
  template: marquee
  image: "featured.png"  

This is a minimal example, of course. You can explore other options and templates in the quarto documentation

Homepage

Your new homepage will be set up by index.qmd.

About Page

In this case we use trestle, but you can also explore other templates. Here you can set up your profile picture and some contact or personal links:

title: "Juan Carlos Castillo"
pagetitle: "JC Castillo Website"
page-layout: full
format:
  html:
    grid:
      body-width: 1440px
page-navigation: false
toc: false
image: "images/avatar.jpeg"
about:
  id: hero-heading
  template: trestles
  image-width: 70%
  image-shape: round
  links:
    - icon: mortarboard-fill
      href: https://scholar.google.es/citations?user=CPJ0qfQAAAAJ&hl=es&oi=ao
    - icon: github
      href: https://github.com/juancarloscastillo
    - icon: envelope
      href: contacto.html
Important

Currently, Quarto only supports Bootstrap Icons. So if you were using any other icon packages in Hugo, you will need to replace them for bootstrap icons. More information

Homepage listings

While before listing were used to build individual pages, the homepage was created as a collection of listings. We achieved this by specifying all the listings we wanted in index.qmd YAML. Then you can place the content of the listing in the document by anchoring it with its id. For example, this listing:

- id: docencia
  contents: 
    - docencia/posts/*/*.qmd

Will be called like this on the document body:

#docencia

By default, Quarto proportionates three listing templates: default, table and grid. Here we have used grid for docencia and proyectos listings, and default for publication, tesis and blog listings.

However, you can also create Custom Listings using EJS templates. We take the code for creating a gallery listing from Quarto developer Mickaël Canouil. The EJS template used looks like this:

<style type="text/css">
.grid-gallery {
  columns: 5 200px;
  column-gap: 0.5rem;
  width: 90%;
  margin: 0 auto;
}
.grid-item {
  width: 150px;
  margin: 0 0.5rem 0.5rem 0;
  display: inline-block;
  width: 100%;
  border-radius: 5px;
}
img.grid-item {
  width: 100%;
  transition: all .25s ease-in-out;
}
img.grid-item:hover {
  transform: scale(1.025);
}
</style>


::: {.grid-gallery}
<% for (const item of items) { %>
::: {}
![](<%= item.path %>){.grid-item .lightbox loading="lazy" group="quarto-grid-gallery"}
:::
<% } %>
:::

As you can see, nothing too fancy, and with basic CSS knowledge you might be able to figure it out. The results are great though.

Unfortunately, I think this feature is very underused at the moment. I would bet that in a few years there will be plenty of great EJS templates for everyone to choose from. For now, though, given its enormous potential to customise your site, it might be worth exploring for yourself.

Body

Here is how a section of this site’s homepage is built. As you can see, the listing here is called inside a section block.

::: {.parallax-container}
::: {.parallax-image-container}
::: {.section-block}
::: {style="text-align: center;"}

::: {#proyectosh1}
:::

<p align="left" style="font-size:36px; font-weight:bold;">Proyectos</p>

:::{#proyectos}
:::


:::
:::
:::
:::

Also, a couple of very interesting tricks can be taken from this example:

Parallax

Thanks to Yan Holtz for creating this feature to add a very nice parallax effect to a Quarto website. To make it work, you need to write some code in your .css file, as he explains on this website, where you can find lots of other very cool tricks to do on your own.

.parallax-container {
  position: relative;
  padding-left: 0em;
  padding-right: 0;
  padding-top: 1em;
  margin-top: 0em;
  max-width: 100%;
  width: 100vw;
}

.parallax-image-container {
  background-image: url(images/background-about.png);
  background-attachment: fixed;
  opacity: 1;
  background-position: center;
  background-repeat: no-repeat;
  background-size: cover;
  margin: 0 auto;
  padding-top: 1.5em;
  padding-bottom: 1.8em;
  box-sizing: border-box;
  max-width: 100%;
  width: 100vw;
}

Section Scrolling Navigation

I wanted to keep this feature from my old site, where when you clicked on the navbar, it did not take you to another page, but scrolled to a section within the home page.

We achieved this by anchoring the section with a # (as in {#proyectosh1}). This creates a “tag” that allows you to identify a section within a page. You can then replace the reference in the navbar options in your _quarto.yml like this

website:
  title: "jc-castillo"
  navbar:
    left:
      - href: index.qmd#proyectosh1
        text: Proyectos

You could also use the listing ID as an anchor. However, this might feel a bit odd, as scrolling would take you directly to the listing content, rather than the section title – creating a slight overshoot effect.

By default, you will jump from one section to another. If you prefer a more smooth effect, like the one I used in my page, you can specify this in your .css file.

html {
  scroll-behavior: smooth;
}

How to manage your publications

For my old site I wrote this function to automatically extract publications stored in Zotero and publish them as a publication record in hugo-academic using blogdown/R. The challenge now was to migrate all my publications and presentations to my new site while losing as little information as possible.

The basic workflow is pretty much the same as before, and you can see it here. Here I will explain the main changes we need to make to adapt the function to Quarto.

Function

You can access to the full code here

First of all, of course, you need to change the file extension, so that you now have `.qmd’ files. So you need to pass from this:

    filename <- paste(x[["date"]], x[["title"]] %>%
                        str_replace_all(fixed(" "), "_") %>%
                        str_remove_all(fixed(":")) %>%
                        str_sub(1, 20) %>%
                        paste0(".md"), sep = "_")

To this:

    filename <- paste(x[["date"]], x[["title"]] %>%
                        str_replace_all(fixed(" "), "_") %>%
                        str_remove_all(fixed(":")) %>%
                                            str_remove_all(fixed("?")) %>% 
                        str_sub(1, 20) %>%
                        paste0(".qmd"), sep = "_")

I also added str_remove_all(fixed("?")) since Quarto seemed not to render files that ended ?.qmd

In fact, if you run the function after changing this, you will already have a .qmd for all your publications. Then all you need to do is list these files in your publication.qmd like this:

title: "Publicaciones y Presentaciones"
page-layout: full
listing:
  contents: posts/*.qmd
  sort: "date desc"
  type: default
  categories: true
  fields: [title, date, author, categories]
  filter-ui: [categories, date]
  page-size: 1000
title-block-banner: true

It may not be as stylised as Hugo (for now), but it is quite functional, listing all your publications and creating an individual entry for each one.

Of course, you may want to customise it further, as YAML may not be ready yet. In particular, you need to change the way links and icons are handled so that you can add documents, slides or repositories of your work. This is how we did it:

    url_fields <- c("url_pdf", "url_preprint", "url_dataset", 
                                    "url_project", "url_slides", "url_video", "url_poster")

     # ----------------- LIMPIEZA Y EXTRACCIÓN DE LINKS -----------------
      
      # Crear lista vacía para los links extraídos de annotation
      annotation_links <- list()
      
      
      # 1. Eliminar el bloque de "Prof. Guía"
      x[["annotation"]] <- gsub(
        "- icon: graduation-cap.*?(?=- icon|$)",  # patrón multilinea hasta el siguiente icon o fin
        "",
        x[["annotation"]],
        perl = TRUE
      )
      
      # 2. Extraer íconos y enlaces del annotation (todos los "- icon: ... href: ..." o "- icon: ... web: ...")
      matches <- str_match_all(
        x[["annotation"]],
        "- icon: ([^\\n]+)\\s*\\n\\s*(icon_pack: [^\\n]+\\s*\\n)?\\s*(name: [^\\n]+\\s*\\n)?\\s*(web:|href:) ([^\\n]+)"
      )[[1]]
      
      # Crear lista con los links extraídos y transformados
      if (nrow(matches) > 0) {
        annotation_links <- apply(matches, 1, function(row) {
            icon_name <- row[2]
            url <- row[6]
            
            if (grepl("github\\.com", url)) {
                icon_name <- "github"
            } else if (!is.na(icon_name) && icon_name == "file") {
                icon_name <- "file-pdf-fill"  # cambiar file a file-pdf-fill
            }
            
            
            list(icon = icon_name, href = url)
        })
      }
      
      # 3. Eliminar bloques de iconos + enlaces de annotation (incluyendo los que comienzan con #)
      x[["annotation"]] <- gsub(
        "(\\n|^)#?\\s*-\\s*icon:\\s*[^\\n]+\\s*\\n\\s*(icon_pack:\\s*[^\\n]+\\s*\\n)?\\s*(name:\\s*[^\\n]+\\s*\\n)?\\s*(web:|href:)\\s*[^\\n]+\\n?",
        "",
        x[["annotation"]],
        perl = TRUE
      )
      
      # 4. Eliminar cualquier bloque "links:" vacío o mal formado que venga de Zotero
      x[["annotation"]] <- gsub(
        "\\n?links:\\s*(\\n\\s*-.*)?",
        "",
        x[["annotation"]],
        perl = TRUE
      )
      
      # 5. Eliminar cualquier campo de URL residual (como "url_pdf : \"\"")
      x[["annotation"]] <- gsub(
        "url_[^:]+:\\s*\"[^\"]*\"\\s*",
        "",
        x[["annotation"]]
      )
      
      # 6. Limpiar líneas en blanco adicionales
      x[["annotation"]] <- gsub("\n{2,}", "\n\n", x[["annotation"]])  # máximo 1 salto doble
      x[["annotation"]] <- trimws(x[["annotation"]])  # eliminar espacios al inicio y final
      
      # ----------------- LINKS DESDE CAMPOS ESPECIALES -----------------
      
      # Mapa de íconos para campos especiales (url_pdf, url_project, etc.)
      icon_map <- list(
        "url_slides"   = list(icon = "file-slides-fill"),
        "url_video"    = list(icon = "camara-video-fill"),
        "url_poster"   = list(icon = "image-fill"),
        "url_pdf"      = list(icon = "file-pdf-fill"),
        "url_preprint" = list(icon = "files-alt"),
        "url_dataset"  = list(icon = "database"),
        "url_project"  = list(icon = "archive")
      )
      
      # Recolectar los links especiales
      links <- list()
      for (field in names(icon_map)) {
        if (!is.null(x[[field]]) && !is.na(x[[field]]) && x[[field]] != "") {
            links <- append(links, list(list(
                icon = icon_map[[field]]$icon,
                href = x[[field]]
            )))
        }
      }
      
      # ----------------- COMBINAR Y ESCRIBIR LINKS EN 'ABOUT' -----------------
      
      # Juntar annotation_links + links especiales
      all_links <- c(annotation_links, links)
      
      # Escribir el bloque 'about' y 'links'
      write("about:", fileConn, append = T)
      write("  template: marquee", fileConn, append = T)
      
      if (length(all_links) > 0) {
        write("  links:", fileConn, append = T)
        for (link in all_links) {
            write(paste0("    - icon: ", link[["icon"]]), fileConn, append = T)
            write(paste0("      href: ", link[["href"]]), fileConn, append = T)
        }
      }

With this code, you should obtain a YAML like this:

title : "Social Cohesion and Attitudinal Changes toward Migration"
date : "2024-01-01"
author : ["Juan-Carlos Castillo"]
publication : " GlobaLab Bremen Conversations on Global Solidarity . Bremen  (Online)"
about:
  template: marquee
  links:
    - icon: github
      href: https://github.com/ocscoes/presentaciones/tree/main/global-solidarities-Jan2024
    - icon: file-slides-fill
      href: https://ocscoes.github.io/presentaciones/global-solidarities-Jan2024/bremen_global_sol2024.html
Important

This code only makes sense if you have previously worked on your Zotero annotations. If you are building a Quarto site from scratch, it might be better to just copy YAML’s about section and paste it into your Zotero annotations, and work on the function from there.

You can customise the body to suit your needs. Here, for example, we write some code to include a how-to-cite box at the end of the document.

# ----------------- GENERACIÓN DE CALL OUT "HOW TO CITE" -----------------
        
        # Preparar autores
        authors <- x[["author"]]
        authors <- str_replace_all(authors, " and ", ", ")  # Cambiar 'and' por ', ' para lista de autores
        authors <- stringi::stri_trans_general(authors, "latin-ascii")  # Eliminar tildes
        authors <- gsub("\\{", "", authors)  # Limpiar llaves
        authors <- gsub("\\}", "", authors)
        
        # Año
        year <- ifelse(!is.na(x[["year"]]), x[["year"]], "s.f.")  # "s.f." si falta el año
        
        # Título
        title <- x[["title"]]
        
        source <- publication  # Usar directamente la variable que ya creaste
        
        # Construir cita formateada (HTML-safe)
        citation_text <- paste0(authors, " (", year, "). *", title, "*")
        if (source != "") {
            citation_text <- paste0(citation_text, ". ", source)
        }
        
        # ----------------- ESCRIBIR COMO CALLOUT -----------------
        
        # Escribir el callout al final del archivo
        write("\n\n::: {.callout-note title=\"How to cite this work\"}\n", fileConn, append = T)
        write(citation_text, fileConn, append = T)
        write("\n:::\n", fileConn, append = T)

Resources

Normally, Quarto will be able to render any local file if you give it the correct path within your project. However, some files - particularly presentations - may require other auxiliary files - notably custom CSS files - to render them correctly. By default, Quarto will not include any of these auxiliary files in your output directory. You will probably notice this because the file will load, as the path is correct, but it will remain blank.

The solution for this is the resource option in your _quarto.yml. Like this:

  type: website
  resources:
          - documents/*/*

You can of course write any path (o paths) you need to include.

Extensions

You can use extensions to further customise your site. Once installed, they are hosted in an _extensions folder in your root directory.

Here we used Garrick Aden-Buie’s Now extension to set the last updated field in the page footer.

Another Stuff

Migrating your old content to a new website may be the right time to check that all your links are working properly. There are tools that allow you to scan your entire website and detect dead links, but many of them are either paid or freemium. There are some [free alternatives] (https://www.deadlinkchecker.com/), but you need to be careful as the information may not be entirely accurate, although it might be a good place to start.