PSBloggen migration
Highlights
- Migrated 10+ years of WordPress content to Markdown via a custom XML β Turndown conversion pipeline
- Dual-stack: Astro SSR editorial site for reviews and editorial, React SPA for live IGDB game data
- IGDB-powered game listings, detail pages, full-text search, genre and developer browsing
- User accounts with favourites and per-game play status β Want / Playing / Completed / Dropped
- In-memory IGDB cache (30 min listings, 2 hr detail) to stay within Twitch API rate limits
- Admin CMS with scheduled publishing, social auto-posting to Bluesky and Buffer, and on-demand zero-downtime rebuilds
- Single-command Docker deploy with nginx routing both frontends through one domain
A small crew of PlayStation enthusiasts had been running a game blog for over ten years. The goal: get off WordPress, keep the content, and build something the team actually wanted to use. The new stack is an Astro static site for reviews and editorial writing, paired with a React portal for live game discovery β both sharing navigation and auth, all self-hosted.
What it is
Editorial site (Astro): Reviews with scores, verdict badges, pros/cons, and platform tags. Editorial posts for longer commentary. Runs as an SSR Node process (@astrojs/node), built in CI. The content directory is mounted as a Docker volume that shadows the baked-in content β detail pages read from the filesystem at request time and are live immediately after a save. Index and listing pages require a rebuild, which runs in the background (~60β90 s for ~4 000 posts), hot-swaps dist/, and restarts only the Astro process β the site stays live throughout. Every saved .md file is also pushed to Git automatically, so Git remains the single source of persistent truth: the volume is the working copy, the repo is the record.
Game portal (React): Upcoming and recent PS4/PS5 releases pulled from IGDB, with cover art, trailers, screenshots, similar games, and full-text search. Genre and developer browsing. User accounts with favourites and play-status tracking.
The migration
WordPress exports to XML. Each post is HTML β category metadata, tags, publish dates, author, and attached media all intact. A Node.js pipeline converted the export:
- Parse XML with
xml2js - Pre-process WordPress-specific markup (caption shortcodes β
<figure>, oEmbed URLs β clean links) - Convert HTML to Markdown with Turndown
- Rewrite internal links and download media locally
- Write each post as
.mdwith Astro frontmatter
Astroβs Zod schema validation caught malformed dates and missing fields at build time.
Architecture
Express API proxies IGDB (keeping Twitch credentials server-side), handles JWT auth in httpOnly cookies, and serves recent editorial posts as JSON for the React homepage. An in-memory TTL cache wraps all IGDB responses.
SQLite (via better-sqlite3) stores user accounts, favourites, and play statuses. Embedded in the Express process β no separate database container.
nginx routes the two frontends through one domain in production:
/_astro/*β Astro pre-built static assets/reviews/,/editorial/β Astro SSR Node process/api/β Express/uploads/β Cloudflare R2 (S3-compatible, no egress fees)/*β React SPA
Tech stack
| Layer | Technology |
|---|---|
| Game portal | Vite + React + TypeScript + Tailwind |
| Editorial site | Astro + Tailwind |
| Backend | Express |
| Database | SQLite via better-sqlite3 |
| Auth | JWT in httpOnly cookies, bcrypt |
| Media storage | Cloudflare R2 (S3-compatible) |
| Production | nginx + Docker Compose |
Status
Currently in beta, weβre working out some of the workflows and look and feel. Goal is to replace the current chunky wordpress around summer.