Add clean program page URLs

This commit is contained in:
Christian Johannesen 2026-10-07 21:59:45 +02:00
parent fbaf88941c
commit 911eeb0685
3 changed files with 40 additions and 16 deletions

View file

@ -9,16 +9,16 @@ machine-language payload is required.
| Program | Type | Controls | Description | Run locally |
| --- | --- | --- | --- | --- |
| [Meteor Dodge](meteor-dodge/meteor-dodge.bas) | Arcade | `A`, `D`, `Q` | Dodge falling meteors and build your score. | [Run program](http://localhost:8134/?program=meteor-dodge%2Fmeteor-dodge.prg) |
| [Code Breaker](code-breaker/code-breaker.bas) | Logic | `0`-`9`, `Q` | Deduce a four-digit secret in ten guesses. | [Run program](http://localhost:8134/?program=code-breaker%2Fcode-breaker.prg) |
| [Submarine Hunt](submarine-hunt/submarine-hunt.bas) | Strategy | Row, Column, `Q` | Find three hidden submarines with limited sonar shots. | [Run program](http://localhost:8134/?program=submarine-hunt%2Fsubmarine-hunt.prg) |
| [Lunar Lander](lunar-lander/lunar-lander.bas) | Simulation | `0`-`9`, `Q` | Choose engine burns and touch down gently. | [Run program](http://localhost:8134/?program=lunar-lander%2Flunar-lander.prg) |
| [Memory Matrix](memory-matrix/memory-matrix.bas) | Memory | `1`, `2`, `3`, `4`, `Q` | Repeat a growing sequence of flashing quadrants. | [Run program](http://localhost:8134/?program=memory-matrix%2Fmemory-matrix.prg) |
| [Robot Maze](robot-maze/robot-maze.bas) | Puzzle | `W`, `A`, `S`, `D`, `Q` | Navigate a freshly generated maze to its exit. | [Run program](http://localhost:8134/?program=robot-maze%2Frobot-maze.prg) |
| [Music Box](music-box/music-box.bas) | Music | `A`-`K`, `1`, `2`, `3`, `Q` | Play the SID chip as a small computer keyboard. | [Run program](http://localhost:8134/?program=music-box%2Fmusic-box.prg) |
| [Pixel Painter](pixel-painter/pixel-painter.bas) | Creative | Cursor keys, Space, `C`, `Q` | Draw character-cell pictures on the screen. | [Run program](http://localhost:8134/?program=pixel-painter%2Fpixel-painter.prg) |
| [Stock Trader](stock-trader/stock-trader.bas) | Simulation | `B`, `S`, `W`, `Q` | Trade a volatile stock over twenty market days. | [Run program](http://localhost:8134/?program=stock-trader%2Fstock-trader.prg) |
| [Starfield](starfield/starfield.bas) | Demo | `1`, `2`, `3`, `Q` | Watch and control a layered animated starfield. | [Run program](http://localhost:8134/?program=starfield%2Fstarfield.prg) |
| [Meteor Dodge](meteor-dodge/meteor-dodge.bas) | Arcade | `A`, `D`, `Q` | Dodge falling meteors and build your score. | [Run program](http://localhost:8134/program/meteor-dodge) |
| [Code Breaker](code-breaker/code-breaker.bas) | Logic | `0`-`9`, `Q` | Deduce a four-digit secret in ten guesses. | [Run program](http://localhost:8134/program/code-breaker) |
| [Submarine Hunt](submarine-hunt/submarine-hunt.bas) | Strategy | Row, Column, `Q` | Find three hidden submarines with limited sonar shots. | [Run program](http://localhost:8134/program/submarine-hunt) |
| [Lunar Lander](lunar-lander/lunar-lander.bas) | Simulation | `0`-`9`, `Q` | Choose engine burns and touch down gently. | [Run program](http://localhost:8134/program/lunar-lander) |
| [Memory Matrix](memory-matrix/memory-matrix.bas) | Memory | `1`, `2`, `3`, `4`, `Q` | Repeat a growing sequence of flashing quadrants. | [Run program](http://localhost:8134/program/memory-matrix) |
| [Robot Maze](robot-maze/robot-maze.bas) | Puzzle | `W`, `A`, `S`, `D`, `Q` | Navigate a freshly generated maze to its exit. | [Run program](http://localhost:8134/program/robot-maze) |
| [Music Box](music-box/music-box.bas) | Music | `A`-`K`, `1`, `2`, `3`, `Q` | Play the SID chip as a small computer keyboard. | [Run program](http://localhost:8134/program/music-box) |
| [Pixel Painter](pixel-painter/pixel-painter.bas) | Creative | Cursor keys, Space, `C`, `Q` | Draw character-cell pictures on the screen. | [Run program](http://localhost:8134/program/pixel-painter) |
| [Stock Trader](stock-trader/stock-trader.bas) | Simulation | `B`, `S`, `W`, `Q` | Trade a volatile stock over twenty market days. | [Run program](http://localhost:8134/program/stock-trader) |
| [Starfield](starfield/starfield.bas) | Demo | `1`, `2`, `3`, `Q` | Watch and control a layered animated starfield. | [Run program](http://localhost:8134/program/starfield) |
## Running the programs

View file

@ -46,7 +46,7 @@ function renderPrograms() {
for (const program of programs) {
const card = document.createElement('article');
card.className = 'program-card';
const programUrl = `/?program=${encodeURIComponent(program.path)}`;
const programUrl = `/program/${encodeURIComponent(program.slug)}`;
card.addEventListener('click', event => {
if (!event.target.closest('a, button')) location.href = programUrl;
});
@ -134,10 +134,11 @@ async function loadPrograms() {
}
}
function startEmulator(programPath) {
function startEmulator(program) {
const programPath = program.path;
library.hidden = true;
player.hidden = false;
const title = titleFromPath(programPath);
const title = program.title || titleFromPath(programPath);
document.title = `${title} — C64 BASIC Collection`;
document.querySelector('#program-title').textContent = title;
@ -164,6 +165,23 @@ function startEmulator(programPath) {
document.body.append(loader);
}
async function loadProgramPage(slug) {
library.hidden = true;
player.hidden = false;
try {
const response = await fetch('/api/programs', { cache: 'no-store' });
if (!response.ok) throw new Error('The program list could not be loaded.');
const { programs } = await response.json();
const program = programs.find(entry => entry.slug === slug);
if (!program) throw new Error('This program does not exist.');
startEmulator(program);
} catch (error) {
const message = document.querySelector('#emulator-error');
message.hidden = false;
message.textContent = error.message;
}
}
refreshButton.addEventListener('click', loadPrograms);
searchInput.addEventListener('input', renderPrograms);
typeFilter.addEventListener('change', renderPrograms);
@ -186,7 +204,11 @@ function setView(view) {
}
setView(localStorage.getItem('program-view') === 'list' ? 'list' : 'grid');
const programPath = new URLSearchParams(location.search).get('program');
if (programPath) startEmulator(programPath);
const legacyProgramPath = new URLSearchParams(location.search).get('program');
const programPage = location.pathname.match(/^\/program\/([a-z0-9]+(?:-[a-z0-9]+)*)\/?$/);
if (legacyProgramPath) {
const slug = legacyProgramPath.split('/')[0];
location.replace(`/program/${encodeURIComponent(slug)}`);
} else if (programPage) loadProgramPage(decodeURIComponent(programPage[1]));
else loadPrograms();

View file

@ -31,6 +31,7 @@ function fetchPrograms() {
const sourceFilePath = resolve(programDirectory, sourcePath);
return {
...program,
slug: program.path.split('/')[0],
source: sourceFilePath.startsWith(programDirectory + sep) && existsSync(sourceFilePath) ? sourcePath : null,
size: statSync(filePath).size
};
@ -72,7 +73,8 @@ function sendSource(response, programPath, headOnly = false) {
}
function serveStatic(response, pathname, headOnly = false) {
const requested = pathname === '/' ? 'index.html' : pathname.slice(1);
const isProgramPage = /^\/program\/[a-z0-9]+(?:-[a-z0-9]+)*\/?$/.test(pathname);
const requested = pathname === '/' || isProgramPage ? 'index.html' : pathname.slice(1);
const filePath = normalize(join(publicDirectory, requested));
if (!filePath.startsWith(publicDirectory) || !existsSync(filePath) || !statSync(filePath).isFile()) {
return false;