My Website
watkorn.me: a static, A+-hardened portfolio that is also a tiny CTF. How it's built, secured and shipped.


This site is my home for CTF writeups and side projects, and a challenge in its own right. The home page is a terminal with five flags hidden around the site. If you're a security person, the fastest way to get to know me is to try to break my website.
Goals
- Play first, read second, hire third. The terminal is the front door. Writeups come next, then projects and contact links.
- Writing a post should never mean touching layout code. A writeup is a Markdown file and a
git push. - Keep the attack surface close to zero. No server, no database, no third-party scripts.
How it works
content/*.md ─▶ scripts/content.mjs ─▶ JSON ─▶ Vite + React
│
prerender every route to HTML ◀┘ (+ sitemap, RSS, CSP)
│
GitHub Actions: build → Playwright tests → publish to gh-pages
│
GitHub Pages ◀── Cloudflare (TLS, HSTS, security headers)
- Content: blog posts and projects are Markdown with frontmatter. At build time, marked and highlight.js turn them into HTML, so no Markdown parser ships to the browser. Drafts are visible in dev and excluded from production.
- Pages: React 19 + React Router 7 on Vite. Every route is prerendered to its own HTML file, so posts are indexable and link previews in Discord or LINE show a real title and image. React then takes over for the interactive bits.
- Deploys: every push to
mainbuilds the site and runs Playwright smoke tests on desktop and mobile. Only if they pass does it publish the static files to thegh-pagesbranch. A failing test means the live site doesn't change.
Security
Being a security person's website, it had better hold up:
- A+ on securityheaders.com. Cloudflare adds HSTS,
X-Frame-Options,Referrer-Policy,Permissions-Policyand COOP, which GitHub Pages can't send. - A strict Content-Security-Policy on every page:
script-src 'self'andstyle-srcwithout'unsafe-inline'. So no inline scripts or inline styles, and a test fails the build if any page triggers a CSP violation. - No source maps in production.
- Zero known vulnerable dependencies: moving from Create React App to Vite took
npm auditfrom 62 findings to 0. Dependabot keeps it that way, and every GitHub Action is pinned to a commit SHA. - Visitor input is never rendered as HTML. All Markdown is rendered at build time from files in the repo.
The CTF inside
The terminal speaks a little shell: ls -la, cd, cat, open <post>, hint, submit <flag>, with history and Tab completion. Five levels (10 to 50 points) are hidden in places a player should learn to look. Flags are checked by SHA-256 hash only, so reading the JavaScript doesn't hand over the answers. Progress lives on the achievements page, stored only in your browser.
A test proves every flag is reachable on the built site and matches its hash, so I can't accidentally ship an unsolvable level.
Design: Handheld Quest
The look is a pocket game console. The page is the console's plastic shell: dandelion yellow by day, grape at night. Content sits on "screens" with the handheld's big bottom-right curve, the terminal is an LCD, and every button is a rubber key that physically presses down.
The mascot is a 32×32 pixel yeti drawn in code. It has 8 recolourable layers, a 2-frame walk cycle, and a Logo Lab for exporting it in any palette. Everything comes from one set of design tokens, so both themes stay consistent. The site works from 320 px phones up. I used the Hallmark and Impeccable design skills to avoid the generic "AI portfolio" look.
Numbers
- Lighthouse (local): Performance 97 · Accessibility 100 · SEO 100
- About 82 KB of gzipped JavaScript; each post's content loads as its own small chunk
- Pixel yeti: 8 KB for both frames of the sprite; every icon is under 10 KB
What I learned
- Prerendering beats a SPA for a content site. Hash URLs were invisible to search engines and link previews.
- A strict CSP pays for itself. Dropping
'unsafe-inline'surfaced two inline styles hiding in the prerendered HTML, and now a test keeps them from coming back. - Tests for the fun stuff matter too. An unsolvable CTF level is a bug.
Stack
React 19 · React Router 7 · Vite · Tailwind CSS 4 · marked · highlight.js · Playwright · Lighthouse CI · GitHub Actions · GitHub Pages · Cloudflare
The source is open on GitHub (link below). Found something? Tell me, or better yet, find the flags first.