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 drives sales without me hand-coding HTML every time. The naive approach was to open a design tool, export a PDF, and write the landing page once. That breaks the moment you need to iterate on pricing, features, or copy: you reopen the design tool, re-export, and hand-edit HTML again.

The real requirement 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 broke the landing page HTML silently.

What was actually happening

1. fpdf2 resolved fonts from the system, which fails on headless Windows. The first version passed "Arial" as the font name to FPDF.set_font(). fpdf2 tries to locate that font via 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. The result is a crash or a garbled PDF with a substituted typeface.

The fix is to bundle a redistributable font and register it explicitly 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 its own freely redistributable license (the DejaVu Fonts License), so these TTF files travel with the product and require no system font installation.

2. Raw config values in the landing page template broke HTML. The landing page generator substituted config values directly into an HTML template using string replacement. A feature description containing <, >, or & (common in technical copy: “supports

tags”, “price & availability”) produced invalid HTML. Some browsers silently recover; 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 also call _html.escape() on both the title and description individually. The only values that bypass escaping are those you control completely at build time (CSS class strings, 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 noting: data-gumroad-action="buy" and data-gumroad-field="price" on the anchor tag activate Gumroad’s overlay checkout when gumroad.js is included. The landing page template ships with placeholder href="#" buy buttons; you swap in your Gumroad product URL and the overlay handles the rest without a redirect.

The whole build is offline. No API keys, 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. Buyers run on machines you have not seen; 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. Treat them the same 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.

Discussion

Powered by GitHub. Sign in to leave a comment.