Setting up Netlify Redirects With Quarto

Configuring Netlify redirects when building Quarto websites using continuous deployment.

Netlify
Quarto
Web Development
R
Author

Paul Johnson

Published

March 1, 2024

If you are using Netlify to host your Quarto website there is a ton of helpful functionality that can smooth out the process. They even give you a fancy deployment status badge, and who among us can really claim they’re better than a nice status badge?

One of the features that Netlify offers is a simple process for handling redirects. Whether you need to redirect traffic from old URLs from your previous site URL structure (it’s me) or you want to shorten URLs to make it easier to share links to your blog posts, Netlify’s _redirects handles all of this very easily.

It’s pretty easy to add a _redirects file to your site if you manually publish from your local machine or link your Netlify site to a Git repository. However, if you have continuous deployment setup with GitHub and/or Netlify or if you want to automate the process of grabbing URLs and generating their redirects (or both), then it’s not immediately obvious how to do this in a painfully manual way with a Quarto website.

There are a few resources out there that will help you figure this out, but my needs were a little different. I had to adapt these guides to fit what I was trying to do, so I thought I’d put a quick explainer together to help others in the same situation.

Standing on the Shoulders of Giants

Much of the process detailed in this blog post has been influenced by or borrowed from other online guides. Rather than try and pitch this as the product of my unique, beautiful mind1, I want to give credit where it is due.

These were the resources I found most useful:

These are much more detailed blog posts about creating a website with Quarto, and there’s a lot more good information in these than just setting up redirects. This blog post will have a shorter, more specific focus, but my solution leans heavily on several of these guides, so I want to dish out the appropriate credit right away. I will also try to highlight throughout the post where my code is adapted from one of these resources.

Setting Up Manual Redirects

First, I created a _manualredirects file that I’ve added to the assets directory (following a similar process to Silvia Canelón). It contains all the blog posts I wrote a couple years ago because the old blog used a slightly different URL structure. I’ve also added a couple other pages that no longer exist, and they redirect to either the homepage or other pages that are successors.

I’m sure it would be possible to automate most of this, but I haven’t figured out an easy fix2. Wherever you’ve got multiple redirects that don’t follow the same pattern and aren’t going to change in the future (so wherever you’re fixing redirects from an old site or old site structure), it’s probably easier to specify these manually.

Each line in a redirects file refers to a different redirect rule, with the original path listed first, followed by the path being redirected to. Your redirects might look something like this:

/portfolio/                               /projects/
/2023/08/good-but-long-post/              /blog/2023-08-good-but-long-post/
/2023/10/okay-post-that-no-one-needed/    /blog/2023-10-okay-post-that-no-one-needed/
/2024/01/boring-post-you-regret/          /blog/2024-01-boring-post-you-regret/

There are quite a few ways you can customise the behaviour of a redirect rule, and for anything more complex I would recommend reading through the Netlify documentation.

Automating Redirects

To automate the process of creating a redirects file you need to add some code somewhere that it can be run regularly. I’ve added these code chunks to the index.qmd file in the project root folder, and I’ve added echo: false to the YAML frontmatter so that the code doesn’t appear on my homepage. This is more or less the same process that Danielle Navarro and Silvia Canelón follow, but the difference is that I am not setting freeze: false in the YAML frontmatter (for reasons explained in Section 4).

There are multiple sources from which redirect rules will be derived, which requires several steps to bring them together in the _redirects file, but the first step is to import the _manualredirects file.

# import redirects 
manual_redirects <- readr::read_lines(here::here("assets", "_manualredirects"))

Redirecting Blog Posts

Besides making sure no one gets lost in the ether using your old URLs, one of the benefits of redirects is creating shorter URLs that are easier to share. In my case, I’m really just removing the date slug from my blog posts, but this still looks a little neater and is easier to remember.

The code chunk below is also adapted from Silvia Canelón’s process, making a few changes to meet my needs, and it defines a function that gets the file paths to every blog post and then defines redirect rules for each.

# get file paths for all blog posts
get_file_paths <-
  function(folder) {
    blog_posts <-
      list.dirs(
        path = c(here::here(folder)),
        full.names = FALSE,
        recursive = FALSE
      ) |>
      tibble::as_tibble_col(column_name = "path") |>
      dplyr::mutate(folder = folder)
  }

# create & extract shortened redirects
blog_posts <-
  # get blog post file paths
  purrr::map("blog", get_file_paths) |>
  # row-bind file paths
  purrr::list_rbind() |>
  dplyr::mutate(
    # remove date slug
    blog_title = stringr::str_remove(path, "(?!\\-)\\d{4}-\\d{2}-\\d{2}-"),
    # create short path
    short_path = paste0(folder, "/", blog_title),
    # combine old path and new path to create redirect rule
    redirects = paste0("/", short_path, " ", "/", folder, "/", path)
  )

blog_redirects <-
  blog_posts |> 
  dplyr::pull(redirects)

Redirecting Categories

Another useful trick that is possible using a similar process is redirecting old tags or categories for blog posts. This probably isn’t necessary for my site because I doubt I have written enough posts or drawn enough traffic to my blog posts that the old tags are being used that often, but it is nice to be thorough. This might be more useful to those of you who are a much bigger deal than me!

The code chunk below is adapted from Danielle Navarro’s process, again with some minor adaptations specific to my use case, defining a function to get all categories listed in blog posts3, and defines redirect rules by specifying the old (tags) and new paths (categories).

# get categories from blog posts
get_categories <-
  function(post) {
    file <- here::here("blog", post) |> fs::path("index.qmd")
    if (!fs::file_exists(file)) {
      return(character(0))
    }
    file |>
      readr::read_lines() |>
      stringr::str_subset("^categories:") |>
      stringr::str_extract("\\[.*\\]") |>
      stringr::str_remove_all("[\\[\\]]") |>
      stringr::str_split(", ") |> 
      purrr::list_c()
  }

# extract categories
categories <-
  # blog post categories
  purrr::map(blog_posts |> dplyr::pull(path), get_categories) |>
  # concatenate categories into a vector
  purrr::list_c() |> 
  unique() |>
  stringr::str_replace_all(" ", "%20")

# extract tags
tags <-
  categories |>
  stringr::str_to_lower() |>
  stringr::str_replace_all("%20", "-")

# write category redirects
category_redirects <-
  paste0("/tags/", tags, " ", "/blog/#category=", categories)

The previous code chunks have compiled all the redirect rules for the site, and most of it uses an automated process. This saves time and limits my opportunities to be an idiot, which I always appreciate.

Bringing it All Together

Finally, we can combine everything into a single vector and save this as _redirects in the project root, for reasons that should become clear in just a moment.

# write the _redirects file
readr::write_lines(
  x = c(manual_redirects, blog_redirects, category_redirects),
  file = here::here("_redirects")
)

We now have a _redirects file that contains all the redirect rules we want to use on our site, from manual redirects to automated blog posts and category redirects.

Including Redirects in Netlify Builds

The solutions I have used as a reference point all run this code every time the site renders, saving the _redirects file in the _site directory. This works in those cases because they don’t appear to be using continuous deployment. They are pushing the _site directory to GitHub and using this to deploy their website via Netlify. There is nothing wrong with doing it this way, and if you want to save yourself some trouble I’d recommend taking this easier (and probably more sensible) route instead. However, I really like Netlify’s build tools, and there are some Netlify plugins that I want to use.

Given that I want to use continuous deployment, I don’t want to check my _site directory into version control, so I have stored the _redirects file in the root. Instead, I need to move the file to the build directory once everything has been built, which leads me to the other sticking point. I can’t set freeze: false because this will cause the R code to run every time the site is rendered, including when it is being built on Netlify. This would require Netlify to have R installed, and, as far as I am aware, that is not possible. I needed a solution that would freeze all computations and make it possible for Netlify to build the site.

My solution was to save the redirects file to the root and include it as a resource for the build process, by adding resources: _redirects to _quarto.yml, nested under projects. This ensures that when Netlify builds the site, it moves the redirects to your build directory, where Netlify can see it! I have also kept freeze: auto for the file containing the redirects code, to avoid Netlify trying to run it and deployment failing. That means that when Quarto renders the site it stores the results of running these code chunks in the _freeze folder, which needs to be checked into version control so that Netlify can use these outputs.

This is an imperfect solution because freezing this code means that it won’t run unless the source code of the file this code is in changes, which probably won’t happen that often. My solution is to run quarto render index.qmd whenever the redirects need updating. This isn’t ideal, and I’m sure this will result in me forgetting to update the redirects, but I’ve yet to think of a better alternative4.

Summary

Netlify offers some valuable features. Netlify’s build tools and redirect capabilities are great, but they don’t necessarily play nice with Quarto, depending on how you want to combine them.

There are guides available online for getting redirects working with Quarto, but things get a little trickier if you want to use continuous deployment with Netlify’s build tools. This post sought to build on the good work others have done and explain how I got redirects working for my particular use case. I suspect there are better ways to approach this. My solution is very much cobbled together. However, it addresses a slightly different problem than the resources I’ve found about setting up Netlify’s redirects with Quarto.

If you have come up with a better way of doing this than me, please let me know!

Support

If you enjoyed this blog post and would like to support my work, you can buy me a coffee or a beer or give me a tip as a thank you.

Footnotes

  1. Please understand that I definitely have a unique, beautiful mind though.↩︎

  2. I think this should be relatively simple for older blog posts, but for the ad-hoc pages that are being redirected, I think a manual approach is probably the only option.↩︎

  3. I really like this function. It would have taken me a long time to come up with something much worse than this, so thanks to Danielle for her work!↩︎

  4. If you have any suggestions, I’d love to hear them.↩︎

Reuse

Citation

For attribution, please cite this work as:
Johnson, Paul. 2024. “Setting up Netlify Redirects With Quarto.” March 1, 2024. https://paulrjohnson.net/blog/2024-03-01-setting-up-netlify-redirects-with-quarto.