There are already other ways to do this. Of course I had to do it the
hard way.
The standard approach to a ukulele songbook is to give lyrics and
chords, usually with chord diagrams. And there is a standard Typst
package to do this sort of thing,
conchord, but I was
never able to get it to work showing ukulele chord diagrams rather
than six-string guitar, or in the specific layout I wanted.
So. Two steps needed.
One, generate SVG chord diagrams. I wrote this bit in Crystal because
I felt like it, it's fast, and it has JSON reading and XML writing
available in the core language. My approach doesn't try to run a music
theory engine the way conchord does; it just takes a list of chords.
(I borrowed the ukulele chord list from GNU Lilypond. Fortunately I'd
written a sanity checker; its versions of Adim and D♯dim, while
technically valid in terms of notes, have the same finger at different
frets on different strings…) I ignore much of the sophistication of
the Lilypond notation: if the same finger is on different strings,
I count it as a barre chord. Easy.
"XML writing", you say? Yup, because SVG is XML. A dedicated SVG
library package might have made things easier, but I'm not doing
hugely complicated graphics anyway.

So that got me a library of SVG chord diagrams, which obviously Typst
can use directly as graphics. Getting it to include a row of them at
the top of the sheet is easy enough; I wrote a chordchart
convenience method so that I can just tell it to put in "C G Am Fmaj7"
and it'll all show up in the right place.
The other fiddly thing, step two, is putting chord names over the
lyrics, and I'd had an unsuccessful bash at this before. I knew I
wanted to parse the "[chord] lyric" syntax that I'd used before (turns
out this is actually the "Chordify" syntax), but I had an inspiration:
I could go through a line chunk by chunk, identifying each as a chord
or a piece of lyric text based on the square brackets, and allocating
them accordingly to upper or lower cells in a grid. Since the chord
always precedes the word with which it's to be played, this is
relatively straightforward. That's done by feeding the line with
chords and words to the overchord function.

So that was working jolly well, and then I realised I had all the
tools I needed to add a pointless extra feature: a transposition
engine! So now there's an optional parameter transpose to any
chordchart or overchord calls, which will push the chords up or
down that number of semitones, and of course the Lilypond chord
listing already has a voicing for each of its chords in each key. I
don't actually need this, but it seemed as though someone might find
it useful.

I haven't uploaded this yet because large parts of it are still
subject to change, but if anyone wants the working version give me a
shout.