If you have tried looking up a boss strategy or character build on a modern gaming wiki recently, you know the pain.
You open a page on your phone, and before the first sentence renders:
- Two sticky video overlays dock themselves to the corners of your screen.
- A full-screen interstitial ad hijacks your tap.
- The page layout violently jumps 400 pixels down (Cumulative Layout Shift at its finest) as five different ad networks race to auction your attention.
- Total transferred payload? 18 MB. Total DOM nodes? 6,500+. Time to Interactive? 11 seconds.
When Bit Reactor and EA announced STAR WARS Zero Company—a tactical turn-based RPG built by veteran XCOM developers—I decided to take a stand. I didn't want another bloated wiki. I wanted a lightning-fast, zero-bloat tactical database that loads in under 200ms, respects battery life, and provides verified gameplay theorycrafts.
Here is the engineering breakdown of how I built Zero Company Guide with Next.js 16, TypeScript, and a strict pre-build validation pipeline.
1. The Core Architecture: Data as Code
Most gaming databases rely on a heavy Headless CMS, WordPress, or an unstructured MediaWiki backend. For a focused tactical guide, all of those introduce runtime database latency, cache-invalidation bugs, and hosting costs.
Instead, I treated all game entities—operators, skills, story missions, bonding matrices, and build synergies—as strict TypeScript structures:
// src/data/operators.ts
export interface Operator {
id: string;
name: string;
callsign?: string;
role: "Vanguard" | "Commando" | "Infiltrator" | "Guardian" | "Tech Specialist";
faction: "Republic" | "Underground" | "Mercenary";
baseStats: {
hp: number;
mobility: number;
willpower: number;
baseAdvantage: number;
};
signatureAbility: {
name: string;
description: "string;"
cooldownTurns: number;
};
}
export const operators: readonly Operator[] = [
// Fully typed, validated at compile-time
] as const;
Why Data-as-Code Wins:
-
Zero Runtime Query Overhead: All data is statically compiled into static HTML/JSON during
next build. - Refactoring Safety: Changing a property name in an interface flags every affected component and view at compile time across the entire codebase.
- Git as Single Source of Truth: Community PRs for patch balance updates can be code-reviewed like regular software changes.
2. The Pre-Build Defensive Pipeline
One of the biggest problems with gaming guides is stale data and dead links. As a game goes through balance patches, guides quietly rot.
Rather than relying on human memory, I wired up an automated pre-build test gate in package.json:
{
"scripts": {
"prebuild": "node scripts/gen-content-dates.mjs && node scripts/audit-data.mjs && node scripts/audit-trend-coverage.mjs && node scripts/audit-links.mjs && node scripts/audit-canonical.mjs",
"build": "next build"
}
}
Here is what these standalone audit scripts enforce before any deployment can succeed:
A. Freshness & Anachronism Guard (audit-data.mjs)
Guarantees that no developer accidentally sets a future timestamp on a guide and flags any data collection older than 14 days for manual review:
// scripts/audit-data.mjs
const todayStr = new Date().toISOString().slice(0, 10);
for (const m of txt.matchAll(/"(\/[^"]*)":\s*"(\d{4}-\d{2}-\d{2})"/g)) {
const [, route, date] = m;
if (date > todayStr) {
console.error(`[audit-data] ❌ Future date detected: ${route} = ${date}`);
process.exitCode = 1;
}
}
B. Trailing-Slash Canonical Auditor (audit-canonical.mjs)
A subtle SEO bug in static hosting (like Cloudflare Pages or AWS S3) is the redirect loop between /page and /page/.
Our auditor recursively parses all static output HTML files in out/, ensuring:
- Every
<link rel="canonical">has a deterministic trailing slash. - OpenGraph
og:urlmatches the canonical URL 1:1. -
sitemap.xmlhas 0 missing routes and 0 duplicate canonicals.
// Verifies out/**/*.html for canonical integrity
const canonicalMatch = html.match(/<link rel="canonical" href="([^"]+)"/);
if (!canonicalMatch || !canonicalMatch[1].endsWith("/")) {
throw new Error(`Invalid canonical in ${file}: must end with a trailing slash!`);
}
3. Media Facade: Eliminating Third-Party Embed Bottlenecks
Embedding YouTube gameplay analysis or trailer clips normally kills your PageSpeed score. A single standard <iframe> loads ~1.2 MB of scripts, fonts, and tracking beacons from Google before the user even clicks play.
We solved this with a Video Facade Component:
// src/components/YouTubeEmbed.tsx
"use client";
import { useState } from "react";
import Image from "next/image";
export default function YouTubeEmbed({ videoId, title, posterUrl }: Props) {
const [isPlaying, setIsPlaying] = useState(false);
if (!isPlaying) {
return (
<div
className="group relative aspect-video w-full cursor-pointer overflow-hidden rounded-lg bg-surface"
onClick={() => setIsPlaying(true)}
>
<Image
src={posterUrl}
alt={title}
fill
sizes="(max-width: 768px) 100vw, 800px"
className="object-cover transition-transform duration-300 group-hover:scale-105"
loading="lazy"
/>
<button
aria-label={`Play ${title}`}
className="absolute inset-0 m-auto flex h-14 w-14 items-center justify-center rounded-full bg-primary/90 text-white shadow-xl transition-all group-hover:scale-110"
>
▶
</button>
</div>
);
}
return (
<iframe
className="aspect-video w-full rounded-lg"
src={`https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1`}
title={title}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
);
}
Result:
- Initial page load payload dropped from 2.1 MB to 84 KB.
- Total Blocking Time (TBT) dropped from 380ms to 0ms.
4. Dark Tactical UI with Zero CLS
For visual styling, I chose Tailwind CSS with custom HSL tokens to evoke a high-contrast Republic tactical terminal.
Typography matters: we paired Orbitron for tactical headings and Inter for dense, readable tabular data, loaded via next/font/google with display: 'swap' and automated CSS variable fallbacks to eliminate Flash of Unstyled Text (FOUT).
/* Custom tactical design tokens */
:root {
--bg-primary: #0a0d14;
--surface: #121824;
--borderline: #1e293b;
--accent-cyan: #38bdf8;
--accent-gold: #f59e0b;
}
Every card, table, and data grid has explicit CSS container sizes or aspect-ratio locks. When the page renders on mobile, there is 0 Cumulative Layout Shift (CLS score: 0.00).
5. The Performance Scorecard
Here is the Lighthouse audit for the production site:
| Metric | Industry Standard Wiki | Zero Company Guide |
|---|---|---|
| Page Weight (Initial) | 14.8 MB | 68 KB |
| HTTP Requests | 120+ | 9 |
| Performance Score | 24 / 100 | 100 / 100 |
| Largest Contentful Paint (LCP) | 4.8s | 0.5s |
| Cumulative Layout Shift (CLS) | 0.38 | 0.00 |
| Total Blocking Time (TBT) | 1,420ms | 0ms |
Key Takeaways
- Stop defaulting to dynamic servers for static content: 95% of gaming databases do not need server-side rendering or database queries on every HTTP hit. SSG is cheaper, faster, and un-crashable.
-
Build your audit pipeline into
prebuild: Don't rely on manual QA to catch broken links, trailing slash redirect bugs, or missing metadata. Automate it with simple Node.js scripts. - Facade patterns are mandatory for embeds: Never load raw third-party iframes on page load. A static WebP placeholder with a click-to-load handler saves megabytes of unnecessary client bandwidth.
If you are a tactics fan or just want to see the UI in action, check out the live site at starwarszerocompany.blog.
Happy to answer any questions about the pre-build pipeline or SSG architecture in the comments below!
Top comments (0)