# GamesJi Hub

An automatically-maintained gaming knowledge base: game pages are discovered from
IGDB/RAWG, written by an AI content pipeline, and only published if they pass an
automated quality gate. There is no admin panel and no login — the system is
operated through config files and scheduled jobs, not a dashboard.

This README explains what's actually here, what's deliberately deferred, and how
the pieces fit together, so you're not discovering any of that in production.

## Why it's built this way

This project exists because of two constraints that are easy to state and easy to
violate by accident:

1. **Google's spam policy explicitly targets this pattern of site** — large
   volumes of AI-generated or scraped pages published with little added value,
   *regardless of intent*. See ["Scaled content
   abuse"](https://developers.google.com/search/docs/essentials/spam-policies#scaled-content-abuse).
   Every generated page goes through `src/lib/content/qualityGate.ts` before it's
   allowed to exist publicly, specifically to stay out of that bucket.
2. **"No CMS" doesn't mean "no way to fix mistakes."** It means the fix
   mechanism is `config/overrides.json` plus scheduled jobs, instead of a
   dashboard. See "Fixing content without a CMS" below — this is a real,
   exercised code path, not a caveat.

## Architecture

```
config/
  overrides.json       <- the "admin panel": excludes, corrections, force-regen
  rss-sources.json      <- editable list of RSS feeds to ingest

prisma/schema.prisma     <- the whole data model + state machine (read this first)

src/lib/igdb/            <- IGDB client (primary metadata source)
src/lib/rawg/             <- RAWG client (secondary/supplemental source)
src/lib/content/
  prompts.ts              <- the actual anti-spam mechanism (see below)
  generator.ts             <- calls Claude via tool-forced structured output
  qualityGate.ts            <- scores generated content, blocks thin/duplicate pages
  mapper.ts                  <- IGDB/RAWG raw data -> normalized GameFacts
  schema.ts                   <- JSON-LD builders (VideoGame, FAQPage, Breadcrumb, ItemList)
src/lib/rss/ingest.ts     <- fetches feeds, dedups, rewrites, links to games
src/lib/config/overrides.ts <- typed loader for config/overrides.json

src/app/api/cron/          <- the four background jobs (see below)
src/app/games/[slug]/       <- the game page template
src/app/games/                <- searchable/filterable directory
src/proxy.ts                    <- security headers, CSP nonce, rate limiting
```

### The four background jobs

Nothing runs continuously; each job does one pass and returns. Trigger them on a
schedule (Vercel Cron via `vercel.json`, or `scripts/run-cron.ts` from system cron
on a VPS — both call the same routes).

| Job | Route | What it does |
|---|---|---|
| `discover-games` | `/api/cron/discover-games` | Pages through IGDB by popularity, cursor persisted in `SyncCursor` so it makes forward progress and wraps around — this is "continuously discover new games," not a one-shot dump. |
| `generate-content` | `/api/cron/generate-content` | Picks up `DISCOVERED`/`NEEDS_REFRESH`/force-regenerate games (batch of 8/run — bounds Anthropic spend), generates content, runs the quality gate, publishes or holds. |
| `refresh-stale` | `/api/cron/refresh-stale` | Re-checks published games against IGDB after 30 days; flags real changes for regeneration. Also syncs `excludedSlugs` from the override file. |
| `fetch-rss` | `/api/cron/fetch-rss` | Ingests configured feeds, rewrites items into original one-line summaries, links them to games, flags linked games for refresh. |

All four are plain authenticated HTTP endpoints (`Authorization: Bearer
$CRON_SECRET`) — see `src/lib/cron/auth.ts`. Every run is logged to the `JobRun`
table, which is your activity log in place of a CMS's — query it with `npx prisma
studio` or any SQL client.

## The three-layer anti-spam design

1. **Grounded prompts** (`src/lib/content/prompts.ts`) — the model only ever sees
   structured facts (name, dates, genres, numbers), never another site's article
   text. There's nothing to closely paraphrase because it's never shown source
   prose, and it's explicitly told to write short/honest sections instead of
   padding when facts are thin.
2. **The quality gate** (`src/lib/content/qualityGate.ts`) — length thresholds,
   generic-filler-phrase detection, and a shingle-based near-duplicate check
   against recently published pages in the same genre. Anything that doesn't
   clear `PASS_THRESHOLD` goes to `PENDING_REVIEW`, not live.
3. **Source-sufficiency gating** (`sourceDataIsSufficientForPublish`) — if IGDB/RAWG
   don't have enough real facts yet (no summary, fewer than 2 of
   {genres, platforms, companies, release date}), the page isn't generated at
   all. It's retried automatically as source data improves — this is the
   "expand pages over time" behavior instead of "everything, immediately."

None of this guarantees every page is great — it's a heuristic floor, not an
editor. See "Known limitations" below for what it doesn't catch.

## Fixing content without a CMS

Edit `config/overrides.json`, commit it. The next relevant job run picks it up —
usually within 15-20 minutes, not a redeploy.

```jsonc
{
  "excludedSlugs": ["some-bad-page"],           // de-indexed & skipped forever
  "forceRegenerate": ["gta-6"],                  // rebuilt on next generate-content run
  "pinnedFacts": {
    "gta-6": { "releaseDate": "2026-11-19" }     // overrides whatever IGDB/RAWG say
  },
  "featuredSlugs": ["minecraft", "gta-6"]
}
```

A published page that fails a *re*-generation (e.g. after `refresh-stale` flags
it) is **not** taken down — `generate-content` only overwrites stored content
when the new version actually passes the gate, so the last good version keeps
serving while a fix is worked out. See the long comment on `NEEDS_REFRESH` in
`prisma/schema.prisma` if you're modifying this behavior; it's easy to
accidentally reintroduce a bug where a bad regeneration nukes a live page.

## Setup

```bash
cp .env.example .env    # fill in the values below
npm install
npx prisma migrate dev  # creates the database schema
npm run dev
```

Required environment variables (see `.env.example` for the full list with
context):

- `DATABASE_URL` — Postgres connection string.
- `IGDB_CLIENT_ID` / `IGDB_CLIENT_SECRET` — from a [Twitch dev
  app](https://dev.twitch.tv/console/apps); IGDB auth rides on Twitch's OAuth.
- `RAWG_API_KEY` — from [rawg.io/apidocs](https://rawg.io/apidocs).
- `ANTHROPIC_API_KEY` — for content generation.
- `CRON_SECRET` — generate with `openssl rand -hex 32`.

**Before going live:** IGDB's free tier is for non-commercial use, and RAWG's
free commercial tier caps at 100k MAU / 500k pageviews per month with a
required attribution link (already included in the site footer and
`SourceCredit`). Since this site runs ads, sort out commercial terms with both
before launch — contacts are in `.env.example`.

## Deploying

**Vercel:** push the repo, set the env vars above in the project settings,
`vercel.json` wires up the four cron schedules automatically (Vercel injects
`Authorization: Bearer $CRON_SECRET` on cron-triggered requests when
`CRON_SECRET` is set as a project env var — confirm this against [Vercel's
current cron docs](https://vercel.com/docs/cron-jobs) before relying on it, in
case it's changed).

**VPS (Nginx, etc.):** `npm run build && npm run start` behind Nginx as a
reverse proxy, then point system cron at `scripts/run-cron.ts` — see the
comment at the top of that file for exact crontab lines.

## Known limitations (read before scaling this up)

This is a solid, real Phase 1 — not a demo — but it's honest to flag what it
doesn't do yet:

- **Guide-heavy sections (tips/strategy) are genre-level, not verified
  game-specific expertise.** IGDB/RAWG don't provide actual mechanics data, so
  `tipsAndTricks`/`howToPlay` are prompted to stay general rather than invent
  specifics — which avoids fabrication, but means they're thinner than a human
  guide-writer's work. Spot-check these for any game where precision actually
  matters before you rely on them.
- **The near-duplicate check is a cheap shingle-sketch heuristic, not real
  embeddings.** Fine for catching "obviously the same template," not a rigorous
  similarity measure. Swap in a vector index if false negatives become a
  problem at scale.
- **RSS source URLs in `config/rss-sources.json` are best-effort** — this
  environment had no outbound network access while building the project, so
  they were never fetched live. Verify each one resolves before relying on it.
- **Filtered directory views (`/games?genre=...`) are `noindex`** by design (to
  avoid a faceted-navigation thin-content pattern), but they're still fully
  usable and linked for visitors — only the canonical `/games` and individual
  game pages are meant to rank.
- **The PWA layer is a real but minimal service worker** (installability +
  offline fallback page), not a full offline-first catalog cache.
- **IGDB's `age_ratings`/`category` numeric enums are mapped best-effort** —
  see comments in `src/lib/content/mapper.ts` and `src/lib/igdb/client.ts`;
  confirm against IGDB's live enum reference if this becomes compliance-relevant.
- **Icon files referenced in `public/manifest.json` don't exist yet** —
  generate real `icon-192.png` / `icon-512.png` / `icon-512-maskable.png` and
  drop them in `public/icons/`.

## Phase 2 candidates (not built, on purpose — see the original scoping discussion)

- Dedicated `/news` blog listing + pagination (news currently surfaces per-game
  only, via "Latest Updates")
- Genre/platform hub pages beyond directory filters
- Real embedding-based duplicate detection
- Distributed rate limiting via Upstash (env vars are already wired up in
  `src/proxy.ts`, just unset by default)
- Monitoring/alerting on `JobRun` failures (right now it's query-it-yourself)
- i18n

## Design system: "Arcade HUD"

Dark-first glassmorphism drawn from in-game stat overlays rather than generic
SaaS gradients: notched-corner glass panels (`clip-notch` / `clip-notch-sm`,
`tailwind.config.ts`) instead of rounded cards, a HUD-style rating ring instead
of a star row, and `Rajdhani` (technical/esports-adjacent display face) +
`JetBrains Mono` with tabular numerals for every stat/data value, paired with
`Inter` for long-form readability in the actual article content. Full token
list and rationale are commented in `tailwind.config.ts` and
`src/app/globals.css`.
