Markdown to a sellable product PDF and a converting landing page

3 min read Gumroadfpdf2Landing pageDigital products

A config-driven Python pipeline that turns chapter markdown files into a styled A4 PDF and generates a self-contained Gumroad landing page, with no Windows font dependency and no API keys.

The symptom

I wanted to publish a digital product on Gumroad: a properly typeset PDF ebook and a landing page that sells it, without hand-coding HTML every time. The naive approach is to open a design tool, export a PDF, and write the landing page once. That breaks the moment you need to change pricing, features, or copy, because you are back in the design tool re-exporting and hand-editing HTML again.

What I needed was a config-driven pipeline where changing one JSON file rebuilds both the PDF and the landing page consistently. Two bugs blocked the first attempt. The PDF renderer crashed on Windows because it tried to load Arial from the system font registry, and a stray angle bracket in a feature title silently broke the landing page HTML.

What was happening

The first version passed "Arial" as the font name to FPDF.set_font(), and fpdf2 tries to locate that font through the OS font registry. On a headless Windows environment (a CI runner, a fresh machine, a buyer’s box with regional fonts) Arial may not exist at the path fpdf2 expects, or the registry lookup returns nothing, and you get a crash or a garbled PDF with a substituted typeface. The fix is to bundle a redistributable font and register it by file path before using it:

from fpdf import FPDF
from pathlib import Path

_FONTS = Path(__file__).parent / "fonts"

pdf = FPDF(unit="mm", format="A4")
pdf.add_font("Body", style="",  fname=str(_FONTS / "DejaVuSans.ttf"))
pdf.add_font("Body", style="B", fname=str(_FONTS / "DejaVuSans-Bold.ttf"))
pdf.add_font("Body", style="I", fname=str(_FONTS / "DejaVuSans-Oblique.ttf"))
pdf.set_font("Body", size=11)

The kit ships DejaVu Sans (Regular, Bold, Oblique) under the DejaVu Fonts License, which allows redistribution, so the TTF files travel with the product and need no system font installation.

The second bug was in the landing page generator, which substituted config values straight into an HTML template with string replacement. A feature description containing <, >, or &, which is common in technical copy (“supports

tags”, “price & availability”), produced invalid HTML. Some browsers recover silently; others render broken structure or drop content. The fix is to pass every config value through html.escape() before substitution:

import html as _html

title    = _html.escape(cfg.get("title", ""))
title_grad = _html.escape(cfg.get("title_grad", ""))
price    = _html.escape(cfg.get("price", ""))
kicker   = _html.escape(cfg.get("kicker", ""))
sub      = _html.escape(cfg.get("sub", ""))

Feature cards call _html.escape() on the title and description individually as well. The only values that bypass escaping are the ones you control completely at build time, such as CSS class strings and known-safe HTML fragments you own.

The fix

The pipeline now works like this:

book.json
  -> build_pdf.py
       load chapter .md files in order
       DejaVu fonts (bundled, path-registered)
       cover page (image or generated fallback)
       auto table of contents
       styled chapter layout with accent colour from config
       => output PDF (A4, any OS)

landing.json
  -> build_landing.py
       html.escape() every config value
       data-gumroad-action="buy" + data-gumroad-field="price" on buy buttons
       Tailwind CDN dark/light theme (CSS-only reveal, no JS required for layout)
       optional hero cover image (base64-encoded inline, no external host needed)
       => self-contained HTML file, ready to drop behind any domain

The Gumroad embed attributes are worth knowing about. data-gumroad-action="buy" and data-gumroad-field="price" on the anchor tag activate Gumroad’s overlay checkout when gumroad.js is included. The template ships with placeholder href="#" buy buttons; swap in your Gumroad product URL and the overlay handles the rest without a redirect.

The whole build runs offline, with no API keys and no internet connection at build time. The only external call happens at purchase time, on Gumroad’s side.

The lesson

Never reference a system font by name in a portable Python PDF builder. Register fonts by absolute file path from a bundled directory, because buyers run on machines you have not seen and the system registry is not yours to depend on.

HTML-escape every external string before it touches a template, including your own config file. Config values are not trusted HTML, so treat them the way you would treat user input.


The kit is available now with launch code LAUNCH for EUR 12 (regular price EUR 19). The code is use-capped and will be removed once the intro period closes.

Related fixes

Discussion

Powered by GitHub. Sign in to leave a comment.