Game companion sites have a trust problem. A build guide can be correct on Tuesday, broken by Friday's patch, and still look authoritative three weeks later. If the data lives directly in React components, nobody notices until a reader points out the wrong unlock requirement or an expired code.
While building Lootr Tools, an unofficial Dungeon Lootr companion, I treated the content pipeline like a small production data system. The goal was simple: bad references, impossible dates, and unsupported drop-rate claims should fail before they reach the site.
Put the facts behind a data contract
Game facts do not belong inside JSX. Every class, aspect, item, build, code, and source lives in typed data modules. A small helper runs each object through a Zod schema at import time:
import { gameClassSchema } from "@/lib/schemas";
import type { GameClass } from "@/types";
export function defineClass(input: GameClass): GameClass {
return gameClassSchema.parse(input);
}
That gives editors normal TypeScript completion while still enforcing rules TypeScript alone cannot protect at runtime:
- slugs must be lowercase kebab-case;
- verification dates must use
YYYY-MM-DD; - drop rates must be decimals from 0 to 1;
- a drop rate cannot appear without a provenance status;
- query-filterable entities can be explicitly marked
noindex.
The last rule matters for fan sites. If an entity is known only by name, or two sources disagree about whether it is even a class, the page can remain useful without being sent to search engines as authoritative content.
Make source references part of the model
A source is not a footer decoration. It is a relationship in the dataset:
export interface SourceReference {
id: string;
title: string;
url: string;
sourceType: "official" | "community" | "editorial" | "unknown";
publisher?: string;
accessedAt: string;
}
Entities reference those source IDs. The UI can then render a real distinction between:
- official;
- community verified;
- conflicting sources;
- unverified.
More importantly, it can avoid claiming certainty the source does not have. A community drop estimate is useful, but it should not be presented with the same confidence as a first-party number.
Validate relationships, not just objects
A class schema can be valid while still pointing to an aspect that does not exist. So the data index performs a cross-reference check before the app uses the dataset:
assertValidDataset({
sources,
classes,
aspects,
items,
builds,
codes,
});
The validator checks for:
- duplicate entity IDs and slugs;
- builds referencing missing classes;
- classes recommending missing aspects;
- aspects recommending missing classes;
- entities citing missing sources;
- item rates outside the 0–1 range;
- item rates without a rate status.
This turns a common content mistake into a local failure with a precise message. A build pointing at a nonexistent class never becomes a production page.
Keep freshness static and honest
Dynamic pages should not manufacture freshness from new Date(). That would make every stale page look current.
Instead, each entity carries a verifiedAt date from the data file:
{
id: "cursed-king",
name: "Cursed King",
verifiedAt: "2026-09-16",
sourceIds: ["pro-game-guides", "gamepur-tier"],
verificationStatus: "verified"
}
The page renders that stored date. If nobody has rechecked the data, the page says so. The content may still be wrong, but it no longer pretends to be fresh.
This is especially useful for Roblox game codes. Two dated sources can disagree on the same day, so the data model allows a status of unknown rather than forcing a false active/expired choice.
Separate decisions from prose
Reference articles answer, "What is this?" Tools answer, "What should I do?"
For decision pages, the recommendation logic is deterministic and testable. The Build Finder scores candidate builds by class, goal, source confidence, current-update compatibility, and aspect match. If no exact build exists, it can fall back to another goal and lower the confidence. If no build exists at all, it derives a conservative plan from class basics instead of inventing gear advice.
Probability tools are also pure functions:
chanceAtLeastOneDrop(p, runs);
runsForTarget(p, target);
planFarm({
dropRate,
requiredSuccesses,
averageRunMinutes,
confidence,
});
Keeping the math outside React makes edge cases easier to test: zero percent drops, hundred percent drops, tiny rates across huge run counts, and unreachable confidence targets.
The result
This approach does not solve game research—it constrains the damage when research is incomplete. The production site still needs regular rechecks, but common content failures now happen earlier:
Invalid game data:
- Build "example-build" references unknown class "does-not-exist"
- Item "example-item" sets a dropRate without dropRateStatus
That is much better than publishing a polished page with a broken relationship.
I'm using this model on Lootr Tools, where it supports class comparisons, aspect references, item drop sources, update notes, code status, a Build Finder, and farming calculators. It is an unofficial fan project and is not affiliated with Roblox or ClickBytes, but the content model still tries to hold itself to a production standard.
Top comments (0)