Drop the Blazor client runtime and load page JS as plain modules

No component in this app uses an interactive render mode - every page is statically server-rendered - yet each page load pulled down 56 KB gzipped of blazor.web.js. The only thing it bought was enhanced navigation, and enhanced navigation was the sole reason the page-script custom element and its 83 lines of loader boilerplate existed. Load each page's JS with <script type="module" src="@Assets[...]"> instead. Collocated .razor.js files are already static web assets, so @Assets and <ImportMap/> keep the fingerprinting page-script was getting through its dynamic import - transitively too, down to the jsonql-js modules. That retires the lifecycle protocol. Every onUpdate and onDispose here existed only to repair the DOM swap enhanced navigation performs, as the comment in Rvrb.razor.js said outright; a real page load tears down listeners, timers and module state for free. Concat's onUpdate was a copy of part of its onLoad and Note's just re-ran loadState, so both go, and the forward declarations that let onLoad hand state to those hooks collapse into plain consts. That last part fixes Calc by accident. submitForm reads `input` and `log`, but the consts it meant to close over were scoped to onLoad, so it was really resolving them through the window globals browsers create from id attributes. At module scope they now bind as intended. Also removes what the runtime left behind: FocusOnNavigate, the #blazor-error-ui circuit banner, and MainLayout.razor.css, which held only that banner's styles. No scoped CSS remains, so Blog.styles.css is no longer generated and its link goes with it. Fixes QRCodeScan along the way. It imported `a` and `div` from common.module.js, which has never exported either, so the module failed to load and the page did nothing - page-script never awaited the import, so the rejection was swallowed. Rebuilt both calls on h() and fixed the undefined `url` beside them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

author
Marijn Besseling <njirambem@gmail.com> · 2026-09-09 19:38 UTC
commit
354201a7a418f8e51babbcec8937cee6a893e2c9
parent
9d011c8738
tree
browse at this commit

28 files changed +204 -389

Blog/Components/App.razor +0 -7

@@ -6,19 +6,12 @@
6 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
7 7 <base href="/"/>
8 8 <link rel="stylesheet" href="@Assets["app.css"]"/>
9 - <link rel="stylesheet" href="@Assets["Blog.styles.css"]"/>
10 9 <ImportMap/>
11 10 <HeadOutlet/>
12 11 </head>
13 12
14 13 <body class="center">
15 14 <Routes/>
16 -<script src="@Assets["_framework/blazor.web.js"]" autostart="false"></script>
17 -<script>
18 - Blazor.start({
19 - webAssembly: {}
20 - });
21 -</script>
22 15 </body>
23 16
24 17 </html>

Blog/Components/Layout/MainLayout.razor +0 -6

@@ -5,9 +5,3 @@
5 5 @Body
6 6
7 7 <SiteFooter/>
8 -
9 -<div id="blazor-error-ui" data-nosnippet>
10 - An unhandled error has occurred.
11 - <a href="." class="reload">Reload</a>
12 - <span class="dismiss">x</span>
13 -</div>

Blog/Components/Layout/MainLayout.razor.css +0 -19

@@ -1,19 +0,0 @@
1 -/* Shown by blazor.web.js when an unhandled error reaches the browser. */
2 -#blazor-error-ui {
3 - display: none;
4 - position: fixed;
5 - inset-block-end: 0;
6 - inset-inline: 0;
7 - z-index: 1000;
8 - padding: var(--s-1) var(--s1);
9 - background-color: var(--background);
10 - color: var(--color);
11 - border-block-start: 2px solid var(--error);
12 -}
13 -
14 -#blazor-error-ui .dismiss {
15 - cursor: pointer;
16 - position: absolute;
17 - inset-inline-end: var(--s-1);
18 - inset-block-start: var(--s-1);
19 -}

Blog/Components/Pages/Calc.razor +1 -1

@@ -1,6 +1,6 @@
1 1 @page "/Calc"
2 2 <PageTitle>Calculator</PageTitle>
3 -<PageScript Src="./Components/Pages/Calc.razor.js"/>
3 +<script type="module" src="@Assets["Components/Pages/Calc.razor.js"]"></script>
4 4
5 5 <main>
6 6 <p>A rpn calculator</p>

Blog/Components/Pages/Calc.razor.js +8 -19

@@ -1,27 +1,16 @@
1 1 import { getById, h, writeError } from "/common.module.js"
2 2
3 -export function onLoad() {
4 - console.log('Loaded');
5 - const form = getById("form");
6 - const input = getById("input");
7 - const log = getById("log");
3 +const form = getById("form");
4 +const input = getById("input");
5 +const log = getById("log");
8 6
9 - form.addEventListener("submit", submitForm);
7 +form.addEventListener("submit", submitForm);
10 8
11 - const urlParams = new URLSearchParams(window.location.search);
12 - const queryInput = urlParams.get('in');
9 +const urlParams = new URLSearchParams(window.location.search);
10 +const queryInput = urlParams.get('in');
13 11
14 - if (input.value.length === 0) {
15 - input.value = queryInput;
16 - }
17 -}
18 -
19 -export function onUpdate() {
20 - console.log('Updated');
21 -}
22 -
23 -export function onDispose() {
24 - console.log('Disposed');
12 +if (input.value.length === 0) {
13 + input.value = queryInput;
25 14 }
26 15
27 16 /** @param {SubmitEvent} event */

Blog/Components/Pages/Concat.razor +1 -1

@@ -1,6 +1,6 @@
1 1 @page "/Concat"
2 2 <PageTitle>Concatenator</PageTitle>
3 -<PageScript Src="./Components/Pages/Concat.razor.js"/>
3 +<script type="module" src="@Assets["Components/Pages/Concat.razor.js"]"></script>
4 4 <main>
5 5 <p>A "concatenative language"</p>
6 6 <form id="form">

Blog/Components/Pages/Concat.razor.js +11 -25

@@ -2,35 +2,21 @@ import { getById, h, writeError } from "/common.module.js";
2 2 import lzString from "/lz-string.module.js";
3 3
4 4 let definitions = {};
5 -let log = undefined;
6 -let input = undefined;
7 -
8 -export function onLoad() {
9 - console.log('Loaded');
10 - const form = getById("form");
11 - input = getById("input");
12 - log = getById("log");
13 -
14 - form.onsubmit = async (event) => {
15 - event.preventDefault();
16 - await submitForm();
17 - };
18 5
19 - const urlParams = new URLSearchParams(window.location.search);
20 - const queryInput = urlParams.get("in");
6 +const form = getById("form");
7 +const input = getById("input");
8 +const log = getById("log");
21 9
22 - if (input.value.length === 0) {
23 - input.value = lzString.decompressFromEncodedURIComponent(queryInput);
24 - }
25 -}
10 +form.onsubmit = async (event) => {
11 + event.preventDefault();
12 + await submitForm();
13 +};
26 14
27 -export function onUpdate() {
28 - const urlParams = new URLSearchParams(window.location.search);
29 - const queryInput = urlParams.get("in");
15 +const urlParams = new URLSearchParams(window.location.search);
16 +const queryInput = urlParams.get("in");
30 17
31 - if (input.value.length === 0) {
32 - input.value = lzString.decompressFromEncodedURIComponent(queryInput);
33 - }
18 +if (input.value.length === 0) {
19 + input.value = lzString.decompressFromEncodedURIComponent(queryInput);
34 20 }
35 21
36 22 async function submitForm() {

Blog/Components/Pages/Generator.razor +1 -1

@@ -1,6 +1,6 @@
1 1 @page "/Generator"
2 2 <PageTitle>Number generator</PageTitle>
3 -<PageScript Src="./Components/Pages/Generator.razor.js"></PageScript>
3 +<script type="module" src="@Assets["Components/Pages/Generator.razor.js"]"></script>
4 4
5 5 <main>
6 6 <button id="bsn">Generate BSN</button>

Blog/Components/Pages/Generator.razor.js +4 -6

@@ -8,13 +8,11 @@ function writeInfo(label, msg) {
8 8 );
9 9 }
10 10
11 -export function onLoad() {
12 - const gen_bsn = getById("bsn");
13 - gen_bsn.addEventListener("click", generateBsn);
11 +const gen_bsn = getById("bsn");
12 +gen_bsn.addEventListener("click", generateBsn);
14 13
15 - const gen_iban = getById("iban");
16 - gen_iban.addEventListener("click", generateIban);
17 -}
14 +const gen_iban = getById("iban");
15 +gen_iban.addEventListener("click", generateIban);
18 16
19 17 function generateBsn() {
20 18 const nr9 = Math.floor(Math.random() * 7);

Blog/Components/Pages/Letterflixd.razor +1 -1

@@ -1,6 +1,6 @@
1 1 @page "/Letterflixd"
2 2 <PageTitle>Netflix to Letterboxd converter</PageTitle>
3 -<PageScript Src="./Components/Pages/Letterflixd.razor.js"/>
3 +<script type="module" src="@Assets["Components/Pages/Letterflixd.razor.js"]"></script>
4 4 <main>
5 5 <p>Convert Netflix viewing history to a Letterboxd import file.</p>
6 6 <p>Download your viewing history from Netflix at <a href="https://www.netflix.com/settings/viewed/"

Blog/Components/Pages/Letterflixd.razor.js +3 -9

@@ -1,15 +1,9 @@
1 1 import { getById, resetLog, writeError, writeInfo, writeDebug, h } from "/common.module.js"
2 2
3 -let netflix_file_input = undefined;
4 -
5 -export function onLoad() {
6 -
7 - const form = getById("form");
8 - netflix_file_input = getById("netflix-file");
9 -
10 - form.addEventListener("submit", submitForm);
11 -}
3 +const form = getById("form");
4 +const netflix_file_input = getById("netflix-file");
12 5
6 +form.addEventListener("submit", submitForm);
13 7
14 8 /** @param {SubmitEvent} event */
15 9 function submitForm(event) {

Blog/Components/Pages/Note.razor +1 -1

@@ -1,6 +1,6 @@
1 1 @page "/Note"
2 2 <PageTitle>Note</PageTitle>
3 -<PageScript Src="./Components/Pages/Note.razor.js"></PageScript>
3 +<script type="module" src="@Assets["Components/Pages/Note.razor.js"]"></script>
4 4
5 5 <main>
6 6 <textarea id="input" class="editor-tall" autofocus></textarea>

Blog/Components/Pages/Note.razor.js +12 -19

@@ -1,26 +1,19 @@
1 1 import { getById, debounce, writeError, resetLog } from "/common.module.js";
2 2 import lzString from "/lz-string.module.js";
3 3
4 -let input = undefined;
5 -export function onLoad() {
6 - input = getById("input");
7 - input.addEventListener("input", debounce(() => {
8 - if (input.value === '') {
9 - window.location.hash = ''
10 - }
11 - else {
12 - window.location.hash = '#' + lzString.compressToEncodedURIComponent(input.value);
13 - }
14 - resetLog();
15 - }, 10))
16 -
17 - window.addEventListener('hashchange', loadState);
18 - loadState();
19 -}
4 +const input = getById("input");
5 +input.addEventListener("input", debounce(() => {
6 + if (input.value === '') {
7 + window.location.hash = ''
8 + }
9 + else {
10 + window.location.hash = '#' + lzString.compressToEncodedURIComponent(input.value);
11 + }
12 + resetLog();
13 +}, 10))
20 14
21 -export function onUpdate() {
22 - loadState();
23 -}
15 +window.addEventListener('hashchange', loadState);
16 +loadState();
24 17
25 18 function loadState() {
26 19 if (window.location.hash !== '') {

Blog/Components/Pages/QRCodeGenerator.razor +1 -1

@@ -1,6 +1,6 @@
1 1 @page "/QRCodeGenerator"
2 2 <PageTitle>QR-Code generator</PageTitle>
3 -<PageScript Src="./Components/Pages/QRCodeGenerator.razor.js"></PageScript>
3 +<script type="module" src="@Assets["Components/Pages/QRCodeGenerator.razor.js"]"></script>
4 4
5 5 <main>
6 6 <textarea id="input" rows="5" autofocus>https://bes.is/</textarea>

Blog/Components/Pages/QRCodeGenerator.razor.js +6 -8

@@ -1,13 +1,11 @@
1 1 import { getById } from '/common.module.js';
2 2 import {QRCode} from '/qrcode.js';
3 3
4 -export function onLoad() {
5 - var qrcode = new QRCode("qrcode",
6 - "https://bes.is/");
4 +const qrcode = new QRCode("qrcode",
5 + "https://bes.is/");
7 6
8 - const input = getById("input");
9 - input.oninput = () => {
10 - const value = input.value;
11 - qrcode.makeCode(value);
12 - }
7 +const input = getById("input");
8 +input.oninput = () => {
9 + const value = input.value;
10 + qrcode.makeCode(value);
13 11 }
No newline at end of file

Blog/Components/Pages/QRCodeScan.razor +1 -1

@@ -1,6 +1,6 @@
1 1 @page "/QRCodeScan"
2 2 <PageTitle>QR-Code scanner</PageTitle>
3 -<PageScript Src="./Components/Pages/QRCodeScan.razor.js"></PageScript>
3 +<script type="module" src="@Assets["Components/Pages/QRCodeScan.razor.js"]"></script>
4 4
5 5 <main>
6 6 <video id="video"></video>

Blog/Components/Pages/QRCodeScan.razor.js +60 -62

@@ -1,4 +1,4 @@
1 -import { getById, writeError, writeInfo, a, div } from "/common.module.js";
1 +import { getById, writeError, writeInfo, h } from "/common.module.js";
2 2
3 3 // regexr.com/2rj36
4 4 const url_like_regex =
@@ -18,81 +18,79 @@ function arraysEqual(a, b) {
18 18
19 19 function logQRValue(value) {
20 20 if (URL.canParse(value)) {
21 - writeInfo(div(a(new URL(url).href, value)));
21 + writeInfo([h("a", { href: new URL(value).href }, value)]);
22 22 } else if (url_like_regex.test(value)) {
23 23 // Not a true URL but close enough for me to create an anchor tag.
24 24 if (!value.startsWith("http://") && !value.startsWith("https://")) {
25 25 value = "https://" + value;
26 26 }
27 - writeInfo(div(a(value, value)));
27 + writeInfo([h("a", { href: value }, value)]);
28 28 } else {
29 29 writeInfo(value);
30 30 }
31 31 }
32 32
33 -export function onLoad() {
34 - // check compatibility
35 - if (!("BarcodeDetector" in globalThis)) {
36 - writeError(
37 - "Barcode Detector is not supported by this browser. Make sure Shape Detection API is turned on.",
38 - );
39 - } else {
40 - writeInfo("Barcode Detector supported!");
41 -
42 - // create new detector
43 - const barcodeDetector = new BarcodeDetector({
44 - formats: ["qr_code"],
45 - });
33 +// check compatibility
34 +if (!("BarcodeDetector" in globalThis)) {
35 + writeError(
36 + "Barcode Detector is not supported by this browser. Make sure Shape Detection API is turned on.",
37 + );
38 +} else {
39 + writeInfo("Barcode Detector supported!");
46 40
47 - const initBarcodescan = (stream) => {
48 - writeInfo("Starting scanner...");
49 - let lastbarcodes = [];
50 - setInterval(() => {
51 - barcodeDetector
52 - .detect(stream)
53 - .then((barcodes) => {
54 - if (barcodes.length === 0) {
55 - } else {
56 - const values = barcodes.map(
57 - (barcode) => barcode.rawValue,
58 - );
59 - if (arraysEqual(values, lastbarcodes)) {
60 - return;
61 - }
41 + // create new detector
42 + const barcodeDetector = new BarcodeDetector({
43 + formats: ["qr_code"],
44 + });
62 45
63 - values.forEach(logQRValue);
64 - lastbarcodes = values;
46 + const initBarcodescan = (stream) => {
47 + writeInfo("Starting scanner...");
48 + let lastbarcodes = [];
49 + setInterval(() => {
50 + barcodeDetector
51 + .detect(stream)
52 + .then((barcodes) => {
53 + if (barcodes.length === 0) {
54 + } else {
55 + const values = barcodes.map(
56 + (barcode) => barcode.rawValue,
57 + );
58 + if (arraysEqual(values, lastbarcodes)) {
59 + return;
65 60 }
66 - })
67 - .catch((err) => {
68 - writeError(err);
69 - });
70 - }, 1000);
71 - };
72 61
73 - const video = getById("video");
74 - video.setAttribute("playsinline", "");
75 - video.setAttribute("autoplay", "");
76 - video.setAttribute("muted", "");
62 + values.forEach(logQRValue);
63 + lastbarcodes = values;
64 + }
65 + })
66 + .catch((err) => {
67 + writeError(err);
68 + });
69 + }, 1000);
70 + };
77 71
78 - /* Setting up the constraint */
79 - const facingMode = "environment"; // Can be 'user' or 'environment' to access back or front camera (NEAT!)
80 - const constraints = {
81 - audio: false,
82 - video: {
83 - facingMode: facingMode,
84 - },
85 - };
72 + const video = getById("video");
73 + video.setAttribute("playsinline", "");
74 + video.setAttribute("autoplay", "");
75 + video.setAttribute("muted", "");
86 76
87 - /* Stream it to video element */
88 - navigator.mediaDevices
89 - .getUserMedia(constraints)
90 - .then(function success(stream) {
91 - video.srcObject = stream;
92 - })
93 - .finally(() => {
94 - writeInfo("Initialized video stream");
95 - initBarcodescan(video);
96 - });
97 - }
77 + /* Setting up the constraint */
78 + const facingMode = "environment"; // Can be 'user' or 'environment' to access back or front camera (NEAT!)
79 + const constraints = {
80 + audio: false,
81 + video: {
82 + facingMode: facingMode,
83 + },
84 + };
85 +
86 + /* Stream it to video element */
87 + navigator.mediaDevices
88 + .getUserMedia(constraints)
89 + .then(function success(stream) {
90 + video.srcObject = stream;
91 + })
92 + .finally(() => {
93 + writeInfo("Initialized video stream");
94 + initBarcodescan(video);
95 + });
98 96 }
No newline at end of file

Blog/Components/Pages/Query.razor +1 -1

@@ -1,6 +1,6 @@
1 1 @page "/Query"
2 2 <PageTitle>Query</PageTitle>
3 -<PageScript Src="./Components/Pages/Query.razor.js"/>
3 +<script type="module" src="@Assets["Components/Pages/Query.razor.js"]"></script>
4 4
5 5 <main>
6 6 <div class="scroll-nav">

Blog/Components/Pages/Query.razor.js +37 -44

@@ -23,58 +23,51 @@ const HISTORY_STORE = "queryHistory"
23 23 const FAVORITES_STORE = "queryFavorites"
24 24 const HISTORY_LIMIT = 50
25 25
26 -let dactal
27 -let db
28 -let input
29 -let lang
30 -
31 -export async function onLoad() {
32 - input = getById("input")
33 - lang = getById("lang")
34 - const favoriteButton = getById("favoriteButton")
35 - dactal = new DACTAL()
36 -
37 - db = await openDB()
38 - await renderHistory()
39 - await renderFavorites()
40 -
41 - fetch("/brp.json").then(async res => {
42 - const cdata = await res.json()
43 - dactal.load(cdata, "personen")
44 - writeDebug("Loaded brp data")
26 +const input = getById("input")
27 +const lang = getById("lang")
28 +const favoriteButton = getById("favoriteButton")
29 +const dactal = new DACTAL()
30 +
31 +const db = await openDB()
32 +await renderHistory()
33 +await renderFavorites()
34 +
35 +fetch("/brp.json").then(async res => {
36 + const cdata = await res.json()
37 + dactal.load(cdata, "personen")
38 + writeDebug("Loaded brp data")
39 + await runQuery(input.value, lang.value)
40 +})
41 +
42 +input.onkeydown = async (e) => {
43 + if (e.key === "Enter" && !e.shiftKey) {
44 + e.preventDefault()
45 45 await runQuery(input.value, lang.value)
46 - })
47 -
48 - input.onkeydown = async (e) => {
49 - if (e.key === "Enter" && !e.shiftKey) {
50 - e.preventDefault()
51 - await runQuery(input.value, lang.value)
52 - // Only real keystrokes count as a run worth remembering, not the
53 - // synthetic Enter dispatched below to trigger the initial query.
54 - if (e.isTrusted) {
55 - await recordHistory(lang.value, input.value)
56 - }
46 + // Only real keystrokes count as a run worth remembering, not the
47 + // synthetic Enter dispatched below to trigger the initial query.
48 + if (e.isTrusted) {
49 + await recordHistory(lang.value, input.value)
57 50 }
58 51 }
52 +}
59 53
60 - lang.onchange = async () => {
61 - input.value = defaultQueries[lang.value]
62 - await runQuery(input.value, lang.value)
63 - }
54 +lang.onchange = async () => {
55 + input.value = defaultQueries[lang.value]
56 + await runQuery(input.value, lang.value)
57 +}
64 58
65 - favoriteButton.onclick = async () => {
66 - const name = await getFavoriteName()
67 - if (name) {
68 - await addFavorite(lang.value, input.value, name)
69 - }
59 +favoriteButton.onclick = async () => {
60 + const name = await getFavoriteName()
61 + if (name) {
62 + await addFavorite(lang.value, input.value, name)
70 63 }
64 +}
71 65
72 - getById("scrollTopButton").onclick = () => window.scrollTo({top: 0, behavior: "smooth"})
73 - getById("scrollFavoritesButton").onclick = () =>
74 - getById("favoritesList").closest(".panel").scrollIntoView({behavior: "smooth", block: "start"})
66 +getById("scrollTopButton").onclick = () => window.scrollTo({top: 0, behavior: "smooth"})
67 +getById("scrollFavoritesButton").onclick = () =>
68 + getById("favoritesList").closest(".panel").scrollIntoView({behavior: "smooth", block: "start"})
75 69
76 - input.dispatchEvent(enterEvent)
77 -}
70 +input.dispatchEvent(enterEvent)
78 71
79 72 function openDB() {
80 73 return new Promise((resolve, reject) => {

Blog/Components/Pages/Rvrb.razor +1 -1

@@ -1,6 +1,6 @@
1 1 @page "/rvrb"
2 2 <PageTitle>rvrb bot</PageTitle>
3 -<PageScript Src="./Components/Pages/Rvrb.razor.js"></PageScript>
3 +<script type="module" src="@Assets["Components/Pages/Rvrb.razor.js"]"></script>
4 4
5 5 <main>
6 6 <h1>rvrb bot</h1>

Blog/Components/Pages/Rvrb.razor.js +3 -24

@@ -5,24 +5,8 @@
5 5 const REFRESH_KEY = "rvrb-auto-refresh";
6 6 const REFRESH_MS = 30_000;
7 7
8 -let timer;
9 -
10 -export function onLoad() {
11 - wireAutoRefresh();
12 - startTicking();
13 -}
14 -
15 -export function onUpdate() {
16 - // Enhanced navigation swaps the DOM without a page load, so whatever was ticking is now
17 - // pointing at elements that are gone.
18 - stopTicking();
19 - wireAutoRefresh();
20 - startTicking();
21 -}
22 -
23 -export function onDispose() {
24 - stopTicking();
25 -}
8 +wireAutoRefresh();
9 +startTicking();
26 10
27 11 function wireAutoRefresh() {
28 12 const checkbox = document.getElementById("autoRefresh");
@@ -52,12 +36,7 @@ function startTicking() {
52 36 };
53 37
54 38 tick();
55 - timer = setInterval(tick, 1000);
56 -}
57 -
58 -function stopTicking() {
59 - clearInterval(timer);
60 - timer = undefined;
39 + setInterval(tick, 1000);
61 40 }
62 41
63 42 // The snapshot says how far into the track the bot was when it was taken, and how long ago that

Blog/Components/Pages/Storage.razor +1 -1

@@ -1,6 +1,6 @@
1 1 @page "/Storage"
2 2 <PageTitle>Storage</PageTitle>
3 -<PageScript Src="./Components/Pages/Storage.razor.js"></PageScript>
3 +<script type="module" src="@Assets["Components/Pages/Storage.razor.js"]"></script>
4 4
5 5 <main>
6 6 <Panel>

Blog/Components/Pages/Storage.razor.js +21 -23

@@ -39,30 +39,28 @@ function InitDB(upgradeCallback) {
39 39
40 40 }
41 41
42 -export function onLoad() {
43 - try {
44 - db?.close();
45 - db = null;
46 - InitDB(null);
47 -
48 - const newStoreButton = getById('newStoreButton');
49 - newStoreButton.addEventListener('click', async () => {
50 - const newStoreName = await getInput("New store", "Store name");
51 -
52 - if (newStoreName) {
53 - writeDebug("New store submitted.");
54 - InitDB((event) => {
55 - writeDebug(`Creating new store: ${newStoreName}, version: ${event.newVersion}`);
56 - event.target.result.createObjectStore(newStoreName, {autoIncrement: true});
57 - writeDebug("New store created: " + newStoreName);
58 - });
59 - }
60 - });
42 +try {
43 + db?.close();
44 + db = null;
45 + InitDB(null);
46 +
47 + const newStoreButton = getById('newStoreButton');
48 + newStoreButton.addEventListener('click', async () => {
49 + const newStoreName = await getInput("New store", "Store name");
50 +
51 + if (newStoreName) {
52 + writeDebug("New store submitted.");
53 + InitDB((event) => {
54 + writeDebug(`Creating new store: ${newStoreName}, version: ${event.newVersion}`);
55 + event.target.result.createObjectStore(newStoreName, {autoIncrement: true});
56 + writeDebug("New store created: " + newStoreName);
57 + });
58 + }
59 + });
61 60
62 - } catch (e) {
63 - writeError(e.message);
64 - throw e;
65 - }
61 +} catch (e) {
62 + writeError(e.message);
63 + throw e;
66 64 }
67 65
68 66 function showStores() {

Blog/Components/Routes.razor +0 -1

@@ -1,6 +1,5 @@
1 1 <Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
2 2 <Found Context="routeData">
3 3 <RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)"/>
4 - <FocusOnNavigate RouteData="routeData" Selector="h1"/>
5 4 </Found>
6 5 </Router>
No newline at end of file

Blog/Components/_Shared/PageScript.razor +0 -7

@@ -1,7 +0,0 @@
1 -<page-script src="@Src"></page-script>
2 -
3 -@code {
4 - [Parameter]
5 - [EditorRequired]
6 - public string Src { get; set; } = null!;
7 -}
No newline at end of file

Blog/Program.cs +2 -4

@@ -4,8 +4,7 @@ using Blog.Services;
4 4 var builder = WebApplication.CreateBuilder(args);
5 5
6 6 // Add services to the container.
7 -builder.Services.AddRazorComponents()
8 - .AddInteractiveServerComponents();
7 +builder.Services.AddRazorComponents();
9 8
10 9 builder.Services.AddHttpClient();
11 10 builder.Services.AddHybridCache();
@@ -38,7 +37,6 @@ app.UseHttpsRedirection();
38 37 app.UseAntiforgery();
39 38
40 39 app.MapStaticAssets();
41 -app.MapRazorComponents<App>()
42 - .AddInteractiveServerRenderMode();
40 +app.MapRazorComponents<App>();
43 41
44 42 app.Run();
No newline at end of file

Blog/wwwroot/Blog.lib.module.js +0 -83

@@ -1,83 +0,0 @@
1 -const pageScriptInfoBySrc = new Map();
2 -
3 -function registerPageScriptElement(src) {
4 - if (!src) {
5 - throw new Error('Must provide a non-empty value for the "src" attribute.');
6 - }
7 -
8 - let pageScriptInfo = pageScriptInfoBySrc.get(src);
9 -
10 - if (pageScriptInfo) {
11 - pageScriptInfo.referenceCount++;
12 - } else {
13 - pageScriptInfo = { referenceCount: 1, module: null };
14 - pageScriptInfoBySrc.set(src, pageScriptInfo);
15 - initializePageScriptModule(src, pageScriptInfo);
16 - }
17 -}
18 -
19 -function unregisterPageScriptElement(src) {
20 - if (!src) {
21 - return;
22 - }
23 -
24 - const pageScriptInfo = pageScriptInfoBySrc.get(src);
25 -
26 - if (!pageScriptInfo) {
27 - return;
28 - }
29 -
30 - pageScriptInfo.referenceCount--;
31 -}
32 -
33 -async function initializePageScriptModule(src, pageScriptInfo) {
34 - if (src.startsWith("./")) {
35 - src = new URL(src.substring(2), document.baseURI).toString();
36 - }
37 -
38 - const module = await import(src);
39 -
40 - if (pageScriptInfo.referenceCount <= 0) {
41 - return;
42 - }
43 -
44 - pageScriptInfo.module = module;
45 - module.onLoad?.();
46 - module.onUpdate?.();
47 -}
48 -
49 -function onEnhancedLoad() {
50 - for (const [src, { module, referenceCount }] of pageScriptInfoBySrc) {
51 - if (referenceCount <= 0) {
52 - module?.onDispose?.();
53 - pageScriptInfoBySrc.delete(src);
54 - }
55 - }
56 -
57 - for (const { module } of pageScriptInfoBySrc.values()) {
58 - module?.onUpdate?.();
59 - }
60 -}
61 -
62 -export function afterWebStarted(blazor) {
63 - console.log("afterWebStarted");
64 - customElements.define('page-script', class extends HTMLElement {
65 - static observedAttributes = ['src'];
66 -
67 - attributeChangedCallback(name, oldValue, newValue) {
68 - if (name !== 'src') {
69 - return;
70 - }
71 -
72 - this.src = newValue;
73 - unregisterPageScriptElement(oldValue);
74 - registerPageScriptElement(newValue);
75 - }
76 -
77 - disconnectedCallback() {
78 - unregisterPageScriptElement(this.src);
79 - }
80 - });
81 -
82 - blazor.addEventListener('enhancedload', onEnhancedLoad);
83 -}
No newline at end of file

CLAUDE.md +27 -13

@@ -34,18 +34,31 @@ follow for any new interactive page.
34 34
35 35 ### The `.razor` + `.razor.js` pairing
36 36
37 -Blazor's `<PageScript Src="./Components/Pages/Foo.razor.js"/>` (wrapping the built-in
38 -`<page-script>` custom element, registered in `wwwroot/Blog.lib.module.js`) loads a JS
39 -module scoped to that page. The module can export `onLoad`, `onUpdate`, `onDispose`
40 -lifecycle hooks, called as the custom element connects/re-renders/disconnects (this
41 -matters across Blazor's enhanced navigation, which doesn't do a full page reload).
42 -Page logic is written in vanilla JS DOM manipulation, not Blazor data binding — components
43 -grab elements with `getById` and wire up listeners directly in `onLoad`.
44 -
45 -`PageScript.razor`, `Log.razor`, `Panel.razor`, and the `StackOp*.razor` components live in
46 -`Components/_Shared/` and are reused across pages (e.g. `Log` renders a debug/error log
47 -panel that JS writes into via `writeDebug`/`writeError`; `Panel` is a bordered
48 -fieldset-with-legend used for docs/credits/grouped controls).
37 +There is **no Blazor client runtime**. Every page is statically server-rendered and its JS is
38 +loaded as a plain ES module:
39 +
40 +```razor
41 +<script type="module" src="@Assets["Components/Pages/Foo.razor.js"]"></script>
42 +```
43 +
44 +`.razor.js` files colocated with a component are static web assets, so `@Assets[...]` resolves
45 +them to a fingerprinted URL and `<ImportMap/>` in `App.razor` maps the bare `/common.module.js`
46 +style imports onto their fingerprinted files too. Never hand-write the plain path — go through
47 +`@Assets` or the module ships uncached.
48 +
49 +The module body *is* the page's setup code: it runs once, at top level, after the DOM is parsed
50 +(`type="module"` is deferred). There are no `onLoad`/`onUpdate`/`onDispose` hooks, because
51 +navigation is a real page load — the browser tears down listeners, timers and module state for
52 +you. Page logic is vanilla JS DOM manipulation, not Blazor data binding: grab elements with
53 +`getById` at top level and wire up listeners directly.
54 +
55 +If you ever reintroduce interactivity or enhanced navigation, this stops being true — module
56 +state would then outlive the DOM it points at, and every page script would need teardown again.
57 +
58 +`Log.razor`, `Panel.razor`, and the `StackOp*.razor` components live in `Components/_Shared/`
59 +and are reused across pages (e.g. `Log` renders a debug/error log panel that JS writes into via
60 +`writeDebug`/`writeError`; `Panel` is a bordered fieldset-with-legend used for docs/credits/
61 +grouped controls).
49 62
50 63 ### `wwwroot/common.module.js`
51 64
@@ -99,4 +112,5 @@ organized into numbered sections (Tokens → Reset/base → Typography → Layou
99 112 Components → Media) with CSS custom properties as the single source of design tokens
100 113 (colors, spacing scale, fonts). It uses `light-dark()` and `color-scheme` for automatic
101 114 dark mode — don't hardcode light/dark colors, extend the token set in section 1 instead.
102 -`MainLayout.razor.css` holds the one layout-specific scoped stylesheet.
115 +There are no scoped `.razor.css` stylesheets, so `App.razor` links `app.css` only; adding one
116 +means adding the `Blog.styles.css` bundle link back.