Open source · MIT · PDF/A and PDF/UA

Write the document.
Get it back on your own paper.

Mela Letterhead puts a real letterhead around a Markdown file — the band across the top of page one, the small running header on the pages after it, the footer with your legal and banking details on the last — and hands you a PDF.

You describe the paper once, in a single letterhead.yaml. Nothing about the layout is specific to any one trade, country or language.

$ pipx install mela-letterhead
  • Python 3.10+one dependency: PyYAML
  • 3 commandsinit · check · build
  • Any languageper-string translation
offer.md
---
title: Quotation — rainwater survey
type: Quotation
reference: AQ-2026-0184
lang: en
---

## 1. What is being quoted

Four collection points, surveyed on
site and reported on individually.

| Item | Hours | Rate |
|---|---:|---:|
| S1 — Site survey | 12 | € 70.00 |
mela-letterhead build
The first page of a quotation: a grey band across the top holding the logo on the left, a tagline under it, and four right-aligned fields opposite — Type, Reference, Date and Valid for — above a justified body with a ruled heading and a price table.
Nothing in the Markdown says where anything goes.

The twenty minutes after the work is done

The paper is not the song

A word processor keeps the room and the music in the same file. So every time you touch the writing, the walls move. Mela Letterhead separates them, and refuses to let them back into the same room.

01

The logo has drifted

Two millimetres to the left, and nobody knows why. Here the mark is placed by the letterhead, not by a drag of the mouse, so it lands in the same place on every document you will ever print.

02

The footer shattered on page three

Page one gets the full band, inner pages a running header, the last page the footer with your VAT number and IBAN. The layout knows which page it is on.

03

The table gave “Hours” 57% of the width

Column widths are recomputed from what is actually in them, your alignment markers kept, the header row emboldened for you.

04

The picture went to page three without its caption

A picture with a caption becomes a figure — a block of its own, centred, captioned in the letterhead's own type. Real typesetting, not HTML pretending to be a page.

A rhythm section does not improvise the stage. It plays the same groove at the same tempo every night and stays invisible, so the song can be heard. That is exactly what a letterhead is.

How it works

Markdown in, PDF out — through Pandoc and Typst

All resolution happens in Python. The Typst module receives a finished structure with one language already chosen and every length already a number of points — which is why it never needs editing to print somebody else's paper.

  1. 1

    Your document

    Plain Markdown, a few lines of front matter. No layout. Ever.

    offer.md
  2. 2

    Your paper

    Resolved for one language, every length converted to points.

    letterhead.yaml → document.json
  3. 3

    Typeset

    Pandoc converts, pictures are staged beside it, Typst compiles.

    pandoc → typst
  4. 4

    Your PDF

    On your paper, in the right language, identical every time.

    offer.pdf
letterhead.yaml — the paper, described once
brand:
  name: Acme Studio
  logo: assets/logo.svg
  tagline: Surveying · Exampleton

palette:
  accent: "#4a3f8a"

header:
  fields:
    items:
      - label: { en: Reference, it: Codice }
        key: reference

footer:
  columns:
    - title: Bank details
      rows:
        - label: "IBAN:"
          value: IT00 X000 0000 0000
          style: highlight

Two shapes, kept apart

Written is what you edit — 22mm, language maps, optional keys, shorthands. Forgiving, because a human maintains it.

Resolved is what the typesetter consumes — one language, points, every key present. Rigid, because a program reads it.

Anything that can go wrong is caught where the offending setting can still be named:

Error: letterhead.yaml: unknown setting 'palete'

  Did you mean 'palette'?

Three ways to start

Which of these three are you?

All three are finished letterheads. None is a lesser version of another.

Drop your own file over assets/logo.svg — PNG and JPEG work too — and that part is done. The mark prints in the band on page one, and smaller in the running header on every page after it.

brand:
  name: Acme Studio
  logo: assets/logo.svg
  tagline: Surveying · Exampleton

The band, the rule and the fields are all yours to colour.

The first page of a quotation with the logo in a pale grey band and four right-aligned header fields opposite it.

What you get

One configuration, any document

Header fields you define

Not four fixed slots — however many your paper actually has. Each one names a key in the document's front matter, so Date and Reference change per document while Department stays put. A field left empty prints a rule to fill in by hand.

A footer that holds real detail

Any number of columns, each a title and a list of rows. E-mail addresses become links on their own; an IBAN can be picked out in the highlight colour. The block centres itself, so you can add a row without touching an offset.

Pictures, where the text needs them

![caption](plate.svg) puts it on the page — PNG, JPEG, GIF, SVG and WebP — and {width=48%} beside it makes two stand side by side. With a caption it becomes a figure; without one it stays in its line.

Every language, including yours

Any string in the configuration can be a mapping from language tag to translation. A document picks its language in its front matter, and a missing translation falls back rather than leaving a hole in the page.

No logo? No problem

A brand with no logo file has its name set in type instead, at the size the logo would have had, with a face, a weight, a colour and a tagline of its own — a finished letterhead, not a fallback.

A PDF you can file

pdf.standard: a-3b and what comes out is PDF/A — every font embedded, nothing fetched from outside, the same document in ten years. Add ua-1 and it is tagged for a screen reader too. The conformance is enforced by the compiler, not claimed. What that means.

Or bring paper of your own

Point page.background at your designer's sheet, turn the bands off, and the tool stops building a letterhead and starts setting Markdown onto yours.

The page redraws itself as you write

A build is about two tenths of a second, so leave one running: build --watch rebuilds on every save of anything it read — the document, the configuration, a locale pack, the logo, every picture. A failed build does not end the session, which is the whole value of it: fix the file, save, and the error goes away.

One letterhead, printed more than one way

A profile is a named set of changes — --profile draft turns the accent red and stamps DRAFT behind the page, final asks for PDF/A instead. A single document can carry its own letterhead: block for the report that wants no footer band.

Your editor knows the settings

init writes a JSON schema beside the letterhead and points the first line at it. Setting names complete, the comments become tooltips, and pallette: is underlined before you save. Nothing is fetched: the schema travels inside the package, so it works offline and matches the version you installed.

Filing it

A PDF you can file

By default you get an ordinary PDF, which is what a letter is. Set pdf.standard when somebody at the other end has asked for something in particular — and what comes back is a document that passed, not a document that claims to have.

letterhead.yaml — two lines
pdf:
  standard: a-3b           # or a list: [a-3b, ua-1]

Enforced, not asserted

Typst checks conformance as it writes, so a document that would not hold up is refused instead of produced. Nothing here stamps a claim onto a file and hopes.

check reports the standard alongside everything else it settles, before a single page is compiled:

  ✓  every setting resolves
     pdf standard   a-3b, ua-1

Two standards, and what each one buys

PDF/A is the archival form: every font embedded, no colour that depends on a profile the reader does not have, nothing fetched from outside. It is what a public administration usually means by “send it as a PDF”, and what makes a document still open the same way in ten years. a-2b and a-3b are the two in common use — A‑3 differs only in allowing a file to be attached inside the PDF, which is how an electronic invoice carries its XML. a-4 is the newer PDF 2.0 edition.

PDF/UA‑1 is the accessible form: the page is tagged so a screen reader can follow it in reading order rather than guessing from where the ink sits, and every picture carries a description. The two can be asked for together — [a-3b, ua-1] is a document that is both filed and readable — with the one exception that a-4 and ua-1 cannot be combined, because A‑4 is PDF 2.0 and UA‑1 is not.

  • Every picture needs a description under ua-1 and the a levels. That is what goes in the square brackets — ![a plan of the roof, with the four points numbered](plate.svg). Say what the picture says, not that there is a picture.
  • PDF/A‑1 allows no transparency, because it is built on PDF 1.4, which had none. A logo exported with a soft shadow is refused. It is the one standard here that may need the artwork changed rather than the configuration; a-2b is the same idea without the restriction.
  • The footer band stops linking under ua-1. A band is drawn as a page artifact — furniture rather than content — and PDF/UA‑1 allows no link inside one, so the e‑mail address prints as the plain text it always was. Nothing moves on the page; the underline goes with the target.
  • The scaffold already builds to ua-1 with nothing edited, which is the test that the two lines above are real rather than nominal.
the conformance levels, in one table
PDF/A conformance levels and what each guarantees
a-1b a-2b a-3b
level b — basic
the document looks the same anywhere, for ever
a-2u a-3u
level u — unicode
…and its text can be extracted and searched
a-1a a-2a a-3a
level a — accessible
…and it is tagged and described, as PDF/UA asks
a-4 a-4f a-4ePDF/A‑4 has no levels: a-4f allows attached files, a-4e is the engineering flavour
ua-1tagging and descriptions, alongside any PDF version up to 1.7

Descriptions reach the page through Pandoc, and only from 3.9.0.1 onward — an older one drops them silently, so the document would be refused for missing what it plainly has. check says so rather than letting you find out that way. The full set of standards needs Typst 0.14; 0.12 knows only a-2b and 0.13 adds a-3b.

Languages

The same letterhead prints an Angebot and an offerta

This is the part worth reading twice, because it is what separates this from a template with your address hard-coded into it. Wherever a string is expected, a mapping from language tag to translation is accepted instead.

  • A document picks its own language. lang: de in the front matter, and the whole letterhead comes out in German — field labels, footer titles, even the logo if you gave a different one per market.
  • A missing translation falls back. Document language → bare subtag → project default → English. A label in the wrong language still prints; an empty one leaves a hole.
  • The tool's own strings live in locale packs. Drop locales/de.yaml beside your configuration. No code changes, nothing to submit upstream.
  • Typst is told the language too, so hyphenation and quotation marks follow it.
one string, three markets
label: Reference                # one language
label: { en: Reference, it: Codice, de: Kennzeichen }
locales/de.yaml — what the tool writes itself
running_header: "{title} · Seite {page} von {pages}"
untitled: "Unbenanntes Dokument"
enitdefrespt-BRnlde_AT…

Quick start

Three commands and there is a PDF on the desk

Mela Letterhead is a Python package, but it drives two external programs that do the real work. Install those first.

1 The toolchain

$ brew install pandoc typst

2 The tool

$ pipx install mela-letterhead

pipx puts it in an environment of its own and the mela-letterhead command on your PATH, which is what you want for something you run rather than import. pip install mela-letterhead works too, inside a virtual environment you have activated. Python 3.10 or newer; the only Python dependency is PyYAML.

Or from a clone, to work on it
$ git clone https://github.com/melasistema/mela-letterhead.git
$ cd mela-letterhead
$ python3 -m venv .venv
$ source .venv/bin/activate      # Windows: .venv\Scripts\activate
$ pip install -e ".[dev]"

-e installs it in place, so pulling a change is enough to get it.

3 Your first letterhead

$ mkdir ~/quotations && cd ~/quotations
$ mela-letterhead init .   # a working letterhead, here
$ mela-letterhead check    # is everything it needs present?
$ mela-letterhead build    # → example-letter.pdf

Then mela-letterhead build --watch and leave it running. It rebuilds on every save of anything it read, and a PDF viewer that reloads the file it is showing turns that into the page redrawing itself while you write.

Honest caveats

What it does not do

Written down rather than hidden, because you will meet them on the second day.

Can I use a PDF as the page background?

No — Typst places PNG, JPEG, GIF, SVG and WebP, and no PDF. A sheet drawn in a page-layout program has to be exported raster: PNG or JPEG, 300 dpi for print. The tool refuses the file by name before the compile rather than failing halfway through it.

Which PDF/A should I ask for?

a-3b unless you have been told otherwise. It is what almost everybody means by PDF/A, and A‑3 differs from A‑2 only in allowing a file to be attached inside the PDF — which costs nothing if you never attach one. Reach for a-2b if the other end is specific about it, and avoid a-1b unless it is named: PDF/A‑1 is built on PDF 1.4 and allows no transparency at all, so a logo with a soft shadow in it will be refused.

A standard is refused for missing alt text my pictures have.

Your Pandoc is older than 3.9.0.1, which is the release whose Typst writer learnt to carry a picture's description across. Before it the description is dropped in silence on the way, and the document is then refused for missing what it plainly has. The tool checks for this and says so up front rather than letting you meet it as a compiler error — but if you are reading this from an older version, that is the cause. Upgrade Pandoc.

What is veil actually doing?

Typst has no image opacity, so page.background.veil is a translucent white rectangle drawn over the picture. That works on white paper and nowhere else — which is exactly how it is documented.

My type looks wrong after changing fonts.

Almost always a substitution. Typst replaces a missing family without failing, so run mela-letterhead check and read the Fonts section for what it actually resolved. Typst itself embeds no sans face, so the sans stack names system fonts and must keep a plausible last resort.

A setting seems to be ignored.

It probably is, and the tool will say so: unknown keys are refused by name with the nearest valid one suggested. There is no silent fallback to a default you did not ask for.

My SVG drawing shifts between computers.

Text in an SVG is set in whatever font the compiling machine has. Convert the lettering to paths — every vector editor will do this — or leave it to the caption, where it is typeset with the rest of the document.

Do I have to keep the example document?

No. init writes a four-page quotation for a company that does not exist, which exercises every part of the layout. Read it once, delete it, write your own.

Describe your paper once.
Then never think about it again.

Free and MIT-licensed. One command installs it, one prints your own quotation before the kettle boils, and the twenty minutes at the end of the job are yours again.