feat(demo): list the demo's sign-ins on /beta and link into it per game
Some checks failed
PR checks / checks (pull_request) Failing after 2m16s
Some checks failed
PR checks / checks (pull_request) Failing after 2m16s
The public demo (demo.runicgateway.com) is live, so the site's demo slot can be switched on. Two decisions of record, PLAN.md §15: - D82: /beta reads the demo's GET /api/v1/public/settings demo.logins on the server, caches it for a minute, and lists the published sign-ins with the next wipe. If the demo does not answer, /beta links to its sign-in page instead. Gate two reads as in place. - D83: a deep link into a game module's pages carries data-demo-game, and applyBrand.mjs fills it only for games in the new mounted demoGames list. The Rust features gain links into /rust and /rust/servers/demo. UO's stay hidden on a Rust demo. The public-demo not-built entry is removed, because its resolvedBy was met. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -193,17 +193,38 @@ if (demoFrom !== demoTo) {
|
||||
* because `global.css` hides `[data-demo-url='']` — the visibility rule stays one rule for
|
||||
* both kinds of link, and only the `href` differs.
|
||||
*/
|
||||
const DEEP_LINK = /href="[^"]*" data-demo-url="[^"]*" data-demo-path="([^"]*)"/g;
|
||||
const DEEP_LINK = /href="[^"]*" data-demo-url="[^"]*" data-demo-path="([^"]*)"(?: data-demo-game="([^"]*)")?/g;
|
||||
|
||||
const deepLinkTo = (demoPath) => {
|
||||
const href = demoTo ? `${demoTo.replace(/\/+$/, '')}${demoPath}` : '';
|
||||
/**
|
||||
* Which games the demo runs (D83). A deep link into a game module's pages carries that
|
||||
* game's id in `data-demo-game`, and it is filled only when the mounted `demoGames` names
|
||||
* it: the demo is a Rust deployment, and a `/uo/market` link into it is a 404. Core's own
|
||||
* links (`/wiki`, `/site/events`) carry no game and follow `demoUrl` alone, because every
|
||||
* deployment has them.
|
||||
*
|
||||
* An absent or malformed list means no game's links, not every game's. A demo whose games
|
||||
* nobody named is one whose game links cannot be known to work.
|
||||
*/
|
||||
const gamesOf = (value) => (Array.isArray(value) ? value.filter((id) => typeof id === 'string') : []);
|
||||
const gamesFrom = gamesOf(previous.demoGames);
|
||||
const gamesTo = gamesOf(resolved.demoGames);
|
||||
const gamesChanged = gamesFrom.join(',') !== gamesTo.join(',');
|
||||
|
||||
if ('demoGames' in mounted && !Array.isArray(mounted.demoGames)) {
|
||||
console.warn('[brand] the mounted "demoGames" is not a list of game ids — no game links will show.');
|
||||
}
|
||||
|
||||
const deepLinkTo = (demoPath, game) => {
|
||||
const root = !game || gamesTo.includes(game) ? demoTo : '';
|
||||
const href = root ? `${root.replace(/\/+$/, '')}${demoPath}` : '';
|
||||
return (
|
||||
`href="${escapeHtml(href)}" data-demo-url="${escapeHtml(demoTo)}" ` +
|
||||
`data-demo-path="${demoPath}"`
|
||||
`href="${escapeHtml(href)}" data-demo-url="${escapeHtml(root)}" ` +
|
||||
`data-demo-path="${demoPath}"` +
|
||||
(game === undefined ? '' : ` data-demo-game="${game}"`)
|
||||
);
|
||||
};
|
||||
|
||||
if (!replacements.length) {
|
||||
if (!replacements.length && !gamesChanged) {
|
||||
console.log('[brand] mount matches what is already applied; nothing to rewrite.');
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -303,8 +324,8 @@ for (const file of walk(CLIENT)) {
|
||||
// After the literal swaps, never before: the plain-slot replacement also matches the
|
||||
// first two attributes of a deep link, so it runs first and this pass corrects the
|
||||
// `href` it just wrote. Recomputing rather than replacing is what makes that safe.
|
||||
after = after.replace(DEEP_LINK, (whole, demoPath) => {
|
||||
const rebuilt = deepLinkTo(demoPath);
|
||||
after = after.replace(DEEP_LINK, (whole, demoPath, game) => {
|
||||
const rebuilt = deepLinkTo(demoPath, game);
|
||||
if (rebuilt !== whole) counts.set('demoDeep', counts.get('demoDeep') + 1);
|
||||
return rebuilt;
|
||||
});
|
||||
@@ -330,6 +351,9 @@ if (counts.get('demoDeep')) {
|
||||
` ${'demoUrl deep'.padEnd(14)} ${demoTo ? `linked -> ${demoTo}/…` : 'links hidden'} (${counts.get('demoDeep')}x)`
|
||||
);
|
||||
}
|
||||
if (gamesChanged) {
|
||||
console.log(` ${'demoGames'.padEnd(14)} ${gamesTo.length ? gamesTo.join(', ') : 'no game links'}`);
|
||||
}
|
||||
|
||||
if (inlineCollisions.length) {
|
||||
console.error(
|
||||
|
||||
@@ -32,6 +32,7 @@ import path from 'node:path';
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { classify } from '../src/lib/brandAssets.mjs';
|
||||
import { games } from '../src/data/games/index.mjs';
|
||||
|
||||
const ROOT = fileURLToPath(new URL('..', import.meta.url));
|
||||
const DEFAULTS = path.join(ROOT, 'brand-default');
|
||||
@@ -157,6 +158,20 @@ if (brand) {
|
||||
}
|
||||
}
|
||||
|
||||
// D83: the one list-valued field. Each entry is a game module's id, checked against the
|
||||
// games this site describes, so a typo is a failed build rather than a demo whose game
|
||||
// links silently never appear.
|
||||
const gameIds = games.map((game) => game.key);
|
||||
if (!Array.isArray(brand.demoGames)) {
|
||||
fail('brand.json is missing the list field "demoGames" (an empty list is the stock value).');
|
||||
} else {
|
||||
for (const id of brand.demoGames) {
|
||||
if (!gameIds.includes(id)) {
|
||||
fail(`brand.json's "demoGames" names ${JSON.stringify(id)}, which is not one of ${gameIds.join(', ')}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kept in step with `TEXT_FIELDS` in `applyBrand.mjs` by reading that file rather than by
|
||||
* restating the list. A field added there and forgotten here would be unchecked; a field
|
||||
@@ -348,6 +363,23 @@ if (!deepPattern) {
|
||||
);
|
||||
}
|
||||
|
||||
// A game's link (D83) must also be read WITH its game. A pattern that matched the first
|
||||
// three attributes and dropped the fourth would treat a /uo/ link as a core one and fill
|
||||
// it on every demo, which is the 404 the attribute exists to prevent.
|
||||
const gameSample = `${sample} data-demo-game="rust"`;
|
||||
let gameRead = null;
|
||||
try {
|
||||
gameRead = new RegExp(deepPattern[1]).exec(gameSample)?.[2] ?? null;
|
||||
} catch {
|
||||
// Reported above.
|
||||
}
|
||||
if (gameRead !== 'rust') {
|
||||
fail(
|
||||
`applyBrand.mjs's DEEP_LINK does not read the game out of \`${gameSample}\`.\n` +
|
||||
' Game links would follow demoUrl alone and point into modules the demo may not run.'
|
||||
);
|
||||
}
|
||||
|
||||
const deepStrays = [];
|
||||
let deepLinks = 0;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user