repos
/ orchard main

orchard

mirror

Every site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.

blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics

7.6 KB · 214 lines · Typst Raw History
  1// The resume, as Typst.
  2//
  3// It replaces a Chromium: the Next.js era rendered an HTML template through
  4// headless Playwright and printed it to PDF, which meant a browser, a Markdown
  5// parser and two npm parsing libraries in the build path of a one page
  6// document. Every other PDF in this repo is already Typst, so this is the one
  7// that was out of step.
  8//
  9// The design is a port, not a redesign. Every number below is the old
 10// stylesheet's, converted once: CSS pixels are 1/96in and Typst points are
 11// 1/72in, so a CSS px is 0.75pt and that factor is the whole translation. The
 12// original PDF embedded Liberation Sans, because "Helvetica Neue, Helvetica,
 13// Arial" resolved to it on the machine that generated it, so naming Liberation
 14// Sans directly is what keeps the metrics identical rather than merely close.
 15
 16#let data = yaml("content.yml")
 17
 18// One CSS pixel. Kept as a function so the stylesheet's numbers can be
 19// transcribed as they were written instead of pre-multiplied by hand.
 20#let px(n) = n * 0.75pt
 21
 22// Liberation Sans, in em above and below the baseline. These are the font's
 23// own metrics and they are what makes the line maths below exact rather than
 24// tuned by eye.
 25#let ascent = 0.9053
 26#let descent = 0.2119
 27
 28// CSS `line-height`, reproduced.
 29//
 30// This is the one place the two systems genuinely disagree and it is why a
 31// first attempt at this port came out vertically cramped on every line. A CSS
 32// line box is `line-height` tall and the glyphs sit in the middle of it, with
 33// the leftover split evenly above and below (half-leading). Typst's line box
 34// is the font's own ascender-to-descender extent, and `leading` adds space
 35// only BETWEEN lines, never above the first or below the last.
 36//
 37// So block heights came out short by the half-leading at both ends, and every
 38// margin measured from a block edge landed too tight. Setting the text edges
 39// to include the half-leading makes a Typst line box the same height as a CSS
 40// one, which lets `leading` go to zero and every margin below be the
 41// stylesheet's number unmodified.
 42#let line-box(line-height) = (
 43  top-edge: (ascent + (line-height - ascent - descent) / 2) * 1em,
 44  bottom-edge: -(descent + (line-height - ascent - descent) / 2) * 1em,
 45)
 46
 47#let ink = rgb("#1a1a1a")
 48#let heading-ink = rgb("#111111")
 49#let summary-ink = rgb("#444444")
 50#let meta-ink = rgb("#666666")
 51#let bullet-ink = rgb("#333333")
 52#let accent = rgb("#2563eb")
 53#let sidebar-bg = rgb("#1e293b")
 54#let sidebar-ink = rgb("#e2e8f0")
 55#let sidebar-muted = rgb("#cbd5e1")
 56#let sidebar-label = rgb("#94a3b8")
 57#let sidebar-rule = rgb("#334155")
 58#let link-ink = rgb("#93c5fd")
 59#let contact-muted = rgb("#dbeafe")
 60
 61#let sidebar-width = px(240)
 62#let page-width = px(816)
 63#let page-height = px(1056)
 64
 65#set page(width: page-width, height: page-height, margin: 0pt)
 66#set text(
 67  font: "Liberation Sans",
 68  size: 9.5pt,
 69  fill: ink,
 70  // The stylesheet never hyphenated, because browsers do not unless asked.
 71  // Typst does, so leaving it on breaks words in the narrow right column that
 72  // the reference PDF keeps whole.
 73  hyphenate: false,
 74  ..line-box(1.35),
 75)
 76// Zero, because line-box above already carries the full line height. Anything
 77// here would be added on top of it.
 78#set par(leading: 0pt, spacing: 0pt)
 79#show link: it => it
 80
 81// A sidebar section: a muted uppercase label over a hairline, then content.
 82#let sidebar-section(label, body) = block(width: 100%)[
 83  #block(below: px(3))[
 84    #block(below: px(3))[
 85      #text(size: 9.5pt, weight: "bold", tracking: px(0.5), fill: sidebar-label)[
 86        #upper(label)
 87      ]
 88    ]
 89    #line(length: 100%, stroke: px(1) + sidebar-rule)
 90  ]
 91  #body
 92]
 93
 94// The dark right column. Placed rather than laid out in the flow, because it
 95// is full bleed to three edges and the main column has to sit beside it, which
 96// is what `position: absolute` bought in the stylesheet.
 97#place(
 98  top + right,
 99  block(width: sidebar-width, height: page-height, fill: sidebar-bg)[
100    // `place(right)` sets the alignment for everything inside it too, so
101    // without this every line in the column is flush right.
102    #set align(left)
103    #block(
104      width: 100%,
105      fill: accent,
106      inset: (top: px(36), right: px(24), bottom: px(20), left: px(24)),
107    )[
108      #block(below: px(4))[
109        #text(size: 11pt, weight: "bold", fill: sidebar-ink)[#data.title]
110      ]
111      #block(below: px(8))[
112        #text(size: 9pt, fill: contact-muted)[#data.location]
113      ]
114      #block(below: px(2))[
115        #text(size: 9pt, weight: "bold", fill: white)[#data.phone]
116      ]
117      #text(size: 9pt, fill: white)[
118        #link("mailto:" + data.email)[#data.email]
119      ]
120    ]
121
122    #block(inset: (x: px(24), y: px(14)), width: 100%)[
123      // The stylesheet used flex `gap: 12px`, which is space between children
124      // and not after the last one.
125      #let sections = (
126        sidebar-section("Links")[
127          #for l in data.links [
128            #block(below: px(1))[
129              #text(size: 9pt, fill: link-ink)[#link(l.url)[#l.label]]
130            ]
131          ]
132        ],
133        sidebar-section("Skills")[
134          #for s in data.skills [
135            #block(below: px(1))[#text(size: 9pt, fill: sidebar-muted)[#s]]
136          ]
137        ],
138        // Inline and comma joined, which the stylesheet did with
139        // `display: inline` plus an `::after` comma on all but the last.
140        sidebar-section("Technologies")[
141          #text(size: 9pt, fill: sidebar-muted)[#data.technologies.join(", ")]
142        ],
143        sidebar-section("Education")[
144          #for e in data.education [
145            #text(size: 9pt, fill: sidebar-muted)[
146              #text(weight: "bold", fill: sidebar-ink)[#e.institution,]
147              #e.years \
148              #e.degree
149            ]
150          ]
151        ],
152      )
153      #for (i, s) in sections.enumerate() [
154        #block(below: if i + 1 == sections.len() { 0pt } else { px(12) })[#s]
155      ]
156    ]
157  ],
158)
159
160// The main column. `margin-right: 240px` in the stylesheet, so its box is the
161// page less the sidebar and its own padding sits inside that.
162#block(
163  width: page-width - sidebar-width,
164  inset: (top: px(36), right: px(32), bottom: px(36), left: px(38)),
165)[
166  #block(below: px(5))[
167    #text(size: 26pt, weight: "bold", fill: heading-ink, ..line-box(1.1))[
168      #data.name
169    ]
170  ]
171
172  #block(below: px(14))[
173    #text(size: 9.5pt, fill: summary-ink, ..line-box(1.4))[#data.summary]
174  ]
175
176  #for (i, section) in data.sections.enumerate() [
177    // `margin-top: 12px`, except on the first heading.
178    #block(above: if i == 0 { 0pt } else { px(12) }, below: px(8))[
179      #block(below: px(2))[
180        #text(size: 10.5pt, weight: "bold", tracking: px(0.5), fill: accent)[
181          #upper(section.heading)#if "note" in section {
182            text(size: 8.5pt, weight: "regular", tracking: 0pt)[ — #section.note]
183          }
184        ]
185      ]
186      #line(length: 100%, stroke: px(1) + accent)
187    ]
188
189    #for entry in section.entries [
190      #block(below: px(8), width: 100%)[
191        #block(below: px(1))[
192          #text(size: 10.5pt, fill: heading-ink)[
193            #text(weight: "bold")[#entry.org —]
194            #text(weight: "regular", style: "italic")[#entry.role]
195          ]
196        ]
197        #block(below: px(3))[
198          #text(size: 8.5pt, fill: meta-ink)[#entry.meta]
199        ]
200        #set text(size: 9pt, fill: bullet-ink)
201        #list(
202          // `ul { padding-left: 16px }` with an outside marker, so the body
203          // edge is 16px in and the disc hangs back into that space.
204          indent: px(3),
205          body-indent: px(6),
206          spacing: px(1),
207          marker: text(size: 9pt)[],
208          ..entry.bullets,
209        )
210      ]
211    ]
212  ]
213]