Building a Japanese-First Read-Later PWA: From Pocket Shutdown to Launch
When Mozilla shut down Pocket in July 2025, I lost my favorite tool. Worse, none of the English alternatives (Instapaper, Readwise, Matter, Raindrop) had Japanese UI, and their article extraction was mediocre on Japanese pages. So I built one. It's called Readbox — Japanese-first, English-too, read-later as a PWA. Here's what I learned shipping it. The stack Next.js 15 App Router + TypeScript strict (no any ) Supabase (Postgres + Auth + RLS) Stripe (JPY + USD prices, locale-routed) Tailwind CSS Service Worker for PWA install + offline read Three things that bit me 1. Article extraction on Vercel serverless First attempt: Mozilla Readability + jsdom. Doesn't bundle on Vercel because of ESM compatibility issues and the 50MB serverless function size limit. I tried 6 approaches — Webpack externals, dynamic imports, edge runtime — none worked cleanly. Ended up using Jina Reader , which returns clean Markdown/HTML from any URL. Trade-off: third-party dependency, rate limits at scale. But it works today, and it's free. 2. Storing article body on-device I didn't want to host millions of articles' worth of HTML on Supabase (cost + privacy). Solution: extracted HTML lives in the browser's IndexedDB only (via Dexie); only metadata (URL, title, tags, read status) syncs to the server. Trade-off: cross-device sync of body content doesn't work seamlessly. Acceptable for a "read it later" workflow where you usually read on the device you saved on. 3. i18n routing — the silent sitemap killer For Japanese + English from one codebase: app/[locale]/ segment with /en prefix for English (default Japanese has no prefix, to preserve old URLs). Middleware detects cookie / Accept-Language and redirects accordingly. The gotcha (cost me a launch-day hour): middleware matcher excludes _next , api , image extensions — but if you forget .xml/.txt/.webmanifest , sitemap.xml and robots.txt get rewritten to /ja/sitemap.xml (which doesn't exist as a route → 404). Fix: export const config = { matcher