Migrating from Pelican to Quarto: A Data Scientist’s Case For The Switch

how-to
quarto
pelican
python
r
reproducibility
I built this site on Pelican. I’m moving it to Quarto. Not because Pelican is bad (it’s a good static site generator), but because if your brand is “reproducible data science,” your website should run code, not just describe it.
Author

Kivan Polimis

Published

May 6, 2026

I built kivanpolimis.com on Pelican in 2016. That was the right call then: Python-based, simple to configure, fast to deploy, Markdown-native. I’ve been writing posts on it for a decade.

I’m moving to Quarto. This post explains why, what the migration looks like technically, and what I think it means for data scientists who maintain personal sites.


What Pelican Does Well (and Where It Stops)

Pelican is a static site generator. You write Markdown, it renders HTML. The workflow for a data science blog is: run your analysis in a notebook or script, export a chart as a PNG, embed the PNG in a Markdown post, deploy. Clean enough.

The problem is that the chart is static. If the underlying data changes, or if you want to show the code that generated the chart, or if you want an interactive visualization, you’re either embedding screenshots or maintaining a separate JavaScript layer that has no connection to your analysis code.

More fundamentally: a static image of a chart is not reproducible. It’s a claim that some code produced some output at some point. Quarto makes the code the source of truth.


What Quarto Actually Is

Quarto is a scientific publishing system. It takes .qmd files (Quarto Markdown) and renders them to HTML, PDF, Word, or slides. Code blocks in .qmd files execute during the render (in Python, R, Julia, or Observable) and their output (tables, plots, printed results) is embedded directly.

A Quarto blog post that includes a regression table generates that table at build time, from the actual model, every time the site builds. The number in the text and the number in the table are the same number because they’re both computed from the same object.

This is not a new idea. R Markdown did this first, and Jupyter notebooks have always supported it. What Quarto adds is:

  • Multi-language support in a single document (Python and R in the same .qmd, sharing a knitr or Jupyter kernel)
  • Scientific publishing features native to the format: BibTeX citations, LaTeX math, cross-references, callout blocks, theorem environments
  • A first-class website publishing system via quarto publish
  • Freeze: a mechanism to cache rendered outputs so you don’t re-execute expensive analyses on every build

The Architecture Comparison

Pelican:

content/
├── posts/
│   ├── nba-shot-charts.md        ← static Markdown
│   └── covid-mortality.md        ← static Markdown
├── images/
│   └── shot_chart_2022.png       ← pre-generated image
└── pelicanconf.py                ← Python config

Quarto:

posts/
├── nba-shot-charts/
│   ├── index.qmd                 ← code + prose, renders together
│   └── _freeze/                  ← cached execution results
├── covid-mortality/
│   └── index.qmd
└── _quarto.yml                   ← site config

The key structural difference: in Quarto, each post is a directory, not a file. The directory contains the prose, the code, any data files, and the freeze cache. The post is a self-contained unit.


A Concrete Example: Reproducing the NBA Shot Chart Post

My NBA Shot Charts post was written in 2022 with static images. Here’s what the Quarto version looks like:

Old approach (Pelican):

# NBA Shot Charts — Part 1

I generated shot charts for every player this season.
Here's the chart for Russell Westbrook:

![Shot chart](images/westbrook_shot_chart.png)

New approach (Quarto):

---
title: "NBA Shot Charts — Part 1"
date: "2026-05-17"
execute:
  echo: true
  warning: false
---

# NBA Shot Charts — Part 1

I'm pulling shot data from the NBA Stats API and generating
charts using `matplotlib` and `BasketballMap`.

```python
from nba_api.stats.endpoints import shotchartdetail
import matplotlib.pyplot as plt

# Pull Westbrook's shots from the 2021-22 season
shot_data = shotchartdetail.ShotChartDetail(
    team_id=0,
    player_id=1630566,
    season_nullable="2021-22",
    season_type_all_star="Regular Season",
    context_measure_simple="FGA",
).get_data_frames()[0]

# Draw the court and plot
fig, ax = draw_court(ax=None, outer_lines=False)
ax.scatter(shot_data["LOC_X"], shot_data["LOC_Y"],
           c=shot_data["SHOT_MADE_FLAG"].map({1: "green", 0: "red"}),
           alpha=0.5, s=30)
ax.set_xlim(-300, 300)
ax.set_ylim(-100, 500)
plt.title("Russell Westbrook Shot Chart — 2021-22")
plt.show()
```

The chart above is generated fresh each time the site builds.
If I update the data pull or the styling, the post updates automatically.

The code is visible (because echo: true), it runs during build, and the chart is generated from the actual data. Someone reading the post can copy that code block and reproduce the chart themselves.


The Freeze Mechanism

Quarto’s _freeze directory is what makes this practical. Executing an API call and rendering a chart on every site build would be slow and fragile. Freeze caches the execution results:

# _quarto.yml
execute:
  freeze: auto   # re-execute only when the source file changes

With freeze: auto, a post’s code only re-executes when you edit the .qmd file. The cache stores the rendered outputs, so quarto render on an unchanged post is instant. The NBA API doesn’t get hit on every git push.

You can force re-execution when you want fresh data:

quarto render posts/nba-shot-charts/ --execute

The Migration Process

For existing Pelican posts, the migration is mostly mechanical:

1. Convert Markdown to Quarto Markdown

Pelican metadata headers look like:

Title: NBA Shot Charts — Part 1
Date: 2022-02-27
Category: Sports
Tags: nba, shot-charts, python

Quarto uses YAML front matter:

---
title: "NBA Shot Charts — Part 1"
date: "2022-02-27"
categories: [Sports, NBA, Python]
---

A short Python script handles the conversion for all existing posts.

2. Move static images into post directories

# Old structure
mkdir -p posts/nba-shot-charts-part-1/
mv content/posts/nba-shot-charts.md posts/nba-shot-charts-part-1/index.qmd
mv content/images/shot_chart_*.png posts/nba-shot-charts-part-1/

3. Update image references

Pelican uses absolute paths from the site root. Quarto uses relative paths from the post directory:

# Old (Pelican)
![Chart](/images/shot_chart.png)

# New (Quarto)
![Chart](shot_chart.png)

4. Convert interactive posts to .qmd with code execution

This is the real work and where I’m spending most of the migration time. The COVID mortality series, the NBA MVP comparisons, the shot charts: these all have code that generated the original analysis. Turning them into executing .qmd posts requires verifying the code still runs against current package versions.


Publishing

# One-time setup
quarto publish gh-pages

# Subsequent deploys
quarto render && quarto publish gh-pages

Quarto renders to _site/ and pushes to the gh-pages branch. GitHub Pages serves the result. The workflow is the same as Pelican’s; only the render step changes.


Why This Matters for the “Rigor at Scale” Brand

The websites that shaped my sense of what a data scientist’s personal site should look like (Eugene Yan’s, Vicki Boykis’s, Chip Huyen’s) are notable for being genuinely technical. Not just descriptions of technical work, but the work itself, documented with enough specificity that you could reproduce it.

Quarto makes that easier because the code and the prose live in the same file. The analysis doesn’t exist separately from the writing about it. Every post is its own reproducible artifact.

That’s the brand case. The practical case is simpler: I’m tired of updating PNG files manually when I fix a bug in a chart. Quarto fixes that permanently.


Current Status

The existing Pelican site stays live during the migration. I’m converting posts one category at a time, starting with Tutorials (most code-heavy) and working toward the How-to and Review posts. The target is a fully Quarto-powered site by the end of Q3 2026.

The Pelican source is archived. The Quarto source is at kpolimis.github.io. PRs welcome if you find something that didn’t migrate cleanly.