- Setting up Quarto
- Setting up python
- Setting up Git
- Setting up GitHub Actions
- Parting words: iterating on this project
First, we make sure Quarto is installed, and then use it to create a project in the current working directory with quarto project create
. This command is interactive and will guide you through creating the kind of project you want. In this case, the key point is that we want a website project to be created in the current working directory.
This will create a few files for use: the _quarto.yml
file for whole-project configuration, the index.qmd
file that will serve as the homepage and entrypoint for the website, and an about.qmd
that will serve as an “About” page in a separate “tab” on the website. We also get a styles.css
file, which allows us to do some aesthetic tweaks to what would otherwise be raw, boring HTML. The said, we don’t have to touch it in this case, as the Quarto renderer comes with a number of CSS themes built-in.
Note
The power of Quarto is that it allows you to write simple Markdown and then render it into very long list of other formats, e.g., PowerPoint, PDF, etc. Because we’re using it to make a website, Quarto will be rendering our markdown into HTML, which can then be viewed in the browser.
Before we proceed, let’s tweak our Quarto configuration a bit. We’ll do these tweaks at two levels: the whole-project level in _quarto.yml
, and in what’s referred to as the frontmatter in each .qmd
file.
First, open the _quarto.yml
in your favorite editor (VSCode, Positron, and RStudio have the best support for Quarto, though you can use any editor you can set to recognize .qmd
files as Markdown). Start by modifying the top block with an additional setting: output-dir: docs
.
_quarto.yml
project:
type: website
output-dir: docs
Note that the YAML language is very sensitive about indentation, similar to Python. Make sure your editor has good auto-indentation support when modifying these files.
Next, we’ll modify the website block, which controls navigation and other website behaviors. In this case, I’m going to add a page by putting this qmd
file in the list:
_quarto.yml
website:
title: "Creating Github-hosted Dashboards"
navbar:
left:
- href: index.qmd
text: Home
- about.qmd
- setup.qmd
We won’t touch the format section, but just note that that’s where we specify the theme we want applied to our HTML. These adjustments could be switching to a different built-in theme, or making out own tweaks manually in styles.css
.
Finally, we’re going to add a section at the bottom that tells Quarto not to rerun code unless it has been updated:
_quarto.yml
execute:
freeze: auto
As the look and feel of our project comes together, we can expect to return to the _quarto.yml
, but for now, that’s all we’ll need. There are a huge number of controls we can tweak, so I recommend keeping the Quarto docs handy.
Each .qmd
has its own YAML-based configuration, which you’ll see in index.qmd
. To demo some of the controls we can use, I modified my index.qmd
frontmatter to the following:
index.qmd
---
title: "Creating Github-hosted Dashboards"
format:
html:
embed-resources: true
keep-ipynb: false
author:
- name: Nick Minor
orcid: 0000-0003-2929-8229
email: [email protected]
affiliation:
- University of Wisconsin - Madison
- Wisconsin National Primate Research Center
editor: source
jupyter: python3
---
We don’t actually need most of this–the key parts are telling it that we want to use the jupyter engine with Python, that we don’t need to keep the intermediate Jupyter representation after rendering, and that we don’t want our HTML to depend on external files. Instead, we want everything embedded into the HTML, which results in a larger HTML but also a simpler, more foolproof project structure.
Like with _quarto.yml
, there are dozens of controls you can use here; if you need something, check the docs for how to get it. Chances are, there’s a setting for it.
That’s all we need to get Quarto working, but we still need to set up Python so that Quarto can run Python code. To do that, I’m going to use the inimitable uv
package manager. The best thing about uv
is that it’s fast, really fast.
The second best thing is that it can, like Quarto itself, be configured with a single, declarative configuration file called pyproject.toml
. This file is in the TOML language instead of YAML, but don’t worry; it should be similarly readable and relatively obvious in what it’s specifying. The uv
team has gone to great lengths to make sure this file is compliant with Python-ecosystem standards, which gets rid of headaches you get with some other Python managers like Poetry. uv
has tons of killer features and is one of the best things to happen to Python in a long time.
To get started, make sure you have uv
installed, and then run uv init --name quarto_dashboards --lib .
. This will create a project called quarto-dashboards
in the current working directory that is a library rather than an executable. If and when our code gets too big to be viewed in our website, we can put it in our python library and import it in our Quarto python. Cool!
This will also generate a Python virtual environment and the aforementioned pyproject.toml
for us. Now, let’s put some stuff in it! Our website will need a few things: the Jupyter engine, a Python kernel, and the labkey Python API, all of which are available on the Python Package Index (PyPI) and are thus installable with uv
. Just run uv add jupyter ipykernel labkey
and then observe the following change in pyproject.toml
:
pyproject.toml
dependencies = [
"ipykernel>=6.29.5",
"jupyter>=1.1.1",
"labkey>=3.3.0",
]
As you can see, we now have our dependencies locked with precise versions, which means our environment will be reproducible.
To get into our virtual environment, run source .venv/bin/activate
. You will now have access to your dependencies.
Tip
One of the ways uv
is standards compliant is by using source .venv/bin/activate
, which is used by many other Python environment managers. That said, it’s kind of verbose and ugly, a lot to type.
Because I move in and out of Python environments a lot, I’ve placed a few aliases (shorthands) for this in the .zshrc
file in my home directory, which is run every time I launch a new terminal window or tab:
~/.zshrc
alias uvv='uv sync --all-extras && source .venv/bin/activate'
alias uvs='uv sync --all-extras'
alias a='source .venv/bin/activate'
alias d='deactivate'
If you use bash
and not zsh
, placing these aliases in your .bashrc
will work too.
With that, you’ll be able to run uvv
to sync and activate a virtual environment–three keystrokes instead of 50. So power, very efficiency.
With that, we have what we need to start working on our website. In a new terminal window, tab, or split, activate your virtual environment, and then run quarto preview
. If we did everything above correctly, this will open a new browser window or tab with our rendered in-progress website for us. Every time we save changes to our .qmd
file, the Quarto preview will see this and re-render our website. Amazing!
You may be tempted to get writing, but first, do your future self a favor and get your version control organized. The key to this will be your .gitignore
. .gitignore
files tell git (you guessed it) what to ignore. This is helpful, but these files can quickly become a Sisyphean task; the bigger your project gets, the more you have to add line after line after line of new things you have to ignore. This makes it easy to accidentally commit files you didn’t mean to.
Instead, we’re going to invert the logic of our .gitignore
file: we’re going to use it to say ignore everything by default, and then only add a line for each exception to that rule. This means we’ll only ever be able to stage and commit files and directories that we’ve explicitly allowed in our .gitignore
. Inverting your logic means more work up front, of course, but your future self will thank you.
This method results in a .gitignore
that looks like this at the time of this writing:
.gitignore
*
# project root exceptions
!.gitignore
!justfile
!_quarto.yml
!about.qmd
!index.qmd
!setup.qmd
!styles.css
!pyproject.toml
!uv.lock
!README.md
!LICENSE
!.python-version
# python library code
!/src
!/src/quarto_dashboards
!/src/**/*.py
# github workflows
!/.github
!/.github/workflows
!/.github/workflows/*.yml
!/.github/workflows/*.yaml
You’ll see ignoring everything is as simple as starting the file with *
, the glob wildcard for anything. Then, for each directory and file we want to allow, we prepend the path with a bang !
, which is the not operator.
We only allow Quarto project files, uv
project files, python scripts from our library, and GitHub workflows we’ll eventually write. With this setup, we’ll never accidentally push Jupyter notebooks, JavaScript files Quarto generates, etc.
Now, just run git init
in your terminal and have at it.
Rather than rendering our website itself, we’re going to use GitHub to do that for us. To do so, we’re going to return to YAML and put together some workflows. The overall architecture here will be:
- Leave our current project setup in a main git branch.
- Use a new branch called
gh-pages
to actually render the website files. - Use GitHub pages to host the files output into the
docs
directory in ourgh-pages
branch. - Make sure GitHub re-renders our website whenever changes are pushed to the
main
branch.
Conveniently, the Quarto developers anticipated this use case and wrote a very helpful tutorial for it, which I’ll partially reproduce here.
The first thing we need to do is set up a “remote”, which is to say a repository on GiHub that we can sync with this project. To do so, I went to the dholab
GitHub org, hit the green new button, and made a repo called “2025-github-website-demo”. I then staged all the files allowed in .gitignore
, committed them, and then ran the following to set that repo as our remote
git branch -M main
git remote add origin https://github.com/dholab/2025-github-website-demo.git
git push -u origin main
With that, the rest is extremely simple: run quarto publish gh-pages
, which will create the gh-pages
branch and plug it into a .github.io
site, and then paste the following Github workflow into the main branch:
.github/workflows/publish.yml
on:
workflow_dispatch:
push:
branches: [main]
name: Quarto Publish
jobs:
build-deploy:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Quarto
uses: quarto-dev/quarto-actions/setup@v2
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"
- name: Install the project
run: uv sync --all-extras --dev
- name: Render and Publish from local venv
run: |
source .venv/bin/activate
git config --global user.name 'GitHub Actions Bot'
git config --global user.email '[email protected]'
quarto publish gh-pages --no-browser
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Define a cache dependency glob
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
This workflow, adapted from the above Quarto tutorial plus the uv
docs, essentially replicates what we’ve done above, except with the benefit of a previously created uv
environment (that’s what uv.lock
records).
With everything set up, I recommend the following workflow for iterating on this project:
- Whenever you starting making changes, make sure you have
quarto preview
running within the python environment. That way, you can keep an eye on the rendered project as you update you.qmd
and Python code. - If you have just installed, run
just readme
whenever you updatesetup.qmd
. This keeps the repo readme up to date. - Be sure to use
uv add
to record any dependencies throughout your Python or.qmd
files. Likewise for allowing new files to be git-tracked by adding explicit exceptions to.gitignore
. - For adding Python, start with putting code into blocks in your
.qmd
files. When that code starts to get a bit large, e.g. >20 lines, consider placing it in our library and importing it as a function instead. - Keep in mind that as currently configured, the website-update action will run any time any file in main is updated. If you’re pushing lots of little updates, it may make sense to either a) temporarily disable the publishing action, or b) restrain the action trigger to only when particular files, e.g., particular
.qmd
or.py
files, are updated.