Building a Programmatic Academic CV with Quarto and Typst

quarto
typst
tutorial
academic
This is a walkthrough of building a data-driven academic CV using Quarto and Typst. It includes publications pulled automatically from a BibTeX file, custom styling, and a clean design that rivals LaTeX or InDesign.
Author

Cameron Wimpy

Published

4/5/2026

Modified

4/6/2026

If you have ever wrestled with LaTeX to format a CV, you know the pain: cryptic error messages, fragile spacing, package conflicts, and the nagging feeling that there must be a better way. There might be. Typst is a modern typesetting system that is fast, readable, and increasingly supported by Posit through Quarto. In this post, I walk through how I built a fully programmatic academic CV that pulls publications directly from a BibTeX file, formats them in APSA style with hanging indents and bold author highlighting, and produces a clean PDF that I think competes with anything you could design in Adobe InDesign.

The full source code is available on GitHub.

Why Typst?

LaTeX has been the standard for academic typesetting for decades, and for good reason—it handles complex math, cross-references, and bibliographies well. But it comes with real costs:

  • Compilation is slow. A multi-page document with bibliographies can take several seconds. Typst compiles in milliseconds.
  • The syntax is hostile to newcomers. Even experienced users regularly fight with vspace, hfill, and mysterious overfull hbox warnings.
  • Error messages are cryptic. A missing brace on line 47 might produce an error on line 312.
  • Package management is fragile. CTAN packages can conflict, and getting a specific look often requires stacking multiple packages.

Typst addresses all of these. Its syntax reads like a mix of Markdown and a scripting language. It compiles instantly. And because Quarto now supports Typst as a first-class output format, you can combine R or Python code chunks with Typst templates in a single .qmd file.

The architecture

My CV has three files:

my-cv/
├── my-cv.qmd           # Content and data (Quarto document)
├── cv-template.typ      # Design and layout (Typst template)
└── publications.bib     # Bibliography (BibTeX)

The .qmd file contains the CV content and an R chunk that reads the BibTeX file. The .typ file defines the visual design—fonts, colors, spacing, and custom functions for entries. The .bib file is the same one I use for my website and papers. One source of truth for publications, used everywhere.

The Typst template

The template (cv-template.typ) is where the design lives. At the top, I define colors and fonts:

#let color-darknight = rgb("#131A28")
#let color-darkgray = rgb("#333333")
#let color-gray = rgb("#5d5d5d")
#let color-lightgray = rgb("#999999")

#let font-header = ("Myriad Pro", "Arial", "Helvetica")
#let font-text = ("Myriad Pro", "Arial", "Helvetica")

If you have Myriad Pro installed (it comes with Adobe products), the CV uses it. Otherwise, it falls back to Arial. This is already easier than LaTeX, where using a custom font often requires fontspec, XeLaTeX, and a specific compilation chain.

The CV entry function

The core building block is cv-entry, a reusable function for employment, education, grants, and everything else:

#let cv-entry(
  title: "",
  organization: "",
  location: "",
  dates: "",
  description: "",
  amount: none
) = {
  pad[
    #justified-header(title, location, amount: amount)
    #secondary-justified-header(organization, dates)
    #if description != "" and organization != "" [
      #v(0.2em)
      #pad(left: 1em,
        text(size: 11pt, fill: color-gray)[- #description]
      )
    ]
  ]
  v(0.5em)
}

This produces a clean two-line entry with the title and location on the first line (justified left and right), and the organization and dates on the second. The amount parameter is optional—I use it for grants:

#cv-entry(
  title: "Exploring Rural Election Administration",
  organization: "MIT Election Data and Science Lab",
  dates: "2022",
  amount: "$95,173",
)

Compare this to the LaTeX equivalent, which typically involves tabular environments, manual hfill calls, and careful spacing. In Typst, the function handles all of that.

Section headings

Section headings get a bold title with a full-width rule underneath:

#let section(title) = {
  set block(above: 1.5em, below: 1em)
  set text(size: 16pt, weight: "regular")
  stack(
    spacing: 0.3em,
    text(color-accent, weight: "bold")[#title],
    line(length: 100%)
  )
}

Quarto’s heading syntax (# Education) automatically maps to this function through a show rule:

#show heading.where(level: 1): it => section(it.body)

This means the .qmd content file uses plain Markdown headings, and the template handles the styling. Clean separation of content and design.

Programmatic publications

This is the part that makes the CV truly programmatic. Instead of manually typing each publication (and inevitably introducing inconsistencies), an R chunk reads publications.bib and generates Typst data structures at render time.

The R chunk

library(RefManageR)
bib <- ReadBib("publications.bib")

cat("```{=typst}\n")
cat("#let my-publications = (\n")

for (i in 1:length(bib)) {
  entry <- bib[[i]]
  cat("  (\n")
  cat("    type: \"article\",\n")
  cat("    author: \"", authors, "\",\n", sep="")
  cat("    title: \"", title, "\",\n", sep="")
  # ... more fields
  cat("  )")
}

cat(")\n")
cat("#display-publications-from-data(\n")
cat("  publications: my-publications,\n")
cat("  bold-name: \"Cameron Wimpy\",\n")
cat("  reverse-numbering: true,\n")
cat("  categories: (\n")
cat("    (\"article\", \"Journal Articles\"),\n")
cat("    (\"incollection\", \"Book Chapters\"),\n")
cat("    (\"essay\", \"Essays & Reviews\")\n")
cat("  )\n")
cat(")\n")
cat("```\n")

This chunk uses RefManageR to parse the BibTeX file, then emits raw Typst code as a data array. Each publication becomes a Typst dictionary with fields like type, author, title, journal, year, etc. The display-publications-from-data function (defined in the template) takes this array and renders formatted citations.

APSA-style formatting in Typst

The template’s format-publication function handles APSA citation style with proper hanging indents:

#let format-publication(entry, number: none, bold-name: "Cameron Wimpy") = {
  set text(size: 10pt, fill: color-darknight)
  set par(leading: 0.65em, hanging-indent: 2em)

  // Build citation string in APSA format
  // Author. Year. "Title." Journal Volume (Number): Pages.
}

The key line is hanging-indent: 2em—Typst handles this natively. In LaTeX, you would need something like the hangparas environment from the hanging package, or manual hangindent and hangafter commands.

The function also bolds your name automatically:

#let bold-author-name(authors-str, bold-name) = {
  let parts = authors-str.split(regex(" and |, and | & "))
  let formatted-parts = ()
  for part in parts {
    let clean-part = part.trim()
    if clean-part.contains(bold-name) {
      formatted-parts.push(text(weight: "bold")[#clean-part])
    } else {
      formatted-parts.push(clean-part)
    }
  }
  formatted-parts.join(" and ")
}

Automatic categorization and numbering

Publications are automatically sorted by year (newest first), grouped into categories (Journal Articles, Book Chapters, Essays & Reviews), and numbered in reverse order. Adding a new publication means adding it to publications.bib and re-rendering. No manual formatting, no renumbering, no copy-paste errors.

Equal authorship is flagged automatically with a star symbol when the BibTeX entry includes note = {equal authorship}.

The header

The CV header uses Font Awesome and Academicons for social links, laid out in a three-column grid:

#grid(
  columns: (1fr, 1fr, 1fr),
  [
    #fa-icon("location-dot") $address$ \
    #fa-icon("envelope") #link("mailto:$email$")[$email$] \
    #fa-icon("orcid", font: "Font Awesome 6 Brands") #link("$orcidurl$")[$orcidhandle$]
  ],
  [
    #fa-icon("phone") $phone$ \
    #fa-icon("earth-americas") #link("$websiteurl$")[$websitedisplay$] \
    #fa-icon("linkedin", font: "Font Awesome 6 Brands") #link("$linkedin$")[$linkedinhandle$]
  ],
  [
    #fa-icon("x-twitter", font: "Font Awesome 6 Brands") #link("$twitter$")[\@$twitterhandle$] \
    #fa-icon("github", font: "Font Awesome 6 Brands") #link("$github$")[$githubhandle$] \
    #ai-icon("google-scholar") #link("$google-scholar$")[$google-scholarhandle$]
  ]
)

The \(variable\) syntax is Quarto’s template variable interpolation—values come from the YAML frontmatter in the .qmd file:

firstname: "Cameron"
lastname: "Wimpy"
email: "cwimpy@astate.edu"
website: "cwimpy.com"
github: "https://github.com/cwimpy"
orcidurl: "https://orcid.org/0000-0002-2049-5229"

Rendering

To build the CV:

quarto render my-cv.qmd

That is it. Quarto runs the R chunk, which reads the BibTeX file and emits Typst code, then Typst compiles the whole thing into a PDF. On my machine, the entire process takes about two seconds for a 13-page CV.

What I like about this approach

  1. Single source of truth. Publications live in one .bib file used across my CV, website, and papers.
  2. Separation of content and design. The .qmd has the data; the .typ has the styling. Change the font or colors in one place.
  3. Readable source files. Anyone can look at cv-entry(title: “Department Chair”, …) and understand what it does.
  4. Fast iteration. Typst compiles in milliseconds, so you can preview changes instantly.
  5. Programmatic publications. Add a BibTeX entry, re-render, done. No manual formatting.
  6. Modern tooling. Quarto, R, and Typst are all actively developed and well-documented.

Getting started

If you want to try this yourself:

  1. Install Quarto (1.4 or later for Typst support)
  2. Clone the repository and navigate into it:
git clone https://github.com/cwimpy/my-cv.git
cd my-cv
  1. Replace the content in my-cv.qmd with your own information
  2. Replace publications.bib with your own bibliography
  3. Render the CV:
quarto render my-cv.qmd

The Typst template is designed to be modifiable. Change the colors at the top, swap the fonts, adjust the spacing—it is all in one file with clear variable names.

Typst is still young, but Posit’s investment in Quarto integration signals that it has a real future in academic publishing. For CVs, it is already better than LaTeX in almost every way that matters.

Back to top