
Process Digitization + Custom Software · 2026
Geology.mn
Client
Geology.mn
Year
2026
Role
Strategy, Product, Engineering, Migration
Stack
Next.js 16 · React 19 · Tailwind v4 · Supabase Postgres · Drizzle · Auth.js v5 · TipTap · Vercel Blob · Resend · Vercel Cron
A 15-year-old geology journal moved from PHP onto a modern bilingual platform. 262 articles, 458 contributor profiles, a dark editorial console, a weekly auto-crawled Top 10 — and zero broken legacy URLs.
Challenge
Geology.mn has been the editorial home of a working geology community for the better part of fifteen years — 262 articles, a 181-entry mineral reference, a 216-term glossary, a 458-name contributors directory, a forum with 26 sections and a hundred-plus posts. The site running it was a legacy PHP install with a MySQL dump for a database. Editorial work was happening, but every change risked the whole thing. The community had outgrown the platform.
Approach
We migrated the entire dataset onto a modern bilingual platform without breaking a single inbound link. Every legacy /index.php?id=… URL 301s to its canonical path in either language. The new editorial workflow lives in a dark admin console with role-based access — admin and editor only — and bilingual sibling articles, so a single piece can carry both records linked to each other. A TipTap rich editor replaced raw HTML pasting. Comments are auth-gated and moderation-queued. New media goes to Vercel Blob; the 1,750 legacy files migrated in place.
The weekly Top 10 is now an automated RSS crawler running on Vercel Cron — Monday at 06:00 UTC — with a manual-trigger admin review before anything goes live.
Outcome
- 262 articles, 181 minerals, 216 glossary terms, 458 contributor profiles, 1,750 legacy media files migrated end-to-end.
- A dark editorial admin console with article CRUD, bilingual siblings, media library, comments moderation, and user role management.
- Zero broken legacy URLs — every
/index.php?id=…301s to its canonical bilingual path. - A weekly auto-crawled Top 10 ingesting RSS 2.0 + Atom, queued for editorial review before publish.
Stack reasoning
Supabase Postgres for the relational core because the dataset is highly relational — articles, authors, minerals, glossary terms, forum threads, comments, all with foreign keys. Drizzle over Prisma for the migration because we needed precise control over the import scripts and Drizzle's schema-as-code is closer to raw SQL. Auth.js v5 (still in beta at build time) because the role model was nuanced — admin / editor / contributor / public — and Auth.js handled it without a custom claim layer. TipTap because raw HTML pasting was the source of half the editorial pain. Vercel Cron because the Top-10 crawl is a single weekly job, not a long-running worker.
What we would do differently
The 301 redirect map was generated from the legacy MySQL dump and kept in a single JSON file. It works, but on a future migration we would push the redirects into Postgres so the editorial team can amend them without a redeploy. Second, the search is currently Postgres ILIKE — fast enough for the dataset size but not great for ranking. A future pass moves search behind Postgres full-text or an external index.
○ Gallery
