Jack Atkinson

Science, Archery, Computing

Jack Atkinson's Website

Jack Atkinson

Science, Archery, Computing

Efficient deployment of Quarto presentations on a website using git hooks

6 minutes
December 24, 2024
computing,  software,  website,  git,  server,  howto,  quarto, 

Recently I have been writing my slides using Quarto . Here I describe the workflow I have converged on to render and deploy them on this website in an efficient and streamlined fashion.


In a previous post I wrote about how I manage my website as a git repository and use a post-recieve git hook to render and deploy it whenever changes are pushed.

Recently (if 2 years is ‘recent’) I have been using Quarto to write my presentations. More specifically, using Quarto to render my markdown presentations into revealjs html files. You can see examples on my slides page.

This was for a few reasons:

But this is not an article primarily about Quarto, rather about how to deploy it efficiently online, so we digress.


Now, Quarto revealjs allows you to embed everything into a single html file using the embed-resources: true option. That is, the images, the animations, the style etc. The nice side of this is that it gives you a single html item to upload and point at (as opposed to having style files and artifacts generated during render in separate files that also need to be tracked). However this comes at the cost of generating HUGE final html files, somewhat defeating the above objectives. It eventually occurred to me that “surely I could just check in the basic files and render them server-side!”

My first attempts at this went fine, including a segment in the hook that navigates to the slides and runs quarto render on each slidedeck before deploying the site. However, as I added more than a couple of slidedecks the time taken to render every one with each rebuild really began to stack up. There are also additional complications once you start exploring the deeper features of Quarto.

To resolve this I decided I would only re-render ‘updated’ slides with each push. This required me to change my build process. Instead of building from a fresh git clone every time I now have an intermediate clone that persists and to which I pull the latest changes. Slides that have been previously rendered are left unchanged, with only those that I specify being re-rendered. After this has been done the site is re-built using hugo and deployed to the public-facing location as before.

Additional tips and tricks

Quarto suggests adding extensions to version control . If we were to do this separately for every slidedeck this would start to add up and be redundant, especially as I often use the same extensions in every presentation. Fortunately we can address this by making the slides/ directory into a Quarto project with an extremely minimal _quarto.yml file. Extensions can now be shared between all sub-folders of the project. Using a project also allows rendering of the entire slidedeck in a directory by running quarto render slideshow/ on the directory.

Quarto will, by default, render slideshow/slideshow.qmd to slideshow/slideshow.html. This will then be viewable at jackatkinson.net/slides/slideshow/slideshow.html. To make these viewable instead at jackatkinson.net/slides/slideshow we can render instead to index.html by adding output-file: index to the .qmd file header. This means we can point a browser at slideshow/ to view the files instead of slideshow/slideshow.html.

Avoiding file duplication can also be extended to style files (.scss) and references (.bib) by pointing to a shared version from the .qmd file where appropriate. These are stored at the top level of the Quarto project. In a similar manner I have a shared directory of images/ as I often re-use the same ones between presentations.

Quarto allows you to execute Python at build time to create images and output. I set up a single shared environment for all slidedecks with requirements listed in a requirements.txt file for easy installation and version control.

Also useful, I have recently begun to break presentations into separate parts using includes . This becomes particularly handy when you want to re-use a specific section of a previous presentation, or add/remove and entire section.

Finally, I control which slidedecks are rendered or re-rendered at each push by listing them in a text file slidelist.txt. Yes, this is a somewhat hacky solution and I could probably do something cleverer by checking locations that have changed in git, but it works for now.

Folder structure

Taking the above points into consideration we have a directory structure as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
slides/
 |
 |- _quarto.yml
 |- style.scss
 |- references.bib
 |- requirements.txt
 |- slidelist.txt
 |
 |- images/
 |   |- ...
 |
 |- extensions/
 |   |- ...
 |
 |- slideshow_1/
 |   |- slideshow_1.qmd
 |   |- _part_1.qmd
 |   |- ...
 |
 |- slideshow_2/
 |   |- slideshow_2.qmd
 |   |- ...
 |
 ...

The post-receive hook

So without any further ado, here is the post-receive hook I am now using:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/bin/bash

GIT_REPO=$HOME/mywebsite.git/
TEMP_DIRECTORY=/tmp/temp
BUILD_DIRECTORY=$HOME/mywebsite_build/
TARGET=/var/www/mywebsite/public_html

# Unset the git directory environment variable from being
# tied to where the hook was fired from.
unset GIT_DIR

echo "    /==============================="
echo "     Post-recieve hook active"

echo "     Moving to build directory."
cd $BUILD_DIRECTORY

echo "     Pulling latest version of website repo."
git pull origin main
git submodule update --recursive

echo "     Building requested slides."
cd $BUILD_DIRECTORY/static/slides/
if [ ! -d .venv/ ]; then
        echo "         Creating virtual environment."
        python3 -m venv .venv
fi
source .venv/bin/activate
pip install -r requirements.txt --quiet
echo "     Building requested slides."
# quarto render requisite slides from slidelist.txt
while read l; do
        quarto render "$l/" --quiet --log "/tmp/$l.log"
        if [ $? == 0 ]; then
                echo "rendered $l"
                rm "/tmp/$l.log"
        else
                echo "Issue rendering $l"
                echo "    see /tmp/$l.log"
        fi
done <slidelist.txt
# remove and rebuild venv each time as it is fast, and
# avoids conflicts when python updates.
rm -r .venv
cd $BUILD_DIRECTORY

echo "     Removing existing site."
rm -rf $TARGET

echo "Building new site."
hugo -s $BUILD_DIRECTORY -d $TARGET

It will be run after every push to main sending feedback back over ssh. Note the extra feature I added whereby the output of quarto is redirected to a logfile in /tmp. This stops it from polluting the feedback over ssh, but means that we can examine output on the server if something goes wrong.

Note also that a side effect of pulling to a local clone as part of the post-recieve hook is that we can’t do any naughty force-pushes. And if we do we need a little manual intervention. Fortunately, if the hook fails for some reason we can always execute it on the server as a bash script.

Footnote - GitHub Actions

It is worth noting that if you only have a single slideshow to deploy this can be done from a GitHub repository using a workflow to deploy to a github.io webpage.

An example of this can be seen in my RSE skills workshop where quarto slides in slides/ are deployed using the deploy_slides.yml workflow.