PSBloggen migration
Migrated a ten-year PlayStation enthusiast blog off WordPress and grew it into a full IGDB-powered editorial platform: curated collections, a daily guess-the-game feature, PS Plus catalog tracking, and reader accounts, all self-hosted.
Read the case study Case study: PSBloggen migration →- Migrated 10+ years of WordPress content to Markdown via a custom XML → Turndown pipeline — 4,300+ posts, 1,700+ backfilled with IGDB metadata
- Dual-stack: Astro SSR editorial site for reviews and editorial, React SPA for live IGDB game data, one shared auth and nav
- Grew past the original scope into a full editorial platform: curated 'Samlingar' collections, a daily guess-the-game feature, PS Plus catalog tracking, author/developer profile pages
- Reader-facing additions: shareable public game lists per user, year-in-review stats pages, a 'this day in gaming history' homepage widget, and a full PlayStation-themed visual redesign with a glowing SVG score arc
- Community features: threaded comments with spam filtering, reader-submitted content reports, contact inbox with email replies — all moderated from the admin
- Admin CMS matured into a real operations console: first-party + Umami analytics, scheduled R2 backups with one-click restore, a Discord review-pipeline bot, bulk publish/audit tooling
- Rebuilt the content pipeline after diagnosing a real regression: dropped Astro's content collections for direct filesystem reads, cutting a content-heavy build from 15s/1.75GB to 4.5s/850MB
- Found and fixed a CI build-cache bug (a GitHub Action silently tearing down a persistent build cache every run) that cut a 2-minute CI build down to under 40 seconds
- Zero-downtime content publishing: admin saves a post, a debounced background rebuild atomically hot-swaps the site with a real filesystem lock — no restart, no visible downtime
- Single-command Docker deploy with nginx routing both frontends, plus two independently-deployed sibling services (a review-pipeline bot and self-hosted analytics) over a shared Docker network
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. What started as a migration plus a game-discovery portal has since grown into a full self-hosted editorial platform — curated collections, a daily guessing game, PS Plus catalog tracking, reader accounts and moderation, and an admin console with its own analytics and operational tooling.
What it is
Editorial site (Astro): Reviews with scores, verdict badges, pros/cons, and platform tags. Editorial posts, curated “Samlingar” collections with mood/category browsing, author and developer profile pages, and a daily “guess the game” feature built from real review data. Runs as a built SSR Node process. Publishing is zero-downtime: the admin saves a post, a debounced background rebuild runs (~60–90s for the full ~4,300-post archive), and swaps the built site in atomically once it finishes — no restart, no visible interruption. Every saved .md file is also pushed to Git automatically, so Git stays the single source of persistent truth.
Game portal (React): Upcoming and recent PS4/PS5 releases pulled from IGDB, with cover art, trailers, screenshots, similar games, and full-text search. PS Plus catalog browsing with monthly navigation. User accounts with favourites, play-status tracking, and per-game notes.
Admin CMS: Grew from a Markdown-generating form into a real operations console — first-party and Umami analytics side by side, scheduled database backups to Cloudflare R2 with one-click restore, a script runner for bulk imports, content moderation for reader reports and comments, and integration with a Discord bot that handles review assignment and voting.
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. The importer since grew into an ongoing pipeline — it also ingests new and scheduled posts straight from the old blog’s RSS feed, not just the one-time historical export.
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, play statuses, comments, and the PS Plus catalog. Embedded in the Express process, backed up to Cloudflare R2 on a schedule.
nginx routes the two frontends through one domain in production, plus a same-origin /stats/ proxy to a sibling analytics deployment (avoids adblockers flagging a third-party analytics domain):
/_astro/*→ Astro pre-built static assets/reviews/,/editorial/→ Astro SSR Node process/api/→ Express/stats/→ Umami (separate deployment, shared Docker network)/uploads/→ Cloudflare R2 (S3-compatible, no egress fees)/*→ React SPA
Tech stack
| Layer | Technology |
|---|---|
| Game portal | Vite + React + TypeScript + Tailwind |
| Editorial site | Astro (built SSR, no client content-collection layer) + Tailwind |
| Backend | Express |
| Database | SQLite via better-sqlite3, scheduled backups to R2 |
| Auth | JWT in httpOnly cookies, bcrypt, hCaptcha on signup |
| Media storage | Cloudflare R2 (S3-compatible) |
| Analytics | First-party pageview counter + self-hosted Umami (sibling deployment) |
| Production | nginx + Docker Compose, shared network with sibling services |
Status
Live and in active daily development — well past the original beta. What launched as a migration plus a game portal has grown into the crew’s full editorial platform: collections, a daily guessing game, PS Plus tracking, comments, and an admin console with real operational tooling behind it.