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
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;
}.grid-item {
imgwidth: 100%;
transition: all .25s ease-in-out;
}.grid-item:hover {
imgtransform: scale(1.025);
}</style>
::: {.grid-gallery}<% for (const item of items) { %>
::: {}<%= item.path %>){.grid-item .lightbox loading="lazy" group="quarto-grid-gallery"}
;
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.
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:
<- paste(x[["date"]], x[["title"]] %>%
filename str_replace_all(fixed(" "), "_") %>%
str_remove_all(fixed(":")) %>%
str_sub(1, 20) %>%
paste0(".md"), sep = "_")
To this:
<- paste(x[["date"]], x[["title"]] %>%
filename 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
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.