Live site: gaming.tyvan.dev

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/PCGAMINGheader 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 pathsslmode, used to disable SSL when neededsslaccept=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.