Anatomy of a template

Typst is a computer program for typesetting documents. Developed by two graduate students from Technical University of Berlin, it is thoughtfully designed to be fast, portable, feature-packed, and script-able. It wouldn’t be unfair to call it the spiritual successor to LaTeX. It is evolving well and taking-on LaTeX’s handicaps with contributions from talented open-source enthusiasts, e.g.,

With good defaults (paper size, font-face, font-size, margins, paragraph properties, etc.), one can get going without needing a single line of preamble code. But if you are a seasoned LaTeX user, then you’d know how useful document classes are — something Typst does not yet come pre-packaged with. A little universe is starting to form — for the users, by the users. If you’re one of them, like I am, and your tastes (or needs) are slightly different from those available there-in, then it would not hurt to know how to write a Typst template from scratch. Here is how — in this note-to-self, like much of this website.

Rule One of publishing is to separate presentation (styling) from content, be it offline or online. So, we create two files:

main.typ: This requires only a few key user-inputs (which would be variables pre-defined in template.typ), e.g. document title, name of the author, and paper size (useful for me) other than the content itself.

#import "template.typ": *
#show: note.with(
  author: "C Kunte",
  paper: "a4",
  title: [On-bottom stability],
)
// report content from here-on

template.typ: Basic preamble is as follows. Defaults will be used when things like margins are undefined in either of the two files.

#let note(
  author: "Author",
  paper: "a4",
  title: [Note title],
  body,
) = {
  // metadata
  set document(title: title, author: author)
  // page properties
  set page(
    paper: paper, // enables user-definable in main.typ
    numbering: "1",
  )
  // print title block (includes title, author, date)
  align(center)[
    #text(2em)[*#title*]
    #v(2em, weak: true)
    #text(1em, author)
    #v(1em, weak: true)
    #datetime.today().display("[month repr:long] [day], [year]")
    #v(5em, weak: true)
  ]
  // content from here-on
  body
}

If I were to automate setting margins based on paper size, then it would be like so, in which top margin overrides y parameter to make room for a custom header.

set page(
  // set margins based on paper size
  margin: if paper == "a5" {
    (x: 0.75in, y: 0.75in, top: 0.9in)
  } else {
    (x: 1.0in, y: 1.0in, top: 1.25in)
  },
)

We can do something similar for font size:

set text(
  size: if paper == "a5" { 11pt } else { 12pt },
)

If you’re a typographical nut, then you’re covered with features to tune your text too, e.g.,

set text(
  font: "Segoe UI", // e.g. "STIX Two Text" or "erewhon",
  top-edge: "cap-height", 
  bottom-edge: "baseline",
  number-type: "old-style",
  size: if paper == "a5" { 11pt } else { 12pt },
  )

Like indented classic look for paragraphs? Then you can do this:

set par(
  spacing: 0.65em, 
  leading: 0.65em, 
  first-line-indent: 12pt, 
  justify: true
)

Here is a little complex header code to make title of the note (paper) appear on every even page, and the active section name appear on every odd page other than the first.

set page(
  header: context {
    // set custom header: make title appear on even pages
    if calc.even(counter(page).get().first()) { 
      emph(title) 
    } else { none }

    // make section appear on odd pages other than the first
    let page-num = counter(page).get().first()
    if page-num > 1 and calc.odd(page-num) {
      let headings = query(heading)
      let curr-heading = none
      let found = false

      for heading-elem in headings {
        if heading-elem.location() != none and heading-elem.location().page() == page-num {
          curr-heading = heading-elem.body
          found = true
        } else if heading-elem.location() != none and heading-elem.location().page() < page-num {
          curr-heading = heading-elem.body // keep track of the last heading on a prev page
        } else if found { break } // stop once we have moved past the curr page
      }
      align(right, emph(curr-heading))
    } else { none }
  }, // context ends
) // page ends
View of a report compiled using Typst typesetting computer program View of a report headers compiled using Typst typesetting computer program
View of the compiled report - first page (top), headers (bott).

Some of the above and more are available at my repository, note. And if you’re a Neovim user, then you can use my plugin: typst-snippets-vim.