A Dev Blog in the Game's Style
How the website you're reading is made: a Python script that turns the Markdown drafts in the game's repository into static pages, framed with the game's own 9-slice UI textures, set in its pixel fonts, and checked by a robot before anything goes online.
Build a website for the blog articles we have in a style fitting the game
Drafts in the repository #
These articles started as Markdown files in docs/blog/ of the game's repository, written from the git
history, the docs and the code, with screenshots from the development review folders. They sat there as
drafts for three days. When the human asked for a website, two constraints shaped it:
- The drafts stay the source. Articles keep being added and corrected next to the code they describe. The site has to be rebuilt from them at any time, never edited by hand.
- It should look like the game. The game's UI is lacquer panels, gold double frames, crimson title bars and pixel fonts. A generic blog theme would have been faster and wrong.
The result is one script, tools/blog/build_site.py, about 830 lines, and a 400-line stylesheet. It writes
plain HTML and CSS with a little JavaScript into website/, and it is now online at
dekiro.d12r.io.
One script, no framework #
A static site generator would have worked. The choice of a single script came from how the rest of the
project works: every generated thing (maps, dungeons, monster models, music, sound effects, the minimap
pictures) is a rerunnable Python tool in tools/, whose output is never edited by hand. The blog follows the
same rule. The dependencies are small and ordinary: markdown-it-py for Markdown, Pygments for code
highlighting, Pillow for images, fontTools for fonts.
python tools/blog/build_site.py # incremental, ~2 s; first run or --clean ~30 s
python tools/blog/check_site.py # link check
python tools/blog/check_site.py --shots review/website # + screenshots at 1440x900 and 390x844
Everything the site knows comes from files that already existed or are short:
| Output | Source |
|---|---|
| Articles, their order, teasers, the 8-post route | docs/blog/NN-*.md and the table in docs/blog/README.md |
| Title and subtitle | the # Title line and the italic paragraph under it |
| Image captions | the Markdown alt text |
| About page | tools/blog/about.md |
| Address, title, tagline, author, dates, hero image | tools/blog/site.json |
| Look | site.css + site.js, the game's assets/ui textures and assets/fonts |
An article that isn't in the README's table isn't published; the build warns. That makes the README the editorial switch: drafts can live in the folder until they're listed.
The build is incremental (images and fonts are only re-encoded when their source is newer) and deterministic: building twice gives byte-identical output, so the site's diff in git shows only what really changed.
The game's UI, in CSS #
The game draws its windows from 9-slice textures: small pixel-art images whose corners stay fixed while
the edges and middle stretch. The window panel is a 40 x 40 pixel PNG with 14-pixel corners; the crimson
title bar is 32 x 18.1 The UI textures are generated too: tools/ui/gen_ui_textures.js draws every 9-slice, ornament and
icon at 1x, and the game's theme sets the slice margins in code.
CSS has the same idea built in: border-image. So the site doesn't imitate the game's frames; it uses the
same files, copied from assets/ui, drawn at 2x with nearest-neighbour scaling:
.panel {
border-style: solid; border-width: 28px;
border-image: url(ui/panel.png) 14 fill / 28px round;
image-rendering: pixelated;
}
14 fill slices the panel at 14 pixels and keeps the middle as the background; / 28px draws the corners
at twice their size; round repeats the edges in whole tiles, so the gold fretwork never squashes;
image-rendering: pixelated keeps the pixels square. The title bars, the tabs in the header, the jade and
red buttons (with their hover and pressed states), the dividers and the tooltip frame all work the same
way, from 22 textures that weigh 30 KB together. The page background is a 48-pixel tile of lacquer noise,
generated deterministically by the build.
Fonts: 2 MB down to 6 KB #
The game uses three free (OFL) pixel fonts: DotGothic16 for headings, Pixelify Sans for UI text and Silkscreen for the tiny labels. On the web, a font file is a download on every first visit, and DotGothic16 is large because it covers Japanese: its TTF is 2 MB.
The site needs only Latin and a few symbols, so the build subsets each font with fontTools to a fixed list of Unicode ranges and saves it as WOFF2:
| Font | TTF | WOFF2 subset |
|---|---|---|
| DotGothic16 | 2,069 KB | 6 KB |
| Pixelify Sans | 79 KB | 10 KB |
| Silkscreen | 32 KB | 8 KB |
One detail sits in a code comment: Pixelify Sans has a "fi" ligature that, at blog sizes, reads like an "A". The subset keeps kerning and drops ligatures. The licences are copied next to the fonts.
Pixel fonts are made for short text. The articles are long, so the body text is set in a system serif at 19 pixels with lines of about 690 pixels; the pixel fonts are for titles, headings, navigation and labels.
Images: WebP with fallbacks #
The articles carry over a hundred screenshots, many of them 4K captures cut down to 1600 pixels. For each one the build writes:
- a WebP at up to 1600 pixels, plus an 800-pixel WebP for phones;
- a JPG or PNG fallback for browsers without WebP;
- a
<picture>element withsrcset,loading="lazy"and the width and height set, so the page doesn't jump while images load.
Pixel art and UI shots need different treatment from photos. The build counts an image's colours on a thumbnail: few colours means pixel art, a chart or UI, which is saved lossless and scaled with nearest neighbour; everything else is lossy at quality 80. Thumbnails for the home page are 480 x 270 crops.
All formats together are 33 MB on disk, most of it images. A visitor downloads only one variant of each, and only the ones they scroll to.
Social cards, a feed and the boring parts #
A link to an article shared on Discord or a social network shows a preview image. The build draws one per article, 1200 x 630: the article's first picture, darkened towards the bottom, the game's gold double frame, the 影 seal, a crimson label with the part number, and the title in DotGothic16. The seal comes from the same file as the iOS app icon.
The rest is the list that makes a site a site: an Atom feed with full articles, sitemap.xml,
robots.txt, a 404 page, a favicon in four sizes, canonical links, .nojekyll for GitHub Pages, and a
.gdignore.2 website/ sits inside the Godot project folder. Without a .gdignore, the editor would import
every website image and font as a game asset.
Most of the site works without knowing where it lives: every link between pages, images and styles is
relative, so website/index.html opens straight from disk. Only the feed, the sitemap and the social cards
need absolute addresses. When the human said "The domain will be dekiro.d12r.io", the change was one line
in site.json:
"base_url": "https://dekiro.d12r.io/",
and a rebuild.
No tracking #
The footer says "No cookies, no tracking", and the build makes it true by construction: no analytics, no
web fonts from a CDN, no embedded players, no external requests at all. Everything a page loads is in
website/.
Checked by a robot #
check_site.py serves website/ on localhost, fetches every page and everything each page links to, and
reports broken links, missing images or fonts, #anchor links to headings that don't exist, and images
without alt text or size. It exits with an error if anything is broken, so it can gate a deploy.
With --shots, it also drives a headless browser (Playwright, using the Edge already installed on the
machine, so no browser download) and saves screenshots at 1440 x 900 and at 390 x 844, a phone. Those
screenshots were the design review. The first round found three things: the text in the crimson title
bars sat on the top edge instead of the middle, an empty column next to the article title on desktop, and
body text that looked huge on phones. That last one turned out to be the 2x screenshot, not the page; the
phone layout got narrower window frames anyway, which left more room per line.
A Tufte-inspired layout #
Wide screens leave room beside a 690-pixel text column, and a second pass on the site uses it the way Edward Tufte's books do: side facts sit next to the line that needs them, not at the bottom of the page. All of it still comes from plain Markdown, and an article that uses none of it renders as before.
- Sidenotes. Cross-references, file paths and small facts are ordinary Markdown footnotes in the
drafts (
[^1], with markdown-it's footnote plugin). The build turns each one into a numbered note in the margin beside its paragraph. A label starting withm-makes an unnumbered margin note instead. On narrow screens a note collapses behind its number and opens on a tap, done with a checkbox and a label, no JavaScript. The Atom feed, where there is no margin, gets them as numbered endnotes. - Margin and full-width figures. The image title picks the kind:
puts a small image (an icon, a sprite, a phone screenshot) in the margin,"full"lets a panorama, a before/after pair or a chart span the text column and the margin. Thesrcsetsizes follow, so a margin figure doesn't download its 1600-pixel version. An unknown title stops the build. - Epigraphs. A blockquote whose last line starts with an em dash becomes an epigraph with that line as its attribution. Most of the newer posts open with the developer's request that started the work.
- Small-caps openings. The first few words after each section heading are set in small caps, cut at the first punctuation mark so a phrase is never split oddly.
- Booktabs tables, in the spirit of the LaTeX package: a few horizontal rules instead of a grid, and columns whose cells are all numbers are right-aligned automatically.
The phone screenshot below is a margin figure: on a wide screen it sits in the margin, not in the text column.
What it took #
The website was one request and one agent run, with one round of fixes after the screenshots, and the
domain a week later. The home page's hero image and the About page's gallery are Steam store screenshots,
which live in a git-ignored folder; so that the site stays rebuildable from a fresh clone, those seven shots
were copied into tools/blog/media/ as JPGs (1.5 MB).
Takeaways #
- Keep the drafts as the source, next to the code, and generate the site from them. Corrections stay in one place.
- Reuse the game's art as-is. 9-slice textures map directly onto CSS
border-image; pixel art needsimage-rendering: pixelatedand whole-number scales. - Subset fonts. A CJK pixel font went from 2 MB to 6 KB for Latin-only use.
- Make links relative and keep the absolute address in one setting.
- Check the built site, not the source, and take screenshots at phone size: that's where layouts break.
What's next #
Back to the game. The next posts return to what players will actually do in Dekiro.