Making bk99.de printable: what @media print changes on paper
Summary
The occasion was Brennen Puth’s post “Websites Should Be Printable”, discussed on Hacker News on 26 October 2026. A test print showed that until then bk99.de printed its menu, social bar and table of contents, that a single post took up to 14 pages, and that dark mode put light grey text on white paper. This post shows the print rules in place since 26 October, how they work with light and dark mode, and how to check the result automatically.
The occasion
Brennen Puth reads a lot on paper and wanted his own site to print properly. His first attempt with Ctrl+P was sobering: the text was two levels too large, the header looked ugly in print, and links were merely grey in black and white. In his post he shows the fix with a few lines of CSS: a noPrint class for everything that does not belong on paper, an onlyPrint class for notes that only appear in print, 12 pt body text and black links.
He leaves one question open: code blocks get cut at page boundaries, and he asks for a suggestion. The answer is further down, because bk99.de had the same problem.
Where bk99.de started
Until now, the stylesheet did not contain a single print rule. A test print to PDF via Playwright showed the result: social bar, main menu with all services, breadcrumbs, the table of contents, the “Related” cards and the footer with all its links ended up on paper. The post on the DNS block in France took eight pages, the PHP2Shell post fourteen.
A bug that only shows up in print weighed more heavily: bk99.de’s colours hang on role variables that dark mode redefines, such as --ink for text. Browsers drop backgrounds when printing. Anyone printing in dark mode therefore got the light grey text of the dark design on white paper, barely readable.
What the print rules do
All rules sit in one @media print block at the end of the stylesheet, about 2.3 KB. They apply to every page type without regenerating a single page.
Colours: The block redefines the same role variables that dark mode uses, only to black and white. The selector :root:not([data-theme="light"]), :root[data-theme] matches in every case: without the toggle, with the light and with the dark design. It is exactly as specific as the dark mode rules and wins because it comes later in the file. There are still no fixed colour values in individual rules.
Left out: social bar, design toggle, main menu, breadcrumbs, table of contents, “Related”, the previous/next navigation, the buttons to the original and to the Web Archive, search field and topic filter, the services on the home page and the footer’s link list. Logo and copyright line stay, so it is clear where the sheet comes from.
Type and breaks: 11 pt body text, headings at 20, 14 and 12 pt. Headings stay with the following paragraph (break-after: avoid), and paragraphs leave at least three lines on their own (orphans, widows).
Links: You cannot click on paper, so the print version writes the target of every link in a post in brackets after it, with bk99.de in front of internal links. Links in the meta line (date, source, category) are excluded, as they would only be noise.
@media print {
.post-card-detail a[href^="http"]:not(.button)::after {
content: " (" attr(href) ")";
font-size: .8em; overflow-wrap: anywhere;
}
.post-card-detail a[href^="/"]:not(.button)::after {
content: " (bk99.de" attr(href) ")";
}
.post-meta a::after { content: none !important; }
}Code blocks: This is the answer to Puth’s open question. break-inside: avoid forbids a page break inside a block; if it no longer fits on the page, it starts on the next one. The same applies to figures, tables, quotes, list items in the knowledge sections and the preview cards in the overview and archive. A block longer than a page is still split; that cannot be prevented, but white-space: pre-wrap at least keeps lines from being cut off.
Expanded and without YouTube: collapsed sections (details) print expanded. Embedded videos appear as title with channel, length and address; the player is not loaded for printing.
How the result is checked
A small script starts the same local server as the release design check and uses Playwright to print eight page types as A4 PDFs: home page, blog overview, year archive, legal notice, a curated post in German and English, an own post with many code blocks and a post with a video. It runs twice, with the light and the dark colour scheme. The text of both versions is identical, and so is the page count.
I looked at the page images. That revealed two bugs a mere count would not have found: at first the meta line got long addresses appended, and preview cards were cut at the page boundary. Both are fixed. That the blog overview and archive sometimes need one more page as a result is intended: a whole card on the next page reads better than two halves.
A trap during the change: the stylesheet the build ships is generated from style.css in the project directory; assets/style.css is overwritten by every build. The first test print therefore looked exactly as before.
What is deliberately missing
The address of the printed page is not on the sheet. CSS cannot read the current address, and a script for it would cost more than it is worth: browsers print the address in the header or footer anyway when that option is enabled in the print dialog.
No page size is set either. The block only sets margins (@page { margin: 16mm 15mm; }); whether A4 or Letter is up to the printer.
Numbers at a glance
- Post on the DNS block: 5 instead of 8 A4 pages; PHP2Shell post: 11 instead of 14.
- Home page: 2 instead of 3 pages; legal notice: 1 instead of 2; English post on the DNS block: 5 instead of 8.
- Blog overview 8 instead of 7 and archive 2008 unchanged at 9 pages, because cards are no longer cut.
- Print block about 2.3 KB; the stylesheet grows to 38.2 KB, the release check allows at most 40 KB.
- Checked: 8 page types, each in light and dark, so 16 PDFs per run.
References
- Brennen Puth: Websites Should Be Printable (24 October 2026)
- Hacker News: Websites Should Be Printable (discussion)
- Test case on the blog: DNS block on call
Remarks
- Puth’s own solution works with classes in the HTML (noPrint, onlyPrint). bk99.de needs no new classes because the existing classes of the page components are enough; so none of the roughly 1,900 pages had to be uploaded again, only the stylesheet.
Links to the original source and the Web Archive open in a new tab.