---
title: "Content Contributions"
description: "How to contribute to the Hitchhiker's Guide — building, signing, releasing, and writing."
hide:
- toc
- navigation
schema:
"@context": https://schema.org
"@type": Organization
"@id": https://anonymousplanet.net/
name: Anonymous Planet
url: https://anonymousplanet.net/code/
logo: ../media/profile.png
sameAs:
- https://github.com/Anon-Planet
- https://opencollective.com/anonymousplanetorg
---
Open source. Every PR matters.
ContributeHelp us improve the Hitchhiker's Guide..
Contributions range from fixing a typo to writing entire new sections.
---
## Setup { #setup }
Install these before anything else.
=== "Linux / macOS"
```sh
# Python 3.11+
python3 --version
# poppler (pdftoppm) and qpdf
sudo apt install poppler-utils qpdf # Debian/Ubuntu
brew install poppler qpdf # macOS
# GPG
sudo apt install gnupg # Debian/Ubuntu
brew install gnupg # macOS
# Python dependencies
pip install "mkdocs-material[imaging]" pillow numpy
```
=== "Windows"
```powershell
# Python 3.11+ from https://python.org
# poppler: https://github.com/oschwartz10612/poppler-windows/releases
# Extract and add the bin\ folder to PATH
# qpdf: https://github.com/qpdf/qpdf/releases
# Extract and add the bin\ folder to PATH
# GPG: https://gpg4win.org
# Python dependencies
pip install "mkdocs-material[imaging]" pillow numpy
```
You also need **Google Chrome** or **Microsoft Edge** installed for the light-mode PDF build (headless Chromium).
---
## Repository Layout { #layout }
```txt
.github/
workflows/
01-build.yml # builds PDFs, uploads artifact
02-sign.yml # hashes + GPG signs, uploads signatures artifact
03-release.yml # publishes GitHub Release with all assets
04-changelog.yml # prepends a new entry to docs/changelog/index.md
publish.yml # deploys MkDocs site to GitHub Pages
docs/
guide/index.md # the guide (single Markdown file)
changelog/ # release notes
code/ # this page
export/ # PDF output (PDFs gitignored; .sha256, .b2sum, .asc tracked)
pgp/ # public signing keys
scripts/
build_guide_pdf.py # MkDocs + Chromium PDF builder
convert.py # pixel-based dark mode PDF converter
install_fonts.py # install fonts locally
update_changelog.py # auto-generates changelog entries from git log
setup_workflow.py # GitHub Secrets setup assistant
verify_pdf.py # signature verification helper
archived/
tag_release.py # ARCHIVED — GPG tag helper (not used in current flow)
```
---
## Building Locally { #build }
```sh
python scripts/build_guide_pdf.py --both
```
Builds the MkDocs site, renders it to `export/thgtoa.pdf` via headless Chromium, then produces `export/thgtoa-dark.pdf`.
| Flag | Effect |
|------|--------|
| `--both` | Light PDF then dark PDF |
| _(none)_ | Light PDF only |
| `--dark` | Dark PDF only (light PDF must already exist) |
Build only the dark PDF from an existing light:
```sh
python scripts/convert.py export/thgtoa.pdf export/thgtoa-dark.pdf
```
| Flag | Default | Description |
|------|---------|-------------|
| `--dpi` | `200` | Rasterization DPI |
| `--batch-size` | `50` | Pages per batch — reduce if OOM |
| `--bg` | `1f1f31` | Background colour (hex) |
| `--text` | `e0e0e0` | Body text colour (hex) |
| `--link` | `5e8bde` | Link colour (hex) |
Preview the site:
```sh
mkdocs serve
# Opens at http://127.0.0.1:8000
```
---
## Contributing Content { #contributing }
Use a Topic Branch
Never commit directly to main. Use a topic branch per change so PRs stay reviewable and independent.
Small PRs
Split large changes into multiple PRs — one for new content, one for fixes, one for style. Big PRs block merges and create review debt.
Conventional Commits
All commits must follow <type>(<scope>): <description> format. Enforced by the commitizen pre-commit hook.
Describe Your Changes
Never leave a PR description blank. Include what changed, why, and any context a reviewer needs. Link related issues.
### Commit Types
| Type | Changelog bucket |
|------|-----------------|
| `feat`, `feature`, `add` | Added |
| `fix`, `bugfix`, `revert`, `security` | Fixed |
| `perf`, `refactor`, `change`, `chore`, `ci`, `docs`, `style`, `test`, `build` | Changed |
Examples:
```sh
feat: add dark-mode PDF export
fix(scripts): handle locked PDF on Windows
docs: update developer workflow guide
chore(ci): pin Chrome version to 120
```
### Rules
- **Do** target PRs at the `main` branch
- **Do** write "WIP" or open a draft PR for unfinished work
- **Do** follow the [50/72 rule](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) for commit messages
- **Don't** make mass search/replace changes without context-checking every instance
- **Don't** abandon a PR mid-review — stay responsive
- **Don't** modify a PR directly while it's under active review — submit changes to the review branch instead
For an example of what _not_ to do, see [PR #51](https://github.com/Anon-Planet/thgtoa/pull/51).
!!! warning "Before you push"
- Make sure the working tree is clean (`git status`)
- Run `mkdocs build` locally if you changed `docs/` to catch broken links
- If you added footnotes, verify each has both a definition `[^N]:` and at least one inline citation `[^N]`
---
## Release Pipeline { #pipeline }
The pipeline is fully manual after the initial build — no step triggers the next automatically. This prevents version mismatches between what was built, what was signed, and what gets released.
```txt
push to main (or manual trigger)
│
▼
01-build.yml
Builds thgtoa.pdf + thgtoa-dark.pdf.
Uploads artifact: pdfs — note the run ID.
│
│ manually trigger 02-sign.yml with the build run ID
▼
02-sign.yml
Hashes (SHA-256 + BLAKE2b) and GPG-signs all files.
Commits export/ back to main.
Uploads: signatures, pdfs-signed — note the run ID.
│
│ manually trigger 03-release.yml with the sign run ID
▼
03-release.yml
Runs VirusTotal. Creates GitHub Release tagged release-YYYYMMDD-.
│
│ manually trigger 04-changelog.yml with the version string
▼
04-changelog.yml
Prepends a new ## [vX.Y.Z] entry to docs/changelog/index.md and commits.
```
### Release Tags
Tags use the format `release-YYYYMMDD-`, e.g. `release-20260527-abc1234`. No version decision is needed at release time — the tag is always unique and traceable to the exact commit.
The version string (e.g. `v1.2.4`) is a separate, human-assigned label that lives only in the changelog.
### Triggering Each Step
**Build:** Push to `main` or go to **Actions → Build PDFs → Run workflow**. Note the run ID.
**Sign:** **Actions → Sign PDFs → Run workflow**, enter the build run ID. Note the run ID.
**Release:** **Actions → Release → Run workflow**, enter the sign run ID.
**Changelog:** **Actions → Update Changelog → Run workflow**, enter the version string. Use `dry_run: true` to preview.
---
## Verifying a Release { #verify }
```sh
# Import the release signing key
gpg --import pgp/anonymousplanet.asc
# Verify the PDFs
gpg --verify thgtoa.pdf.asc thgtoa.pdf
gpg --verify thgtoa-dark.pdf.asc thgtoa-dark.pdf
# Verify the hash files
gpg --verify sha256sums.txt.asc sha256sums.txt
gpg --verify b2sums.txt.asc b2sums.txt
# Check the PDF hashes match
sha256sum -c sha256sums.txt
b2sum -c b2sums.txt
```
Expected output:
```txt
gpg: Signature made Sun 31 May 2026 03:23:26 AM EDT
gpg: using EDDSA key C3023DBEA3FB38C438BA1EEDCEC60AEDE8B992A2
gpg: Good signature from "Anonymous Planet Release Signing Key" [ultimate]
Primary key fingerprint: C302 3DBE A3FB 38C4 38BA 1EED CEC6 0AED E8B9 92A2
```
You can safely ignore GitHub/Codeberg warnings like "The email in this signature doesn't match the committer email."
---
## Troubleshooting { #troubleshooting }
**`cairosvg` missing during MkDocs build**
`pip install "mkdocs-material[imaging]"` — required by the `social` plugin.
**`KeyError: 'JPEG'` in convert.py**
`sudo apt install libjpeg-dev && pip install --force-reinstall pillow`
**`qpdf: can't find PDF header`**
qpdf only accepts PDF inputs — ensure you are on the current version of `convert.py`.
**GPG signing fails — `No secret key`**
Re-export: `gpg --armor --export-secret-keys ` and re-paste the full block including headers into the `GPG_PRIVATE_KEY` secret.
**GPG signing fails — `Bad passphrase`**
The `GPG_PASSPHRASE` secret has a trailing space or newline. Re-paste without surrounding whitespace.
**`03-release.yml` fails on VirusTotal**
`VT_API_KEY` is missing, invalid, or over the 500 req/day free-tier limit. Re-run after a few minutes.
**`02-sign.yml` fails downloading PDF artifact**
Wrong `build_run_id`, or the artifact expired (90-day retention). Trigger a new build.
**Changelog already contains version X**
`update_changelog.py` errors if the version is already present. Choose the next version string.
**Footnote warnings — `link '#fnref:N' has no anchor`**
A definition `[^N]:` exists without a matching inline citation. Add the citation or remove the orphaned definition.