In tough job markets, submitting 100 résumés before getting hired is not uncommon. That’s a lot of work to land a cold interview. Let’s create a configurable cover letter to catch the attention of prospective employers; at least those who sift through applications that pass the AI filter. We’ll start by defining high-level contents.
If a cover letter you create from this post improves your employment situation, please donate to support further development of the software.
Follow along step-by-step or jump to the end to download the theme and supporting files.
Refer to the user manual for instructions on installing KeenWrite and ConTeXt.
The document structure will contain all the elements we want to include in the cover letter, such as the company logo, your name, contact details, letter, references, and showcase.
Begin the document with the company logo, applicant name, and role tucked inside of a header section:
::: header
::: logo

:::
[{{employee.name}}]{.applicant}
[{{employee.role}}]{.role}
:::
Avoid using a .title class because that class is reserved for the document
title and will create an extra page.
Next is nested contact information:
::: contact
::: address
{{employee.address.line.1}}
{{employee.address.line.2}}
{{employee.address.line.3}}
:::
[{{employee.contact.phone}}]{.phone}
[{{employee.contact.email}}]{.email}
[{{employee.portfolio.url}}]{.portfolio}
:::
We’ll style the phone, email, and link with icons having an accent colour that matches the company accent colour. Before that, though, there’s a walk ahead.
The cover letter content has the following structure:
::: letter
::: opening
To whom it may concern,
:::
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
::: closing
Sincerely,
::: signature

:::
{{employee.name}}
:::
:::
Isolating the closing section allows the presentation layer to tweak the paragraph spacing around the signature while informing the maintainer where the letter ends. The opening section isn’t used by the theme we’ll craft; we’ll include it for symmetry plus the possibility of future adjustments by the presentation layer.
Optionally, we can add references:
::: references
> "Donec rhoncus mollis ante at posuere. Mauris porta, lectus id tempus
> hendrerit, ligula diam ultrices dui, vel tempus odio purus sagittis velit.
>
> "Cras sodales justo sem, at hendrerit ipsum venenatis et. Nam aliquet porta
> nibh. Duis nec velit faucibus, facilisis nisi non, sodales nibh."
>
> ~ Name, role, date
> "Interdum et malesuada fames ac ante ipsum primis in faucibus.
> Pellentesque congue sollicitudin orci vel gravida.
>
> "Nulla rhoncus porttitor sapien, ac vulputate magna consectetur eu.
> Sed scelerisque elementum eros, ut fermentum augue ornare non."
>
> ~ Name, role, date
:::
Note the blank lines after and before the triple colon syntax (:::), which
help readability.
Lastly, we highlight a related work sample:
::: showcase
Insert showcase here.
:::
Let’s verify that we can produce a PDF file using the following steps:
keenwrite.bin \
-i cover-letter.md \
-o cover-letter.pdf \
--set=employee.name="Your Name" \
--set=employee.contact.phone="1-888-555-1212" \
--set=employee.contact.email="you@hostname.ca" \
--set=employee.portfolio.url="https://localhost/showcase" \
--theme-dir="$HOME/path/to/keenwrite/themes/tarmes"
cover-letter.pdf.Verify that variable names get replaced with values:

Let’s fix that hot mess by creating a new theme.
The new theme, Aspiros, will style the cover letter.
We need separate directories for the new theme and cover letter. Although the instructions are for a Linux operating system, macOS and Windows will have similar steps.
cd $HOME/path/to/keenwrite/themes
mkdir aspiros
cd aspiros
cp ../tarmes/xhtml.tex .
echo "name=Aspiros" > theme.properties
mkdir -p $HOME/work/jobs/template
cover-letter.md into $HOME/work/jobs/template.$HOME/work/jobs/template.Make a script that rebuilds the document. Create a file named build.sh in
the template directory and make it executable (chmod +x build.sh):
#!/usr/bin/env bash
keenwrite.bin \
-i cover-letter.md \
-o cover-letter.pdf \
--set=employee.name="Your Name" \
--set=employee.contact.phone="1-888-555-1212" \
--set=employee.contact.email="you@hostname.ca" \
--set=employee.portfolio.url="https://localhost/showcase" \
--theme-dir="$HOME/path/to/keenwrite/themes/aspiros"
Note the change to the theme directory option, pointing to the new theme.
Create a file named main.tex in the aspiros directory having the following
contents:
\input xhtml
\input figures
The typesetting system looks for main.tex as the entry point for instructions
that define the appearance of the output document.
Create a file named figures.tex in the aspiros directory. Add
instructions to configure images:
\setupexternalfigures[
order={svg,pdf,png},
maxwidth=\makeupwidth,
]
% Remove captions from all images.
\setupcaption[figure][location=none]
Run the build.sh script to verify that the document generates, but logs
a couple of errors indicating that there are missing images.
template/logo.template/signature.We’ll add these shortly.
By default, documents contain a title page and a table of contents. To remove both, add a configuration file to adjust the document.
main.tex:
\input xhtml
\input document
\input figures
document.tex and hijack the instructions in ../xhtml/xml-document.tex:
\define\TextFrontMatter{}
Rebuild the document.
Our document shall have three colours, no more, no less. (Five is right out.)
Aside, colours need to have a sufficiently high contrast ratio to be legible.
Create a new file named colours.tex and define it as follows, where
x= assigns a hex code colour:
\startsetups document:start
\definecolor[TextColourForeground][x=\documentvariable{foreground}]
\definecolor[TextColourBackground][x=\documentvariable{background}]
\definecolor[TextColourAccent][x=\documentvariable{accent}]
\setupcolors[
textcolor=TextColourForeground,
]
\stopsetups
\setupbackgrounds[page][
background=color,
backgroundcolor=TextColourBackground,
]
\definestartstop[applicant][
color=TextColourAccent,
]
Note that:
\startsetups document:start ensures that the document variables
have been parsed from the XHTML metadata before typesetting begins; and\documentvariable retrieves command-line metadata to control
the output document appearance.Update the build script to pass in hex colours accordingly, with defaults having good colour contrast:
#!/usr/bin/env bash
readonly COLOUR_FG="${1:-EEEEEA}"
readonly COLOUR_BG="${2:-22222A}"
readonly COLOUR_AC="${3:-55BBFF}"
keenwrite.bin \
-i cover-letter.md \
-o cover-letter.pdf \
--set=employee.name="Your Name" \
--set=employee.contact.phone="1-888-555-1212" \
--set=employee.contact.email="you@hostname.ca" \
--set=employee.portfolio.url="https://localhost/showcase" \
--metadata=foreground="${COLOUR_FG}" \
--metadata=background="${COLOUR_BG}" \
--metadata=accent="${COLOUR_AC}" \
--theme-dir="$HOME/dev/java/keenwrite/themes/aspiros"
Also update main.tex to include the colour definitions:
\input xhtml
\input document
\input colours
\input figures
Typically, we list the files in dependency order. Since colours are referenced
in many setups, its corresponding \input line is inserted near the top of
main.tex.
Cover letters are typically short, making page numbers crufty. Let’s eliminate
them. First, create a new file named page.tex and reference it in main.tex:
\input xhtml
\input document
\input colours
\input page
\input figures
Next, update page.tex to include:
\setuppagenumbering[location=none]
That’s it (for now)! Rebuild the document to see:

The font is flimsy, so we’ll fix that next.
For the main body font, we’ll use Libre Baskerville at 10 points, which improves on EB Garamond’s legibility. The font pairs well with Bench Nine, a condensed font that leaves room for a logo and a name at the top of the page.
/usr/local/share/fonts/ttf.OSFONTDIR environment variable includes
/usr/local/share/fonts// (the trailing double slash indicates subdirectories).rm -rf /tmp/luatex-cache
rm -rf $HOME/luametatex-cache
fc-cache -f -v
mtxrun --script fonts --reload
Create fonts.tex using the following content (and list it in main.tex):
\definefontfeature[TextFontFeature][default][
kern=yes,
liga=yes,
tlig=yes,
trep=yes,
tquo=no,
mode=node,
protrusion=quality,
expansion=quality,
]
\definefontfamily[TextFont] [rm] [Libre Baskerville] [features=TextFontFeature]
\definefontfamily[TextFont] [ss] [Bench Nine] [features=TextFontFeature]
\definefont[TextApplicant][BenchNineBold at 38pt]
\setupstartstop[applicant][
style={\TextApplicant\WORD},
]
\usetypescript[TextFont]
\setupbodyfont[TextFont, rm, 10pt]
The font feature definitions control the behaviours described in the following table:
| Key | Value | Description |
|---|---|---|
kern | yes | Enables kerning, adjusts space between specific character pairs (e.g., WA). |
liga | yes | Enables standard ligatures (e.g., fi combined into a single glyph). |
tlig | yes | Enables traditional ligatures. |
trep | yes | Enables text replacements for specific typographic substitutions. |
tquo | no | Disables typographic quotation marks (straight quotes to curved quotes). |
mode | node | Specifies node processing mode for advanced control over glyphs and features. |
protrusion | quality | Enables protrusion, to allow hanging punctuation inside margins. |
expansion | quality | Enables font expansion to aid line justification and reduce gaps. |
Font features disable typographic quotation marks (tquo) because KeenWrite
will curl them automatically.
We also snuck in some styling (\TextApplicant\WORD) for the applicant name so
that any text marked with the {.applicant} class will appear in bold,
uppercase, and with a larger font size.
With the font in place, let’s update the build script with a name and contact information:
keenwrite.bin \
-i cover-letter.md \
-o cover-letter.pdf \
--set=employer.company.name="Henry Baskerville" \
--set=employee.name="Sherlock Holmes" \
--set=employee.role="Private Investigator" \
--set=employee.contact.phone="020 7224 3688" \
--set=employee.contact.email="sherlock@holmes.co.uk" \
--set=employee.portfolio.url="https://www.sherlock-holmes.co.uk" \
--set=employee.address.line.1="221B Baker Street" \
--set=employee.address.line.2="Marlyebone, London" \
--set=employee.address.line.3="NW1 6XE" \
--metadata="foreground=${COLOUR_FG}" \
--metadata="background=${COLOUR_BG}" \
--metadata="accent=${COLOUR_AC}" \
--theme-dir="$HOME/dev/java/keenwrite/themes/aspiros"
Download the following images into the template folder:
Eventually, we’ll want to swap the logo with company-specific branding and insert your own signature. Until then, after rebuilding, the document resembles:

This is starting to look like a cover letter.
Now let’s start laying out the header:
In ConTeXt, we can make all floats (such as tables and figures) be
left-justified, or we can be more surgical and shift specific figures. To
perform the latter, append the following snippet to figures.tex and notice
how the floats:left definition is referenced within the start-stop
environment:
\startsetups floats:left
\setupfloat[location=left]
\stopsetups
\definestartstop[logo][
setups=floats:left,
before={\starthanging},
after={\stophanging\vskip-1.575em},
]
\defineexternalfigure[logo.svg][
height=38pt,
]
The \vskip is a cheat to top-align the applicant name with the logo.
Next, update fonts.tex to change the font for the role, insert a blank
vertical whitespace to offset it from the applicant name, and use the
condensed font for the address:
\definefont[TextRole][BenchNineBold at 18pt]
\definestartstop[role][
before={\blank[medium]},
style=\TextRole
]
\definestartstop[contact][
style=\ss\tfb
]
Setting style=\ss\tfb is a short way of typesetting the contact information
using the configured sans-serif (\ss) font in a smaller font size (\tfb).
Create a new file, header.tex having the following contents (and append
an \input entry to main.tex):
\setupblackrules[
height=1pt,
color=TextColourAccent,
width=\textwidth
]
\definestartstop[header][
before={\startalignment[flushright]},
after={\stopalignment \blank[small] \blackrule \blank[small]},
]
Using flushright will right-justify the entire header section, except for the
logo, which was explicitly floated to the left.
Take a look:

You may have noticed a large amount of vertical whitespace at the top and
bottom of the page. This is because the areas apportioned to the header and
footer are still present, despite being empty. Another way to remove the page
number is to eliminate the page header altogether, along with the page footer
to maintain overall balance. Update page.tex to set the state to none for
the header and footer, like so:
\setupheader[state=none]
\setupfooter[state=none]
\setuppagenumbering[alternative=singlesided]
Setting the page numbering to single-sided will prevent alternating margin widths on every other page, which is more useful for printed materials that have a physical gutter than on-screen PDF documents.
More content will now fit on the first page without triggering a page break.
To direct the recruiters attention, we want to use a two-column setup to
split the sidebar from the letter body. We’ll make the changes in a new
file called layout.tex and update main.tex as before. Step-wise,
first insert the following into the layout file:
\definestartstop[letter]
\definemixedcolumns[Columns][
n=2,
balance=yes,
distance=.05\makeupwidth,
maxwidth=.715\makeupwidth,
separator=rule,
rulecolor=TextColourAccent,
]
This sets up columns where:
n= assigns the number of columns;distance= controls spacing between the vertical rule and the
right-hand text;maxwidth= dictates the total amount of space allocated for
the columns;separator= enables a vertical bar; andrulecolor= indicates what colour to use for the vertical bar.Next, we need to control each column width independently. To do this, we
need to put the content for each section—namely, the contact/address
information and the letter/body—inside of their own “frames.” There are
some nuances we’ll explore after appending the following into layout.tex:
\definestartstop[contact][
before={%
\startmixedcolumns[Columns]\bgroup
\startframedtext[frame=off, width=.35\makeupwidth, offset=\zeropoint]
},
after={
\stopframedtext
},
]
\setupstartstop[letter][
before={%
\startframedtext[frame=off, width=.6\makeupwidth, offset=\zeropoint]
},
after={
\stopframedtext
\egroup\stopmixedcolumns
},
]
In TeX, braces ({}) group commands together. However, when a command starts
and stops across boundaries, sometimes we have to use the \bgroup and
\egroup brace synonyms to indicate the beginning and ending of grouped
commands. We need to do this here because the mixed columns begin with the
contact block and end with the letter block, splitting across block
boundaries. Were we to use literal braces, it would confuse the compiler
because after={\stopframedtext}\stopmixedcolumns} is invalid syntax: the
after={ section is closed prematurely by the first brace encountered.
In ConTeXt, a framed command tells the typesetter to treat an entire swath of commands as indivisible. In this case, we need the framed text command because the cover letter body has multiple paragraphs. Moreover, it also allows specifying the widths for each column. We’ll come back around to this later when controlling the column width via command-line arguments.
Lastly, \makeupwidth is the width of the content between the left and right
margins. Multiplying the value with a fraction allows computing relatively
sized columns. Ideally, we could tell ConTeXt to fit the contact information
snugly inside of its column and allow the content column to expand, but that
seems impossible time of writing.
The result:

A few more issues remain: references, icons, and sample work.
Trivially add a page break before the references section by inserting the
following into references.tex:
\definestartstop[references][
before={\page},
]
You know the drill: be sure to append \input references to main.tex.
While here, stylize the blockquotes to add a little pizzazz:
\defineframedtext[blockquote][
frame=off,
leftframe=on,
framecolor=TextColourAccent,
rulethickness=.5em,
width=\makeupwidth,
offset=\zeropoint,
loffset=1.5em,
roffset=1.5em,
align={normal, verytolerant, stretch, fullhz},
]
Setting the alignment helps ensure that typesetting avoids overflowing the line. That is, it permits the typesetting system to wrap long lines by adjusting whitespace.
Update the cover letter to use Markdown’s hyperlink syntax, prefixing the links
with tel:, mailto: and https: to identify the protocols. We’ll map those
protocols to icons shortly.
[{{employee.contact.phone}}](tel:{{employee.contact.phone}})
[{{employee.contact.email}}](mailto:{{employee.contact.email}})
[{{employee.portfolio.url}}](https://{{employee.portfolio.url}})
Enable hyperlinks by changing the interaction state value to start inside of
hyperlinks.tex and creating a definition for the \href macro:
\setupinteraction[state=start]
\define[2]\href{%
\goto{\color[TextColourForeground]{#1}}[url(#2)]%
}
We could also use TextColourAccent, but the icons will have a spot of
colour, making it slightly redundant.
Remember to update main.tex with \input hyperlinks.
Edit build.sh and update both the contact information and website:
--set=employee.contact.phone="020-7224-3688" \
--set=employee.contact.email="sherlock@holmes.co.uk" \
--set=employee.portfolio.url="sherlock-holmes.co.uk" \
Re-run the build script to make sure the output is correct.
We want to use icons that represent a phone, email, and website. This will entail:
We’ll take a scalable vector graphic version of each icon and covert them into MetaPost. Once the icons are in MetaPost form, we can change the colour dynamically. Another approach would be to make a font.
Many open source icon packs are available. For this example, we’ll pick FontAwesome’s icons.
After selecting the three desired icons, load them into your faviourite SVG editor (e.g., Inkscape) and make sure they are all the exact same width and height (e.g., 64 x 48).
Be sure to compress the SVG icons, which will normalize the paths, clean up the colours, and make the data easier to use.
Start with the following skeleton, call the file icon.tex:
\enabletrackers[metapost.svg.result]
\startbuffer[icon]
-- SVG
\stopbuffer
\starttext
\includesvgbuffer[icon]
\stoptext
Replace the -- SVG comment with the complete SVG file contents, such as
the following (truncated for brevity):
\startbuffer[icon]
<svg xmlns="http://www.w3.org/2000/svg">
<path ... />
<path ... fill="#eee" />
</svg>
\stopbuffer
Run context icon.tex and scan the logs for MetaPost code:
fill ...
withcolor svgcolor(0.216,0.424,0.863)
;
fill ...
withcolor svggray(0.933)
;
The key parts are the calls to svgcolor(). Paste the entire MetaPost (MP)
code into the icon.tex file enclosed by \startMPcode and \stopMPcode:
\enabletrackers[metapost.svg.result]
\startbuffer[icon]
<svg xmlns="http://www.w3.org/2000/svg">...</svg>
\stopbuffer
\starttext
\includesvgbuffer[icon]
\startMPcode
fill ...
withcolor svgcolor(0.216,0.424,0.863)
;
fill ...
withcolor svggray(0.933)
;
\stopMPcode
\stoptext
Re-run context icon.tex and verify that the icon appears in the PDF file
twice: once for the SVG code and once for the equivalent MetaPost code.
Create another file called icons.tex in the template directory and
reference it in main.tex. In this file will be the MetaPost icon definitions.
The general form follows:
\startuseMPgraphic{envelope}
fill ...
xyscaled \overlaywidth
withcolor \MPcolor{TextColourAccent}
;
fill ...
xyscaled \overlaywidth
withcolor \MPcolor{TextColourForeground}
;
\stopuseMPgraphic
\startuseMPgraphic{phone}
...
\stopuseMPgraphic
\startuseMPgraphic{website}
...
\stopuseMPgraphic
Replace each call to svgcolor with withcolor \MPcolor{...} according to
how you want the icon’s base and accent colours to appear. The usage of
xyscaled will adjust the icon’s width, maintaining its aspect ratio (because
no height is given). All that remains is configuring its overlay integration.
Let’s revisit the \href macro from hyperlinks.tex. Conceptually, we want
to map the protocols to different icons. One way to accomplish this is to
use a frame with a background drawing. Update the file contents to:
\defineoverlay[envelope][\useMPgraphic{envelope}]
\defineoverlay[phone][\useMPgraphic{phone}]
\defineoverlay[website][\useMPgraphic{website}]
\define[2]\href{%
\doifinstringelse{mailto:}{#2}{\def\Icon{envelope}}{}
\doifinstringelse{tel:}{#2}{\def\Icon{phone}}{}
\doifinstringelse{https:}{#2}{\def\Icon{website}}{}
\goto{\color[TextColourForeground]{%
\hskip.5em%
\inframed[frame=off, background=\Icon, width=.015em]{}%
\hskip.75em #1%
}}[url(#2)]%
}
The first lines map the overlay to the MetaPost graphics’ drawing instructions.
The \doifinstringelse lines, read as do if in string else, compare the
substring matches of the protocol against the source document’s anchor link.
While this means that mailto:https:me@domain.com could throw a wrench into
the works, we control the inputs and won’t be lobbing such treachery. Knowing
that each link type supports a different protocol, we’re free to set up the
hyperlink texts with suitable icons.
Temporarily disabling pagination for the references, our nearly complete cover letter resembles:

Having cleanly separated the cover letter content from its presentation, we can
now match a company’s branding instantly. Running ./build.sh edefed 1c381f e36818 with the target company’s logo produces:

The last file to create is showcase.tex. Sample work will be set on a page by
itself, shrunk to fit the image (or PDF page):
\definestartstop[showcase][
before={\startTEXpage[strut=no]\startalignment[middle]},
after={\stopalignment\stopTEXpage},
]
Download the showcase.pdf file into the template directory
and update the showcase section of the cover letter to provide a name:
::: showcase
[Hounds of Baskerville]{.role}

:::
Rebuild the document.
A few minor issues remain:
role class.These are left as exercises for the reader.
The following archives contain the source files created in this post and are released into the public domain: