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:
- Mortal Shell II Wiki — verified guides for Mortal Shell II, Open Beta notes, Shells, weapons, bosses
- Valor Mortis Demo Guide — source-tracked demo guide for the One More Level soulslike
- Resonance: A Plague Tale Legacy Guide — A Plague Tale Legacy story, characters, combat, walkthrough planning
- Beast of Reincarnation Wiki — Game Freak's action RPG, Emma and Koo, bosses, builds
- Grave Seasons Guide — Ashenridge farming murder-mystery, romance, walkthrough
- Star Wars Galactic Racer Wiki — pre-release Star Wars racing wiki with confidence labels
The repos are open source, so you can read the code and reuse the pattern:
github.com/shi0318/MortalShell2github.com/shi0318/ValorMortisgithub.com/shi0318/plaguetalegithub.com/shi0318/beastreincarnationwikigithub.com/shi0318/graveseasonsgithub.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
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.