# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What this is `bes.is` — a Blazor Server app (single ASP.NET Core project, no test project) hosting a collection of small, mostly self-contained JS/HTML experiments/tools (RPN calculator, QR code scan/generate, a Netflix→Letterboxd converter, a JSON query playground, BRP lookup, etc.), each on its own route. ## Commands Run from the repo root or `Blog/`: ``` dotnet build # build dotnet run --project Blog # run locally (http://localhost:5296) dotnet watch --project Blog # run with hot reload ``` There is no test project in the solution — don't invent `dotnet test` commands or test files unless asked to add a test project first. Publishing is done via the Rider run configuration "Publish Blog to custom server" (self-contained `linux-x64`, Release, ReadyToRun) — not part of normal dev workflow. ## Architecture **One project, one layout, many independent page-features.** `Blog/Components/Pages/*.razor` are the routes (`@page "/Xyz"`), each linked from `SiteFooter.razor`. Most pages are a `.razor` file paired with a `.razor.js` file of the same name — this is the pattern to follow for any new interactive page. ### The `.razor` + `.razor.js` pairing There is **no Blazor client runtime**. Every page is statically server-rendered and its JS is loaded as a plain ES module: ```razor ``` `.razor.js` files colocated with a component are static web assets, so `@Assets[...]` resolves them to a fingerprinted URL and `` in `App.razor` maps the bare `/common.module.js` style imports onto their fingerprinted files too. Never hand-write the plain path — go through `@Assets` or the module ships uncached. The module body *is* the page's setup code: it runs once, at top level, after the DOM is parsed (`type="module"` is deferred). There are no `onLoad`/`onUpdate`/`onDispose` hooks, because navigation is a real page load — the browser tears down listeners, timers and module state for you. Page logic is vanilla JS DOM manipulation, not Blazor data binding: grab elements with `getById` at top level and wire up listeners directly. If you ever reintroduce interactivity or enhanced navigation, this stops being true — module state would then outlive the DOM it points at, and every page script would need teardown again. `Log.razor`, `Panel.razor`, and the `StackOp*.razor` components live in `Components/_Shared/` and are reused across pages (e.g. `Log` renders a debug/error log panel that JS writes into via `writeDebug`/`writeError`; `Panel` is a bordered fieldset-with-legend used for docs/credits/ grouped controls). ### `wwwroot/common.module.js` Shared JS helpers imported by page scripts: - `h(tag, attrs?, children?)` / `t(text)` — a small hyperscript-style DOM builder (no framework, just `document.createElement`/`appendChild`). Prefer this over manual DOM building or template strings in new page scripts. - `getById(id)` — like `document.getElementById` but throws instead of returning null. - `writeError` / `writeInfo` / `writeDebug` / `resetLog` — write into the `#log` element that `Log.razor` renders. - `debounce(fn, wait)`. Other `wwwroot/*.js` files (`dactal.js`, `qrcode.js`, `lz-string.module.js`, `jsonql-js/`) are third-party or semi-vendored libraries used by specific pages (e.g. `Query.razor.js` uses both `dactal.js` and `jsonql-js` to run two query languages against the same mock BRP dataset in `wwwroot/brp.json`). ### rvrb feature (stats read off another BEAM node) `Rvrb.razor`/`.razor.cs`/`.razor.js` (route `/rvrb`) shows the status and stats of the rvrb Elixir bot (`~/Developer/elixir/rvrb`), which runs on the same server. `Services/RvrbService.cs` gets them by joining the bot's Erlang cluster: [BeamSharp](https://github.com/Besselking/BeamSharp) (referenced as a project from the sibling checkout, it is not on NuGet yet) makes this site a hidden Erlang node and calls `Rvrb.Stats.snapshot/0` on the bot the way any BEAM node would — no HTTP endpoint on the Elixir side. The node is started on the *first request* rather than at boot, and a failure is a value (`RvrbStatus.Unreachable`) rather than an exception: the site must not fail to start, or a page fail to render, because EPMD is down or the bot was deployed without distribution. `Models/RvrbSnapshot.cs` holds the shapes and `Services/RvrbSnapshotReader.cs` decodes the Erlang term into them by hand — the term is an Elixir map written by Elixir, not a serialized C# type. Configuration lives under `Rvrb` (`RvrbOptions`): `Node`, `LocalNode`, `CallTimeout`, `CacheFor` in `appsettings.json`, and `Cookie` — the shared Erlang cookie — from user secrets in development (`dotnet user-secrets set "Rvrb:Cookie" ...`) or `Rvrb__Cookie` in the environment. The bot's own side of this (enabling distribution on its release) is documented in the rvrb repo's README. ### BRP feature (the one page with a real backend) `BRP.razor`/`.razor.cs` and `BrpTestData.razor` are backed by `Services/BrpService.cs`, which calls an external "Haal Centraal BRP" lookup API (base address configured in `Program.cs` as `https://brp.bes.is/`) and caches responses via `HybridCache` (`Microsoft.Extensions.Caching.Hybrid`). `Models/BRPEntry.cs` and `Models/RaadpleegMetBurgerservicenummer.cs` model the request/response shapes. Test/mock data lives in `Resources/test-data.json` and `wwwroot/brp.json`. ### Styling `wwwroot/app.css` is one hand-maintained stylesheet (no CSS framework, no build step), organized into numbered sections (Tokens → Reset/base → Typography → Layout → Controls → Components → Media) with CSS custom properties as the single source of design tokens (colors, spacing scale, fonts). It uses `light-dark()` and `color-scheme` for automatic dark mode — don't hardcode light/dark colors, extend the token set in section 1 instead. There are no scoped `.razor.css` stylesheets, so `App.razor` links `app.css` only; adding one means adding the `Blog.styles.css` bundle link back.