Cover image for How I Built a Source-Tracked Game Wiki Network with Astro 5

shi0318

Most early game wikis have a trust problem. Pages mix official facts, beta discoveries, trailer guesses, and community assumptions without telling readers what is actually confirmed. I wanted to fix that for the games I follow, so I built a small network of source-tracked game wikis with Astro 5.

This post walks through the architecture, the content model, and the small tricks that make the whole network cheap to host and easy to update.

What I actually built

Six independent wikis, all on the same stack:

The repos are open source, so you can read the code and reuse the pattern:

  • github.com/shi0318/MortalShell2
  • github.com/shi0318/ValorMortis
  • github.com/shi0318/plaguetale
  • github.com/shi0318/beastreincarnationwiki
  • github.com/shi0318/graveseasons
  • github.com/shi0318/StarWarsGalacticRacer

Why Astro 5 for pre-release game wikis

For pre-release content, the priorities are clear: fast first paint, tiny JS, automatic sitemaps, predictable deploys, and content that is easy to write without opening a CMS. Astro 5 fits every one of those.


text
output: 'static',
trailingSlash: 'always',
integrations: [sitemap({ ... })],
vite: { plugins: [tailwindcss()] }
That's basically the whole config. No runtime, no Workers, no database. Pages render as plain HTML and ship straight from Cloudflare's edge.
The confidence-label content model
The thing that makes these wikis different from a static dump is a small content contract. Every guide file carries a status in its frontmatter:
status: official
status: beta
status: trailer
status: series
status: unconfirmed
A <StatusBadge> component renders that status as a visible label on every page. A <SourceTable> component lists the source URL, the date checked, and a note. So a reader can see at a glance whether a boss list is confirmed by Steam, inferred from a trailer, or a forum rumor.
In code that's just:
import { z } from 'astro:content';

const guideSchema = z.object({
  title: z.string().max(60),
  description: z.string().min(50).max(160),
  status: z.enum(['official', 'beta', 'trailer', 'series', 'unconfirmed']),
  sources: z.array(z.object({
    url: z.string().url(),
    date: z.date(),
    note: z.string(),
  })),
});
This buys two things. First, the site never silently publishes speculation as fact. Second, the same schema lets us gate pages out of the sitemap when the status is too weak to deserve a Google snippet.
Per-page sitemap lastmod from git
The neat trick I'm happiest with is per-page lastmod. Out of the box, Astro's sitemap integration defaults lastmod to build time. For a network of six wikis that re-deploys whenever any one page changes, that's a weak signal for Google.
So I override serialize to map each URL back to its source file, then ask git for that file's last commit:
import { execFileSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

function lastmodFor(url) {
  const slug = new URL(url).pathname.replace(/^\/+|\/+$/g, '');
  const candidates = slug === ''
    ? ['src/pages/index.astro']
    : [
        `src/pages/${slug}.astro`,
        `src/pages/${slug}/index.astro`,
        `src/content/guides/${slug}.md`,
        `src/content/guides/${slug}.mdx`,
      ];
  const file = candidates.find(rel => existsSync(join(ROOT, rel)));
  if (!file) return BUILD_TIME;
  try {
    return execFileSync(
      'git',
      ['log', '-1', '--format=%cI', '--', file],
      { cwd: ROOT, encoding: 'utf8' }
    ).trim();
  } catch {
    return BUILD_TIME;
  }
}

sitemap({
  serialize(item) {
    return { ...item, lastmod: lastmodFor(item.url) };
  },
})
Result: only the page you actually edited moves forward in time. Everything else stays put. That gives Google a clean recency signal without any CMS.
Filtering weak pages out of the sitemap
Speculation pages are useful for in-page exploration but shouldn't waste crawl budget. I keep a small allow-list of NOINDEX_URLS and filter the sitemap the same way the page meta tags are filtered:
const NOINDEX_URLS = ['/gloom/'];

sitemap({
  filter: page => !NOINDEX_URLS.includes(new URL(page).pathname),
})
So a page that's labeled trailer or unconfirmed can still exist for navigation, but only the verified pages compete for Google.
Why this network works for pre-release games
Three things make the pattern worth copying:
Status as a first-class concept. Pre-release content is moving. Modeling that explicitly in the content schema forces honest writing and lets you automate what's safe to publish.

Static everything. No DB, no API, no Workers. Cloudflare Pages handles the entire network for free.

Open source as a moat. Anyone can fork the repo, swap the data, and ship a wiki for their own niche game in a weekend. That's how a small network of six becomes a pattern.

If you want to try it on your own game, the simplest path is:
git clone https://github.com/shi0318/MortalShell2.git my-game-wiki
cd my-game-wiki
npm install
npm run dev
Then edit src/data/site.ts, drop your pages into src/content/guides/, and ship.
If you want to follow the network as it grows, the launch hub is Mortal Shell II Wiki. Feedback and PRs are welcome on any of the six repos.

Enter fullscreen mode Exit fullscreen mode