A zero-config registry: adding a game means adding a directory
App lists, detail routes, the home page picks and the sitemap are all discovered by import.meta.glob. The price is Vite's hard rule: literal relative paths only.
This site has twelve games and twenty tools. When I added the thirty-third app, I changed no configuration at all.
A three-line contract
Every app is a self-contained directory:
src/games/<slug>/
meta.ts default-exports AppMeta (localized title/description, order, featured, fullscreen)
App.vue default-exports a Vue component
logic/ pure logic, no DOM, runnable by the test suite directly
The directory name is the slug, which is the URL segment.
How the registry finds them
const gameMeta = import.meta.glob('../games/*/meta.ts', { eager: true });
function collect(kind, modules) {
return Object.entries(modules)
.map(([path, mod]) => {
const slug = path.split('/').slice(-2)[0];
if (!mod.default) throw new Error('registry has no default export: ' + path);
return { kind, slug, ...mod.default };
})
.filter((entry) => !entry.draft)
.sort((a, b) => (a.order ?? 999) - (b.order ?? 999) || a.slug.localeCompare(b.slug));
}
Three details matter. The slug comes from a path regex rather than an out-of-band file name. A missing default export throws, because failing the build beats shipping a missing card. And sorting depends only on order and the slug, never on filesystem iteration order.
One source, three consumers
The list page, the detail route and the home page picks all derive from the same listApps(kind). Routes are generated for three locales in appDetailPaths(kind), the list builds cards from that array, and the home page takes the first three featured entries.
With a single source, the answer to where do I register a new app is: nowhere.
The rule you cannot dodge
Vite can only statically analyse literal paths at build time. So the glob pattern cannot become an alias, and it cannot move into a variable. This is one of exactly two places here where a relative path is mandatory.
The price
The registry eagerly imports every meta.ts at build time. That is fine, since they are tens of lines each. The apps themselves stay lazy, loaded through a different glob and a runtime import().
Zero config depends on a stable contract. The moment one app needs extra build-time information, that information should not be stuffed into meta.ts; the question is whether every app deserves that field.

Comments
…