Room and machine info cards for CTF writeup sites. Point a writeup at its TryHackMe room or Hack The Box machine with url: in the front-matter, and writeup-cards fetches the platform's public details and renders a card at the top of the writeup:
- Hack The Box: synopsis, difficulty, release and retirement, OS, creators, rating, user/root owns, first bloods, the player-rated difficulty bars and the radar graph (Enumeration, Real-Life, CVE, Custom Exploitation, CTF-Like).
- TryHackMe: description, difficulty, release date, type, estimated time, people joined, votes and access.
No framework, no build step and no runtime requests: a Python script saves JSON, and a dependency-free ES module turns it into HTML at build time or in the browser. Charts are inline SVG. The cards are accessible, escape every value, and follow light/dark mode.
Open examples/demo.html to see both cards with sample data (run node examples/render.mjs to regenerate it).
pip install pillow # icons are saved as small WebP files
export HTB_APP_TOKEN=... # optional, for Hack The Box (see below)
python writeup_cards.py fetch writeups/ --out meta/Your writeups need a url: (or link: / room:) in their front-matter:
---
title: Titanic
url: https://app.hackthebox.com/machines/Titanic
---That writes meta/htb/machine-titanic.json (and .webp), or meta/thm/<room-code>.json for a TryHackMe room. Then render:
import { roomCardHtml } from "./room-card.js";
const meta = JSON.parse(await readFile("meta/htb/machine-titanic.json", "utf8"));
const html = roomCardHtml(meta, { iconBase: "/meta/htb/" });and include room-card.css on the page. examples/render.mjs is a complete build-time example.
| Option | Default | |
|---|---|---|
iconBase |
none | URL folder holding the saved icon (meta.icon). Without it, or meta.iconSrc, no icon is shown. |
locale |
"en-GB" |
Locale for numbers and dates. |
headingLevel |
"h2" |
Tag for the room name, to fit your page's outline. |
difficultyBars(buckets) and radarGraph(values) are exported too if you only want the charts. Links and icons are only rendered for http(s) or site-relative URLs.
Override the --wc-* custom properties on .wc-card or any ancestor, for example:
.wc-card { --wc-surface: #0b0c0e; --wc-link: #9fef00; --wc-font: "Inter", sans-serif; }Difficulty colours are --wc-easy, --wc-medium, --wc-hard and --wc-insane.
Uses the official v4 API with your own App Token: on app.hackthebox.com go to Account Settings → App Tokens, create one, and set it as HTB_APP_TOKEN (in CI, as a repository secret). Machines and challenges are supported; Academy modules and CTF-event challenges aren't (they have no public details). Retired machines are fetched once and kept.
Check your token and see exactly what gets parsed:
python writeup_cards.py probe machine:TitanicUses TryHackMe's public room API. Plain HTTP requests are often refused by TryHackMe's bot protection (HTTP 429), almost always from CI runners. When that happens, writeup-cards keeps the details it already saved and says so.
To refresh TryHackMe rooms, run locally with --browser. That retries with an ordinary headless browser session, which a home connection is normally let through. It needs Playwright (pip install playwright && playwright install chromium).
python writeup_cards.py fetch writeups/ --out meta/ --browserThen commit the updated meta/ folder.
writeup-cards does not disguise browsers or work around bot protection, and pull requests that do won't be accepted. If you need TryHackMe data in CI, ask TryHackMe about API access.
Saved details refresh after --refresh-days (default 7). A failed fetch keeps the last saved file, and the command exits 0, so a bad day never breaks your site build.
name: Writeup cards
on:
schedule: [{ cron: "0 6 * * 1" }] # weekly
workflow_dispatch:
permissions:
contents: write
jobs:
cards:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: paracausaltelemetry/writeup-cards@v1
with:
writeups: writeups
out: meta
htb-token: ${{ secrets.HTB_APP_TOKEN }}
- run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add meta
git diff --cached --quiet || (git commit -m "Refresh writeup cards" && git push)Then rebuild your site as usual. In CI the Action refreshes Hack The Box; refresh TryHackMe locally as described above.
- Python 3.9+ and Pillow for fetching; Playwright only for
--browser. - Any modern browser or Node 18+ for
room-card.js(an ES module).
python -m unittest discover -s tests
node --test tests/room-card.test.mjs
node examples/render.mjsBuilt for paracausaltelemetry.com, where it powers the writeup pages. Not affiliated with TryHackMe or Hack The Box. Their names, data and images belong to them, and the sample data here is for demonstration only. Use their services within their terms.