PSBloggen migration

Migrated a ten-year PlayStation enthusiast blog off WordPress and built an IGDB-powered game discovery portal on top of the new stack.

Case study: Case study: 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 .md with 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

LayerTechnology
Game portalVite + React + TypeScript + Tailwind
Editorial siteAstro + Tailwind
BackendExpress
DatabaseSQLite via better-sqlite3
AuthJWT in httpOnly cookies, bcrypt
Media storageCloudflare R2 (S3-compatible)
Productionnginx + 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.