Hello, quarto.
Things you get
- A quick overview of how to integrate quarto with hugo.
Prerequisites
- Basic Linux and bash knowledge.
Introduction
Writing scientific articles with reproducible code execution has been on my to-do list for quite some time. Recently, I came across Quarto, a tool that allows you to execute Python cells and save the output to markdown. This process, known as rendering, generates markdown files that can be used by Hugo to generate static HTML.
Quarto provides a seamless integration between Python and Hugo, making it easy to create interactive and dynamic content.
Setup
First, we need to install Quarto. This can be done via an official installer or by installing a specific release. For the latter the following script can be used.
|
|
Next, I needed to add a index.qmd
file to the project root path to be able to call quarto render
from that directory. This will render all *.qmd
files in the project.
For the python runtime it is best practice to set up a virtual environment and install the basic jupyter requirement. I prefer poetry
for dependency management and uv
for creating virtual environments.
So, for me that basic step is
|
|
where my pyproject.toml
looks like this:
|
|
The pyproject.toml
can also be generated by calling poetry init
.
I added the package-mode = false
setting, since I use poetry for dependency management only, i.e. housekeeping my venv.
quarto render
.Finally, you can (and it makes sense) add a configuration file _quarto.yml
to the root path, which contains the global quarto configuration. Right now, I use the following setup:
|
|
However, you can also add quarto settings per document. These will then be merged with the global settings.
Workflow
The main steps of creating content with quarto and hugo are depicted in Figure 1.
graph LR; A(qmd) --> B(jupyter) --> C(md) --> D(Pandoc) --> E(hugo md) A -->|quarto render| E E -->|hugo serve| F(html)
Figure 1: Quarto & hugo workflow.
We first write some content, a quarto markdown file (*.qmd), that might contain diagrams (mermaid, graphviz, …) or python code. Next, quarto renders the file and generates output with the specified format. You can choose from many different output formats; we will need hugo-md
, which is a hugo compatible markdown file.
You can then run quarto preview
from the root path, which will run a hugo serve
command for you and call quarto render
on changes.
IDE
For VS Code you can install the Quarto extension. This gets you some basic shortcuts for running Quarto commands (e.g. quarto preview). Besides that, for me really useful is the preview functionality for diagrams. It lets you create mermaid diagrams or graphviz graphs on the fly and see the changes in a split screen live view !
![Diagram preview](images/quarto-preview.png)
Deployment
For the deployment of this homepage, I use a simple Jenkins CI/CD Pipeline. Here CI means building the website html and CD means simply copying the html to a folder on the same machine that is served by nginx.
When using quarto we have the additional quarto render
step, that has to be executed before running hugo
, which builds the html content. As described earlier we also need to install some binaries and a python environment to be able to run quarto render
.
Installing the required binaries into ${HOME}/bin
on a CentOS machine could look like this:
|
|
My current Jenkinsfile
looks like this:
pipeline{
agent any
environment{
// look at local installation of hugo first if a
// installation with the wrong version exists
PATH="${HOME}/bin:${HOME}/bin/poetry/bin:${WORKSPACE}:${PATH}"
}
stages {
stage('Update submodules') {
steps{
sh "git submodule update --init --recursive"
}
}
stage('Install binaries'){
steps{
sh'''#!/bin/bash
chmod +x ./scripts/install.sh
./scripts/install.sh
'''
}
}
stage('Create python venv and install packages'){
steps{
sh'''#!/bin/bash
# as required by pyproject
uv venv --python 3.9
source .venv/bin/activate
poetry install
'''
}
}
stage('Build static HTML') {
steps{
sh'''#!/bin/bash
set -x
sed -i "s/{{COMMIT}}/${GIT_COMMIT:0:6}/g" config.toml
sed -i "s/{{DATE}}/$(date '+%A %e %B %Y')/g" config.toml
'''
sh "rm -rf public"
sh "poetry run quarto render && hugo --cacheDir $HOME/hugo_cache"
}
}
stage("Update HTML"){
steps{
sh'''#!/bin/bash
set -x
rm -rf /usr/share/nginx/html/*
cp -r public/* /usr/share/nginx/html
'''
}
}
}
}
The relevant parts are adding ${HOME}/bin
to the $PATH
, installing the binaries with our magic script, creating the venv with the correct python version, installing the python project and running the quarto render hugo workflow.
Python Example
Finally, because it is so much fun, I want to showcase a simple python example. It uses the fantastic tool polars. The data file lives in the root path of this post and is called clean_data.csv
.
|
|
|
|
Since we are running a jupyter python kernel we are also allowed to run inline shell scripts with the %%sh
cell magic !
|
|
++ hostname
+ echo 'Hello world from web-01 !'
+ pwd
+ grep -i csv
+ ls -alh .
+ curl wttr.in/Münster
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0100 8696 100 8696 0 0 43049 0 --:--:-- --:--:-- --:--:-- 43049
+ set +x
Hello world from web-01 !
/var/lib/jenkins/workspace/homepage_main/content/posts/quarto-intro
-rw-r--r--. 1 jenkins jenkins 5.0M Jul 13 07:16 clean_data.csv
Weather report: Münster
\ / Sunny
.-. +26(27) °C
― ( ) ― ↖ 9 km/h
`-’ 10 km
/ \ 0.0 mm
┌─────────────┐
┌──────────────────────────────┬───────────────────────┤ Sat 20 Jul ├───────────────────────┬──────────────────────────────┐
│ Morning │ Noon └──────┬──────┘ Evening │ Night │
├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤
│ \ / Sunny │ \ / Sunny │ \ / Sunny │ \ / Sunny │
│ .-. +24(26) °C │ .-. 29 °C │ .-. +29(31) °C │ .-. +24(26) °C │
│ ― ( ) ― ← 9-11 km/h │ ― ( ) ― ↑ 8-9 km/h │ ― ( ) ― ← 14-20 km/h │ ― ( ) ― ↖ 7-15 km/h │
│ `-’ 10 km │ `-’ 10 km │ `-’ 10 km │ `-’ 10 km │
│ / \ 0.0 mm | 0% │ / \ 0.0 mm | 0% │ / \ 0.0 mm | 0% │ / \ 0.0 mm | 0% │
└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘
┌─────────────┐
┌──────────────────────────────┬───────────────────────┤ Sun 21 Jul ├───────────────────────┬──────────────────────────────┐
│ Morning │ Noon └──────┬──────┘ Evening │ Night │
├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤
│ \ / Partly Cloudy │ _`/"".-. Patchy rain ne…│ Cloudy │ .-. Patchy light r…│
│ _ /"".-. +24(26) °C │ ,\_( ). +26(28) °C │ .--. 22 °C │ ( ). 20 °C │
│ \_( ). ↗ 10-13 km/h │ /(___(__) → 10-13 km/h │ .-( ). → 9-16 km/h │ (___(__) → 13-23 km/h │
│ /(___(__) 10 km │ ‘ ‘ ‘ ‘ 9 km │ (___.__)__) 10 km │ ‘ ‘ ‘ ‘ 9 km │
│ 0.0 mm | 0% │ ‘ ‘ ‘ ‘ 0.4 mm | 100% │ 0.0 mm | 0% │ ‘ ‘ ‘ ‘ 1.1 mm | 100% │
└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘
┌─────────────┐
┌──────────────────────────────┬───────────────────────┤ Mon 22 Jul ├───────────────────────┬──────────────────────────────┐
│ Morning │ Noon └──────┬──────┘ Evening │ Night │
├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤
│ Overcast │ \ / Sunny │ \ / Partly Cloudy │ Overcast │
│ .--. 17 °C │ .-. 22 °C │ _ /"".-. 22 °C │ .--. 20 °C │
│ .-( ). → 15-20 km/h │ ― ( ) ― → 16-19 km/h │ \_( ). → 11-17 km/h │ .-( ). → 6-10 km/h │
│ (___.__)__) 10 km │ `-’ 10 km │ /(___(__) 10 km │ (___.__)__) 10 km │
│ 0.0 mm | 0% │ / \ 0.0 mm | 0% │ 0.0 mm | 0% │ 0.0 mm | 0% │
└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘
Location: Münster, Regierungsbezirk Münster, Nordrhein-Westfalen, Deutschland [51.9501317,7.61330165026119]
Follow @igor_chubin for wttr.in updates