Anatomy of a template

Developed by two graduate students from Technical University of Berlin, Typst is a computer program for typesetting documents, thoughtfully designed to be fast, portable, feature-packed, and script-able. It would not be unfair to call it a spiritual successor to LaTeX. Together with contributions from talented open-source enthusiasts, it is evolving well and blowing-up LaTeX’s handicaps, 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. And so, 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 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(
  title: [On-bottom stability],
  author: "C Kunte",
  paper: "a4",
) // 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(
  title: [Note title],
  author: "Author",
  paper: "a4",
  body,
) = {
  set document(
    title: title,
    author: author
  ) // metadata

  set page(
    paper: paper, // enables user-definable in main.typ
    numbering: "1",
  ) // page properties

  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)
  ] // print title block

  body
}

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

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

We can do something similar for font size:

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

Typographical controls are covered 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 },
) // typographical properties

Prefer indented classic look for paragraphs? You can do this:

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

Here is a little complex header code to make the 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 report (top-left); template (top-right); report headers (bott).

Some of the above are available at my repository, note. And if you’re a Neovim user, then there is a snippet plug-in too.