Dave Jarvis — October 15, 2025
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.
- Reason. State why you’re applying, relevant experience, and what value you bring.
- Company. Use the company’s logo and colours to make your application shine.
- Contact. Include your name, title, mailing address, phone number, and email address.
- Portfolio. Link to a portfolio that shows how your interests align with the company’s mission.
- References. Add praise, references, recommendations, and accolades.
- Showcase. Show an outstanding piece of work.
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.
Logo
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.
Body
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.
References
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.
Sample
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:
- Download cover-letter.md.
- Run (change the theme directory path according to your system): 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="[email protected]" \ --set=employee.portfolio.url="https://localhost/showcase" \ --theme-dir="$HOME/path/to/keenwrite/themes/tarmes"
- Open 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.
Directories
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.
- Open a terminal.
- Run the following commands: 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
- Move cover-letter.md into $HOME/work/jobs/template.
- Change directory to $HOME/work/jobs/template.
Build script
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="[email protected]" \ --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.
Main
Create a file named main.tex in the aspiros directory having the following contents:
\input xhtml \input figuresThe typesetting system looks for main.tex as the entry point for instructions that define the appearance of the output document.
Figures
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.
- Missing image template/logo.
- Missing image template/signature.
We’ll add these shortly.
Document
By default, documents contain a title page and a table of contents. To remove both, add a configuration file to adjust the document.
- Update main.tex: \input xhtml \input document \input figures
- Create document.tex and hijack the instructions in ../xhtml/xml-document.tex: \define\TextFrontMatter{}
Rebuild the document.
Colours
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:
- using \startsetups document:start ensures that the document variables have been parsed from the XHTML metadata before typesetting begins; and
- using \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="[email protected]" \ --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 figuresTypically, 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 figuresNext, 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.
Fonts
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.
- Download Libre Baskerville.
- Download Bench Nine.
- Extract the font files into /usr/local/share/fonts/ttf.
- Ensure the OSFONTDIR environment variable includes /usr/local/share/fonts// (the trailing double slash indicates subdirectories).
- Run the following commands to reload the fonts: 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:
| 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.
Applicant
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="[email protected]" \ --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:
- set the logo size;
- move the logo to the upper-left corner;
- shift the applicant name to the upper-right corner;
- restyle the role name and contact details; and
- separate the header using a coloured horizontal rule.
Revise logo
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).
Insert horizontal rule
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.
Columns
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; and
- rulecolor= 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.
References
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.
Hyperlinks
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="[email protected]" \ --set=employee.portfolio.url="sherlock-holmes.co.uk" \Re-run the build script to make sure the output is correct.
Icons
We want to use icons that represent a phone, email, and website. This will entail:
- finding free icons from the same icon pack;
- resizing and scaling the icons to the same dimensions;
- compressing the SVG icons;
- converting icons from SVG to MetaPost;
- updating the MetaPost code to accept scale and colour; and
- integrating the icon with the document’s hyperlinks.
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.
Pack
Many open source icon packs are available. For this example, we’ll pick FontAwesome’s icons.
Rescale
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).
Compress
Be sure to compress the SVG icons, which will normalize the paths, clean up the colours, and make the data easier to use.
Convert
Start with the following skeleton, call the file icon.tex:
\enabletrackers[metapost.svg.result] \startbuffer[icon] -- SVG \stopbuffer \starttext \includesvgbuffer[icon] \stoptextReplace 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> \stopbufferRun 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 \stoptextRe-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.
Update
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} ... \stopuseMPgraphicReplace each call to svgcolor with withcolour \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.
Integrate
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:[email protected] 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:

Showcase
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:
- Add vertical whitespace between the address and contact information.
- Shrink the left-hand column, expand the right-hand column.
- Pass the column widths via KeenWrite.
- Right-align the last line of the references.
- Add a heading to the references page.
- Add a new class for the showcase heading to avoid usurping the 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:
.png)


