The personal site of Tim Conrad: security research, a CVE write-up, a PGP key, and product pages for two browser games.
It is plain HTML, CSS and a little JavaScript. There is no build step and no dependency. Fonts are self-hosted, and a Content-Security-Policy on every page blocks third-party requests. The only exception is the live game demo on a product page, which loads from its own host after the visitor presses play.
| Page | Path | Content |
|---|---|---|
| Front page | / |
Overview, the two games, the latest thoughts, the CVE summary, the PGP key, contact |
| Thoughts | /thoughts/ |
Ideas I work through and come back to; each in /thoughts/<slug>/ with a status, sources, revisions and feedback |
| Thought | /thoughts/humans-md/ |
A humans.md for working with models (animated diagrams, the file HUMANS.md to download) |
| Thought | /thoughts/encrypted-patches/ |
Encrypted patches, unlocked on patch day (inline SVG diagrams) |
| Write-up | /research/cve-2022-46166/ |
CVE-2022-46166: template injection in Spring Boot Admin |
| Kleinbahn | /projects/kleinbahn/ |
Reel, live demo from tiny-train.beacon.host, features, stills |
| Wortzauber | /projects/wortzauber/ |
Reel, live demo from wortzauber.beacon.host, features, stills |
| llms.txt | /llms.txt |
A summary of the site for language models (llmstxt.org format), made at deploy time |
| RSS feed | /thoughts/feed.xml |
The thoughts, newest first, made at deploy time. /blog/feed.xml is the same feed for readers of the old address |
| Old blog URLs | /blog/, /blog/encrypted-patches/ |
Redirect to /thoughts/ |
| PGP key | /key.asc |
Public key, fingerprint A886 6CB1 6B07 9685 5615 74B2 A91D ABBE C996 7390 |
| Old write-up URL | /CVE/CVE-2022-46166/ |
Redirects to the new write-up address |
| Encrypted sites | /sealed/<site>/ |
Private games and files, encrypted with sealed. A login page, then the browser decrypts the site. Not linked, not in the sitemap, kept out of search engines by robots.txt |
| Path | What it is |
|---|---|
index.html, 404.html |
Front page and not-found page |
research/, projects/, thoughts/ |
One folder with an index.html per page |
blog/, CVE/ |
Redirects from old addresses |
assets/site.css |
All styles and the colour tokens of both themes |
assets/theme.js |
Applies the saved theme before the first paint |
assets/site.js |
Theme toggle, copy buttons, the table of contents, the tap-to-play demo, diagrams with steps and animated diagrams |
assets/fonts/ |
Site fonts (SIL Open Font License, see OFL.txt) |
assets/img/ |
Pictures and the recording of the write-up |
assets/projects/ |
Reels, posters, stills, phone pictures and gameplay loops of the two games |
assets/og.png |
Social preview image |
CNAME, .nojekyll, robots.txt, sitemap.xml |
Hosting and crawler files |
tools/deploy.sh |
Publishes the site files to gh-pages |
tools/ship.sh |
Merges a PR, waits for the deploy and checks t7d.dev |
tools/preview-artifact.py |
Builds a page as a Claude artifact bundle, for a review before it is published |
tools/reels/ |
Scripts that film the two games and cut the reels |
tools/review/, docker-compose.dev.yml |
The review site: live reload and a note tab. Never published |
.github/workflows/deploy.yml |
Runs the deploy on every push to master |
DESIGN.md |
How the site looks and sounds: colours, type, layout, components and the wording rules |
CLAUDE.md |
Instructions for Claude Code in this repository |
.claude/skills/updating-product-reels/ |
Working guide for Claude Code: how to take the reels and pictures again |
.claude/skills/adding-a-thought/ |
Working guide for Claude Code: how to turn a link into a new thought, and how to revise one |
.claude/skills/updating-game-timeline/ |
Working guide for Claude Code: how to film the timeline clips of a game page |
.claude/skills/handling-review-notes/ |
Working guide for Claude Code: how to handle Tim's notes from the review site |
.claude/rules/pages.md |
Rules for a new page; Claude Code loads them only when it works on an HTML page, the sitemap or the deploy script |
.claude/settings.json |
A hook that prints the number of open review notes at the start of a session |
tools/check.py |
Checks the CSP rules and the sitemap of every page; CI runs it |
python3 -m http.server 8000Then open http://localhost:8000. All paths are absolute, so serve from the repository root.
For live reload and the note tab, run python3 tools/review/devserver.py instead (port 8000).
The review site serves this clone with live reload and a tab »Note« at the right edge. A note keeps the page, the width, the theme and the part that you pointed at. It is a JSON file in .review/notes/, and git ignores that folder. The site files on gh-pages do not change: tools/deploy.sh never publishes tools/.
To run it in a container behind a login proxy:
- Make the notes folder:
mkdir -p .review/notes. - Write the host name into
.env:REVIEW_HOST=<host>. - Start the stack:
docker-compose -p t7d-review -f docker-compose.dev.yml up -d.
tools/review/show.py <id> opens the page of a note and frames its part in a picture. A handled note goes to .review/notes/done/ with an "answer" field, which the tab shows under »My notes«. Tests: python3 -m unittest tools/review/test_devserver.py.
GitHub Pages serves the gh-pages branch at https://t7d.dev. master is the source, and nobody edits gh-pages by hand.
- Merge a change into
master. - The workflow
.github/workflows/deploy.ymlrunstools/deploy.sh. - The script copies the site files of that commit to
gh-pages, writesllms.txtand the feeds from the pages withtools/gen-feeds.py, and pushes.tools/,.claude/,.github/and this README stay onmaster.
To ship a pull request in one step, run tools/ship.sh [<pr>]. It merges the PR, puts this clone on master, waits for the deploy and the Pages build, and checks that t7d.dev serves each site file of the PR. It runs no checks: look at the pages first.
The folder sealed/ on gh-pages does not come from master. The admin app of sealed in the lab builds each encrypted site and pushes it to gh-pages as sealed/<site>/: a login page, a keyring and ciphertext, never the plain files. tools/deploy.sh keeps that folder as it is, and sealed changes nothing outside it. Users, passwords and access are managed in the sealed admin app, not here. The setup is in "Publish below t7d.dev" in the README of sealed.
To deploy by hand, run tools/deploy.sh on master. To look at llms.txt and the feed locally, run python3 tools/gen-feeds.py (git ignores the two files). A new top-level file or folder that belongs to the site must be added to the SITE list in that script, or it is not published.
- Copy
research/cve-2022-46166/index.htmlto a new folder underresearch/and edit the content. - Link it from the Research section of
index.html. - Add the URL to
sitemap.xml.
The live demo updates by itself when the game is published to beacon.host. The reel, the poster, the stills and the phone picture are files in assets/projects/ and must be taken again:
node tools/reels/kleinbahn-shots.mjs ../tiny-train # frames of the Kleinbahn shots
node tools/reels/wortzauber-takes.mjs ../wortzauber --rec # recordings of the Wortzauber takes
node tools/reels/overlays.mjs ../wortzauber # titles and captions
python3 tools/reels/build.py kb
python3 tools/reels/build.py wz ../wortzauber
python3 tools/reels/build.py publish # writes assets/projects/This needs Node with Playwright, ffmpeg, and both game repositories cloned next to this one. The skill in .claude/skills/updating-product-reels/SKILL.md has the details, the checks and the pitfalls.
Each product page allows exactly one frame host. Add the new host to frame-src in the Content-Security-Policy line of that page, or the phone frame stays empty.
See DESIGN.md.