Live site: gaming.tyvan.dev

Gaming Feed dashboard

Why I Built This

I read gaming news more often than I should probably admit in a professional setting.

The problem is not that there is no gaming news. The problem is the opposite. There is too much of it, scattered across different publishers, feeds, launchers, communities, and whatever tab I opened three days ago and now refuse to close because “I might need it later”.

So I wanted a small web app with one job:

  • show latest gaming articles
  • keep them easy to scan
  • preserve the original publisher links
  • avoid turning the homepage into a casino lobby with CSS

That became Gaming Feed, a simple Next.js app backed by an existing PostgreSQL table of RSS-style articles.

The Shape of the App

The app is intentionally narrow. It is not trying to be a social network, recommendation engine, or “AI-powered engagement platform”, which is usually a normal app wearing a very expensive hat.

The main page shows a readable feed with:

  • article titles
  • summaries
  • sources
  • authors
  • published dates
  • cover images
  • source filters
  • search
  • pagination

Each article opens the original URL in a new browser tab. I do not want to trap the reader inside my app. The publishers did the reporting, so the app should point people back to them.

Tech Stack

The final stack is:

  • Next.js 15 with the App Router
  • React 19
  • TypeScript
  • Tailwind CSS
  • PostgreSQL with pg
  • Zod for query parsing
  • date-fns for formatting dates
  • lucide-react for icons

The original design considered Prisma, but I ended up using direct PostgreSQL queries through pg.

For this project, that made more sense. The database already existed, the article table already had data, and I did not need a migration layer just to ask Postgres, “hey, got any news about games where people yell at frame rates?”

Data Model

The app expects RSS-style article records.

The useful fields are:

  • source identifier
  • external article identifier
  • title
  • original URL
  • summary
  • author
  • image URL
  • publication time
  • fetch time

The default table is:

public.rss_articles

The app maps the database column img_url to imageUrl in the UI layer.

There is also an ARTICLES_TABLE environment variable, so the table can be changed without editing code. Since dynamic table names can get sketchy fast, the implementation validates and quotes the table name before putting it into SQL.

That is the kind of boring detail that saves future me from becoming present me’s unpaid incident responder.

Querying Articles

The article query layer handles the feed behavior:

  • default page size of 20
  • maximum page size of 50
  • source filtering
  • search across title and summary
  • stable ordering by publish time, then fetch time
  • total count for pagination

The ordering is:

published_at DESC NULLS LAST, fetched_at DESC

This keeps newer articles first, while still giving articles without a publish date a reasonable place in the list.

Search currently uses ILIKE. It is simple, readable, and good enough for the current article volume.

If the database grows into a dragon hoard of gaming headlines, I would probably move this to PostgreSQL full-text search or trigram indexes. For now, ILIKE does the job without asking me to open a second browser tab and question my life choices.

UI Design

I wanted the interface to feel like a compact feed, not a landing page trying to sell me my own keyboard.

The main screen has:

  • a branded R/PCGAMING header link
  • a search form
  • source filter chips with article counts
  • article cards with thumbnails
  • cleaned summaries
  • source labels
  • author and date metadata
  • newer/older pagination controls
  • empty and loading states
  • a database error state

The article cards are the important part. They need to be easy to scan on desktop and still usable on mobile.

Images use the article image when available. If an article does not have one, the app falls back to recognizable source images for known sources like IGN, PC Gamer, and Steam.

RSS summaries can sometimes arrive with HTML fragments, entities, and other tiny bits of content confetti. The app cleans those summaries before rendering them so the feed does not look like someone sneezed into an XML parser.

API Routes

I also added two API routes:

GET /api/articles
GET /api/sources

/api/articles accepts the same query parameters as the main page:

page
limit
source
q

It returns article data plus pagination metadata.

/api/sources returns the available sources with display labels and article counts.

The page itself is server-rendered, but having API routes gives me a clean path for future client-side features or external integrations.

Database Configuration

Database access is centralized in lib/db.ts.

The app reads DATABASE_URL from the environment and supports a few practical PostgreSQL connection options:

  • schema, mapped to the PostgreSQL search path
  • sslmode, used to disable SSL when needed
  • sslaccept=accept_invalid_certs, used for trusted environments with invalid certs

There is also a small database check script:

npm run db:check

It loads .env if present, connects to PostgreSQL, applies the same schema and SSL handling as the app, and prints basic database context plus the article count.

This is one of those scripts that looks small until the day something breaks. Then it becomes a tiny flashlight in the cave.

Docker

The production image uses a multi-stage Dockerfile:

  • install dependencies with npm ci
  • build the Next.js app
  • copy the standalone output
  • run as a non-root user

Next.js standalone output keeps the runtime image smaller and simpler. The app listens on port 3000, which is then exposed through Kubernetes.

The container is not fancy. It builds the app, runs the app, and tries very hard not to bring extra drama to the cluster. Respectable behavior.

Kubernetes

The deployment manifests include:

  • namespace
  • deployment
  • service
  • ingress
  • secret-based DATABASE_URL
  • optional ARTICLES_TABLE
  • readiness probe
  • liveness probe
  • basic CPU and memory requests/limits

The app is stateless, so it does not need a PersistentVolume. The only persistent part is PostgreSQL, which lives outside this app.

Traffic follows the usual path in my cluster:

Browser
  -> Cloudflare
  -> Traefik
  -> Gaming Feed service
  -> Next.js pod
  -> PostgreSQL

The public app URL is:

https://gaming.tyvan.dev

Final Result

The project now gives me a small gaming news feed with:

  • server-rendered browsing
  • PostgreSQL-backed article loading
  • search
  • source filtering
  • pagination
  • source-aware image fallbacks
  • original publisher links
  • JSON API endpoints
  • Docker packaging
  • Kubernetes deployment manifests
  • a database connectivity check

It is not trying to reinvent news. It is just a clean place to read gaming headlines without wrestling fifteen tabs and a launcher that somehow needs an update before showing me patch notes.

What I Would Improve Later

A few next steps are obvious:

  • add automated tests for query parsing and API serialization
  • add a .env.example
  • replace external fallback images with local assets
  • add full-text search if the article table gets large
  • add CI for linting, building, and publishing the container image
  • replace placeholder Kubernetes image names with the final registry path

For now, the important thing is that the app works, the feed is readable, and I have one more personal service running happily on the cluster.

Which means, naturally, I will soon find another tiny problem and deploy an entire app for it. This is how it starts.