
Oldweb Diary
A digital museum of early internet culture. Built a full-stack nostalgia platform with three retro OS themes, 50+ meme origin stories, This Day in Internet History, a Lost Media Vault, and a community Memory Wall.
THE IDEA
The early internet was weird, wonderful, and almost entirely undocumented in one place. Rickroll origin stories, Flash animations nobody saved, GeoCities pages that vanished overnight, and the existential dread of the MySpace Top 8.
Oldweb Diary is a digital museum dedicated to preserving that era — built as a full-stack web app styled to look and feel like the operating systems people were actually using at the time.
THE PRODUCT
A fully functional nostalgia platform with five main sections:
1. Meme Museum
50+ memes with full origin stories, era classification, tags, like counts, view counts, and a related memes section. Each meme has its own detail page with dynamic OG images for social sharing.
2. This Day in Internet History
A date-matched events engine that shows what happened on today's date across internet history. Falls back to featured events when no match exists for the current date.
3. Lost Media Vault
An archive of Flash animations, viral videos, old websites, and browser games that nearly disappeared. Each item is tagged as Available, Partially Lost, or Fully Lost, with related media suggestions for internal discovery.
4. Memory Wall
A community submission system where users share their earliest internet memories. Includes a moderation queue, platform tags, like functionality, and pagination.
5. Retro Assets Library
A downloadable collection of Win95 sounds, animated cursors, wallpapers, icons, and bitmap fonts from the era.
THE RETRO OS THEME SYSTEM
This was the defining design challenge of the project. The entire UI is skinned to look like one of three operating systems — and users can switch between them at any time.
Win95: Teal desktop, grey windows, navy titlebars, inset/raised border shadows, MS Sans Serif typography.
WinXP: Bliss-inspired CSS gradient wallpaper, warm off-white windows, blue gradient titlebars, colored window buttons (yellow/green/red), rounded Start button.
Mac OS9: Platinum windows, gradient titlebars, left-side close button with centered title, SVG minimize/maximize icons, Chicago font, Apple Menu in system tray.
The skin system uses CSS custom properties (--skin-* variables) so every component automatically adapts to any theme without conditional rendering. Switching themes updates a single data-skin attribute on the <html> element.
Zero theme flash on page load — achieved by reading the user's saved theme from an HTTP cookie server-side in layout.tsx and rendering the correct data-skin in the initial HTML response. No JavaScript needed for the first paint.
THE TECHNICAL DEEP DIVE
Stack
- Framework: Next.js 16 (App Router, Turbopack)
- Language: TypeScript throughout
- API layer: tRPC v11 with type-safe client/server
- ORM: Prisma v5 with PostgreSQL (Supabase)
- Caching / counters: Upstash Redis
- Media storage: Cloudflare R2
- Deployment: Vercel (Singapore region)
- Monitoring: Sentry + PostHog
Hardest Technical Problems
Problem 1: OG Image Generation in Edge Runtime
Each meme detail page generates a dynamic social share card via @vercel/og. The challenge: the edge runtime doesn't have Node.js APIs (Buffer, Uint8Array workarounds required), .webp images fail silently, ImageResponse crashes on JSX .map() calls, and boxShadow is unsupported.
Solution: Split into two routes — an edge runtime OG renderer that fetches image data from a separate Node.js route that handles Prisma. Used Array.from(bytes).map().join('') + btoa() for edge-safe base64 encoding. Skipped .webp files entirely (showed title placeholder instead).
Problem 2: Theme Flash Elimination
Initial approach — localStorage + useEffect — caused a visible flash of Win95 theme before the correct theme loaded. Several intermediate approaches failed (inline <head> script, useState lazy initializer) because React's hydration mismatch warnings persisted and the flash remained.
Final solution: Cookie-based SSR. The server reads the oldweb-skin cookie in layout.tsx before rendering, sets data-skin in the HTML response, and passes initialSkin as a prop to SkinProvider. The browser receives the correct theme in the first byte of HTML — JavaScript is never involved in the initial render.
Problem 3: Prisma Sort by Month/Day
The admin events panel needed events sorted featured-first, then by calendar day (January before December) — not by year. Prisma's orderBy can't express EXTRACT(MONTH FROM eventDate).
Solution: Raw SQL query using $queryRaw with EXTRACT(MONTH) and EXTRACT(DAY) ordering, properly typed with Prisma's raw query type system.
Problem 4: ESLint Rule Compliance Without Breaking Functionality
Nine ESLint errors across the codebase — all setState in useEffect patterns in admin form components, and a window.location.hash mutation error in the tab component.
Solution: Moved all state initialization to useState lazy initializers and event handler inline logic. Replaced the hash mutation in AdminTabs with a useEffect that watches active state and writes to window.location.hash from within an effect — satisfying the immutability rule while preserving tab persistence behavior.
THE ADMIN CMS
A password-protected 6-tab admin panel at a secret URL, built entirely without a third-party CMS:
- Memes — create, edit, delete, feature/unfeature with auto-slug generation
- Events — full CRUD, sorted by featured-first then month/day ascending
- Memories — approve or reject pending community submissions
- Lost Media — create and manage vault entries with status classification
- Retro Assets — manage downloadable asset library
- Site News — post and hide announcements shown on the homepage
Authentication uses iron-session with HTTP-only cookies and a secret admin URL. The session cookie is set on login and cleared on logout — a subtle bug where the logout route was creating the session on a different response object than was returned (causing the cleared cookie to be discarded) was the trickiest auth fix in the project.
SEO SETUP
- Dynamic
generateMetadataper meme and lost-media page (title, description, OpenGraph, Twitter card) - Dynamic OG images per meme via
/api/og-meme/[slug] sitemap.xmlauto-generated from all meme + lost-media slugs via Prismarobots.txtblocking admin path and API routes, pointing to sitemap- Canonical URLs on all dynamic pages
- JSON-LD
Articleschema on meme and lost-media detail pages - JSON-LD
WebSiteschema withSearchActionon homepage - Descriptive alt text via a shared
altText.tsutility (e.g. "Rickroll — viral internet meme from 2007, mid-2000s") - PageSpeed Insights: 100 SEO, 100 Best Practices on both mobile and desktop
WHAT THIS TAUGHT ME
1. CSS custom properties are the right tool for theming at scale
Three completely different OS aesthetics from a single component library — no conditional rendering, no theme context drilling. Just var(--skin-*) variables and a data-skin attribute on the root element. Every new component automatically inherits the correct theme.
2. Edge runtime has sharp edges
@vercel/og is powerful but has real constraints that only surface in production: no Buffer, no .webp, no boxShadow, no .map() in JSX. Developing and testing in localhost masked these completely. The lesson: for edge functions, test in a Vercel preview deployment early.
3. Cookie-based SSR is the correct solution for theme persistence
localStorage-based theming always causes a flash. The right mental model is: if you need something to affect the server-rendered HTML, it has to come from the server — and the only client data the server can see is cookies.
4. Full-stack solo projects reveal the gaps you didn't know you had
Building the admin CMS, the authentication system, the Redis hit counter, the OG image pipeline, the SEO layer, and the deployment config — all on the same project — forces you to develop opinions about every layer of the stack. That breadth is worth more than going deep on any single piece.
THE RESULT
A live, production site at oldwebdiary.com — fully deployed on Vercel with a custom domain, Supabase PostgreSQL database, Cloudflare R2 media storage, and Upstash Redis hit counter.
50+ memes documented. 50+ internet history events. 13 lost media items. 20 retro assets. A community memory wall. Three fully functional OS themes. Zero compromises on the nostalgia.
VISIT WEBSITE
Website Performance Optimization
25%+ PageSpeed improvements across multiple sites. Server-side rendering, code optimization, and modern UI/UX. Fast delivery without technical debt.
Splitlog — Personal Run Management Platform
I was tired of copy-pasting my Strava stats into Notion after every run. So I built my own platform to automate it. First full-stack project. First time with Next.js, Supabase, and real API integrations. The moment my run synced automatically? That's when it clicked.