WikiBento is a drag-and-drop dashboard for Wikimedia β and for the adjacent open collections Wikimedia work leans on: the Internet Archive (items and their views, books you can search inside, audio and video, and how a page looked years ago) and maps over OpenStreetMap. The things worth keeping an eye on live in many places β pageview and stats APIs, wiki pages, recent changes, Commons, archive.org β and WikiBento brings them onto one reactive board you can arrange, point at a subject, and send as a link: article metrics, external link counts, category sizes, file usage, GLAM impact stats, archive items and books, maps, listings and feeds you can click through β plus output widgets that speak (π Speaker) or translate (π Translator) what they hold β and they chain: a translation carries its language, so a speaker reads a French paragraph in a French voice (ISSUE-97).
Why this exists at all, now that an agent can generate a dashboard from a sentence:
docs/WHY-WIKIBENTO.md β the argument, and the running ledger of measured API
behaviour (timeouts, CORS realities, honest failure states) that a one-shot dashboard does not have. The
widgets are the demo; the ledger is the product.
It's a single-page React app built on react-grid-layout
(the grid engine behind Grafana and Kibana), β0.84 MB (~248 KB gzipped), hostable as static files on Toolforge
or anywhere. Client-driven, with a server side only where a browser cannot go. Every widget fetches its own
data in your browser β Wikimedia's REST and Action APIs, Commons, Wikidata, Wikistats, Commons Impact Metrics,
WDQS, MinT, and the Internet Archive's metadata, IIIF and Wayback APIs β and the app carries a small, audited set
of server-side routes for the handful of jobs a browser genuinely cannot do: a map service that refuses
browser-shaped requests, two sources that send no CORS header, a short-URL expansion, a cap that has to be enforced
before 39 MB arrives, and the Ask advisor's LLM relay, where the key and the prompt live. Each route is inventoried,
bounded and guarded β the table is Proxied services below, the full
inventory is docs/PROXIES.md. No accounts, no login, nothing stored about you.
npm run build prints exact figures.
Live: wikibento.toolforge.org Β· Example: Alysa Liu Β· Source: github.com/fuzheado/wikibento
Start here: ?config=/demos.json β an index board linking the whole suite (onboarding β flagships).
| demo | what it shows |
|---|---|
| π Article switcher | the simplest board: one param, two cards |
| π€ Biosphere levels | a biology diagram whose every label is clickable β the words were read out of the picture by OCR, and each opens the article its meaning wants |
| π Clickable zones | a photograph with six clickable zones β its own Commons notes, read from the file's wikitext β where a click publishes the peak's name and the π Article Excerpt beside it loads that mountain |
| πΌοΈ Image tour | three views of one ridge chained by zones β click Next view β and the next picture loads with its own zones ready, because each card holds one view and the file travels on the card's zones channel |
| π· Steam turbine, labelled | a power-plant diagram whose every label is a door β the words were read out of the drawing by OCR, and a click loads the article and its pageviews beside it (MIT OpenCourseWare, CC BY-SA 4.0) |
| an MIT chart of weapon-system lifecycles where each bar is a door β the bars were found by a pixel scan, not by eye, and a click opens the aircraft's article and its pageviews | |
| ποΈ Commons gallery | Commons' curated layer as data β a gallery page's own captions and order, and a click on a tile drives the reader beside it |
| ποΈ Category gallery | Images from a Commons category β alphabetical, newest, random or largest β with the category's total in the subtitle when a card shows only part of it |
| π Page picker | one validated page-name box with a wiki beside it β three cards follow, and none of them names a project |
| π Translate chain | two params + a three-step dataflow (excerpt β translator β π speaker), where the translation's language picks the spoken voice |
| ποΈ Params & galleries | buttons, a slider and a month stepper driving galleries |
| π Dataflow pipeline | Text List β Filter β Count β Display |
| βοΈ A Wikimedian in Residence, four years in | a programme measured in full: three residents in four languages, the impact of their Commons uploads from Commons Impact Metrics (12- and 24-month trends, top files, the 108 wikis that use them), one file's 15-month traffic, and the core articles' language coverage from Wikidata |
| ποΈ One template, any institution | exact GLAM impact stats (Commons Impact Metrics) β type any institution/category in one box and every CIM card follows; five flagship collections are the starting shortlist |
| π Article vitals | summary, traffic, ORES quality, WikiProjects, edits, images |
| π§ Query power | live SPARQL across WDQS, Humaniki and QLever β and a result's coordinates drawn as a map |
| π Born in 1929 | one board, one story: two lives on a shared timeline (21 dated events), plus each subject as prose, images and traffic β Anne Frank and Martin Luther King Jr., both born in 1929 |
| π°οΈ Two lives, one axis | lives on a shared timeline, two ways (calendar or aligned at birth), zoomable 8Γ, per-card title and light/dark card theme β Anne Frank Γ Martin Luther King Jr., Marie Γ Pierre Curie |
| π Embed any page | frame a 3D model (Objectium) in a card |
| π Read a document | a 329-page PDF and a DjVu read page by page β turn, zoom, jump, facing pages β beside an Internet Archive book (one reader, two archives), plus a small work transcribed on Wikisource whose page text appears with its proofreading grade (Validated β and Not proofread (uncorrected OCR) where that is the truth) |
| π° A wiki page in a card | any wiki page rendered in a card β the lead, one section, or the whole thing β with its tables and images, and every blue link a pick target |
| π One image filling the box | a single Commons file as an edge-to-edge tile β letterboxed or cropped, with the chrome switched off |
| πΌοΈ Edge to edge | the switch that takes a card's chrome away β a QR code, a Commons document, an IA book, a wiki page |
| πΊοΈ A map in a card | a coordinate, a Wikidata item or a page title β plus any number of points, and the same list as a path or an area; geometry from another widget or pasted GeoJSON |
| π Story mode | one Wikipedia article as a continuous scroll β its images and captions, the article's own chapters, and a panel per image chosen by that image's shape |
| π Internet Archive | a scanned book you can turn, zoom and search inside (16 and 304 pages) read as facing pages, archive items by media type, and two players streaming the real files β an 11-minute film and a LibriVox playlist, straight from archive.org/download/ |
| π° The front page, as boxes | In the news, Did you know, On this day, the day's featured article and picture β five Wikipedia templates rendered with the wiki's own HTML and styles, each linked back to its template |
| π Click through | Click a sea in a live Wikipedia box and another widget uses it: the page viewer loads the article and the value card shows the string that travelled. β Links in the box decides whether a click opens a tab, sends to the board, or both |
| π§© Full catalog | all 36 widget types on one board β its article switcher drives five cards |
Every board also works in kiosk mode β add ?kiosk=1. Configs are plain JSON (see
docs/JSON-FORMAT.md); any URL, on-wiki page or GitHub raw file works the same way. Writing a
board from outside the app β in a chat, a notebook or another tool? Read the served contract instead:
https://wikibento.toolforge.org/board-guide.md (generated from the manifest and the two specs above, with the whole
catalog and the two reference grammars), and the machine-readable pair
https://wikibento.toolforge.org/manifest.json + https://wikibento.toolforge.org/dashboard.schema.json.
A ready-made dashboard hosted on Wikimedia Commons also loads from an on-wiki page β
?config=https://commons.wikimedia.org/wiki/Commons:WikiPortraits/Bento-demo.json (or the short link
?config=https://w.wiki/TR9R, expanded via the same-origin /api/resolve endpoint). Any CORS-enabled host
works the same way.
36 widget types. Each section below is a category in the in-app Add Widget panel; the full table β what every widget shows and the API behind it β is docs/WIDGET-CATALOG.md.
| category | widgets |
|---|---|
| Articles (5) | π Article Pageviews Β· π Article Excerpt Β· π Edit History Β· π Article Quality (ORES) Β· π§ WikiProject Assessment |
| Categories & GLAM (11) | π Category Size Β· π GLAM Category Usage Β· and nine CIM widgets: snapshot, views over time, top files / wikis / pages / editors, global leaderboard, file spotlight, file traffic |
| Files & Media (5) | πΌοΈ Gallery (an article β images and video β a Commons gallery page, a wiki category, or a pasted list) Β· πΌοΈ File Usage by Wiki Β· π 360Β° Panorama Viewer Β· π¬ Video / Media Player Β· π Document Reader |
| Rankings & Platforms (4) | π External Link Count Β· π Wiki Stats Β· π Top 10 Wikipedias Β· π₯ Top Wikipedia Articles |
| Content & Embeds (9) | ποΈ Board Controls Β· π Text / Markdown Β· π³ QR Code Β· π Speaker Β· π Translator (MinT) Β· π Article List Β· π Wiki Page Β· πΊοΈ Map Β· π° Wikipedia Box |
| Queries & Power (1) | π§ SPARQL Query (WDQS Β· QLever Β· Humaniki) |
| Dataflow (4) | π§Ύ Text List Β· π Filter Lines Β· π’ Line Count Β· π¨οΈ Value Display |
| Web & History (3) | π¦ IA Item Stats Β· π IA Book Β· π°οΈ Wayback Snapshot Gallery (alpha) |
All 27 data-driven widget types render live data in the browser; the 9 static ones (Text/Markdown, QR Code, Board Controls, Speaker, Wiki Page, Text List, Filter Lines, Line Count, Value Display) render from config β no fetch.
- Drag, drop, resize β a 12-column grid with vertical compaction; grab a title bar to move a widget, the bottom-right corner to resize it
- Responsive β under 768px the grid collapses to a single-column card stack in grid reading order, so a desktop arrangement still makes sense on a phone
- Presentation β βΆ Present hides every editing affordance and locks the grid for a clean data wall;
?kiosk=1loads any board that way directly (a shareable presentation link), and β£ Lean does the same without taking over the screen - Story mode β any gallery can present an article's images as one continuous scroll: each image gets the
panel its own shape deserves (an edge-to-edge panorama, a centred plate for tall portraits at native size, or an
image-and-text split whose text side alternates), with the article's own sections as chapters and a progress bar down
the side. Set Display β Story on any gallery and point it at an article, or open
?config=/story-demo.json.json` (Gallery, Images come from An article). The technique is ported from the Met / Google-Arts-&-Culture prototype; in Present or Lean mode a single tall card is the whole screen, which is the way to show it to a room. - A map in a card β πΊοΈ Map takes a place: a coordinate (
48.8584, 2.2945), a Wikidata item (Q64), or a page title (read from that wiki first, following redirects, then from its Wikidata item). Wikimedia's own map service draws it at the card's own size, with our pin at the centre, OpenStreetMap's credit on the image, and zoom, fit and map label language under β. One image means no tiles and no map library β so it prints and exports properly, and it works Edge to edge. Points take the same vocabulary, one per line (up to 100), drawn as markers on the image; with Frame the points on, the centre and the zoom are computed to fit them all. The same list can draw a shape β A path (a line through the places, in order) or An area (a polygon; the ring is closed for you) β and geometry also arrives as pasted GeoJSON (RFC 7946, the format every map and drawing library speaks: rings closed, swapped axes detected, sizes capped, and a shape that cannot be read says why) or from another widget: a map card publishes what it draws as GeoJSON, so one card can draw another's shape. Dated features carry their time (datetime,start_datetime/end_datetime, and a time per vertex for a moving path), which is the shape a timeline already reads β see docs/GEOMETRY.md. The same machinery is the SPARQL widget's Map renderer, so a query that returns coordinates is a map (auto-detected from the result, or chosen in β). The title bar names the place the fetch resolved (Q64β Berlin), and when the relay fails the card prints the relay's own reason with Try again rather than leaving a broken image. Open?config=/map-demo.json. - Nothing proxied is the weak point β six services need our server rather than your browser
(docs/PROXIES.md), and all six share one set of rules: a host allowlist, a streamed byte cap, a deadline,
a per-client rate limit, an in-flight ceiling and bounded caches.
scripts/relay-guard-e2e.mjstries to break them on everynpm test. - Filling the box β a gallery can be one file filling its card. Paste a single
File:name, set Display β Single image, and two switches decide the rest: Image fit (Letterbox shows the whole file, Fill crop covers the box) and the Edge to edge switch, which takes away the title bar, the padding and the card's own title so the picture reaches the edges. Edge to edge works on the π¬ Video / Media Player too β its file line goes, and for a single file its transport controls with it, while the video's own controls stay β and on the π 360Β° Panorama, the π³ QR Code, the π Document Reader, the π IA Book and the π Wiki Page. A gallery's captions are a switch as well, so a tile can carry no text at all. A click on a picture does nothing by default β Clicking an image offers Open the file page in a new tab, Send it to the board, Both, or Nothing, because on a phone a click that misses what you were aiming at should not throw you onto Commons; a gallery you can click through says so in one setting. Open?config=/image-tile-demo.jsonto see all four. - A wiki page in a card β the π° Wikipedia Box renders any page by title, not only a Main Page template:
choose the lead (light, and dense with links), a section by number or heading name, or the whole page. Its own
tables, images and references come with it β styled by the app rather than by Wikipedia's skin β and the card's footer
says what it is showing. Then pick from it: arm a brush and click the blue links inside the card, and each click
places a card for what you clicked instead of following the link. That loop is the reason the mode exists; open
?config=/wiki-page-demo.jsonto watch it. - π Clickable zones on a picture β a single image can carry zones: boxes in percentages of the picture, written
one per line in the gallery's Clickable zones field (
x,y,w,h | label | kind | value | action), each with a label, a kind and a value. A click publishes the value on that card's ownzoneschannel, so another card loads the article, the file or the category it names β and because the boxes are percentages of the file, they hold at any card size. The picture's own background is not a link, so a click that misses every zone does nothing. Five boards show what that is for: a photograph whose zones are the file's own Commons notes, a biology diagram read word by word by OCR, a power-plant diagram and a lifecycle chart whose bars are the doors β each feeding an article and its traffic β and an image-to-image tour where a click loads the next view with its own zones. docs/ZONES.md has the design notes and what is still to come. - Pick mode β π Pick βΎ arms a brush: choose a widget type, then click items inside the cards you are already reading and each click places a card for that item. One click, one card, named in the toast; the same item twice refuses rather than making a twin; Undo takes it back off. It is a second, power-user verb beside + Add Widget, not a replacement β the menu only offers types that can consume the thing you clicked. Clickable today: article-list rows, ranked article rows (including the expanded top-pages card), any ranking row whose own link is a Wikipedia article or a Commons file, GLAM sample strips, CIM file rows and gallery tiles β a row's link declares what it is.
- Content-fit galleries β image galleries default to full window width and fit their height to the image count after loading, until you resize one yourself, and then your size sticks
New here? docs/GUIDE.md explains the model β board params (shared inputs), widget config (local settings) and dataflow (derived values) β with worked examples.
- ποΈ Board params β declare a
paramsblock, reference{{name}}in any widget. Board Controls renders buttons, menus, text fields, number sliders and month steppers, plus a lookup box that validates free text against live Wikimedia data (type any museum, and the CIM cards follow) β one click re-aims every referencing widget. Each card can be scoped to a subset of params, so one drives the article and another the target language. - π Widget-to-widget dataflow β a widget can emit its output and another can consume it, either
through a
sourcepicker in the β panel or{{widget:id}}interpolation in any config field; consumers re-fetch automatically when the value changes. Article Excerpt emits its first paragraph, so a Translator, Speaker or Markdown card can read the article. Try it:?config=/flow-demo.json. Every card's β opens the two-sided spawn panel: the producers it can read from, the consumers it can feed, and the refused wires said out loud rather than dropped β the pairings come from the registry's ownoutputs/kinds, so nothing is offered that cannot work.npm run smoke:spawnproves it in a browser. - π·οΈ Names, not positions β every widget has a visible, editable instance name. Renaming one that others reference repoints them atomically, after a confirm dialog that says how many references it will update.
- β¨ Ask β describe what you want in plain language ("random sampling of images from a category") and get
widget recommendations with settings pre-filled, powered by Wikimedia's LiftWing LLM through a same-origin
relay, with an offline keyword fallback. The catalog it sees is generated by
npm test, so it cannot drift from the registry. - π° Live Wikipedia boxes β render a template with the wiki's own markup and its own styles: the five Main Page boxes (In the news, Did you know, Today's featured article, the selected anniversaries, Picture of the day) or any other, including dated ones that keep themselves current. A box's links open in a new tab; set Links in the box to send to the board and a click publishes the page the reader chose for another widget to load or draw. docs/WIRING-BOARDS.md is the wiring guide.
- π Every wiki, chosen quickly β one picker for all 364 projects, ordered by the ones you use, your default wiki, a curated shortlist, then everything else; searchable by name, code, script or English name. β Settings holds your wiki and the recent list.
- Add, configure, inspect β a searchable catalog; a β panel per widget with Apply & Reload pinned at the bottom so every setting stays reachable in a short card (the β panel likewise); asset-aware titles that say what each widget is analyzing.
- ποΈ Commons galleries β a gallery page (a main-namespace page with a
<gallery>tag: hand-picked images and hand-written captions, unlike a category, which has neither) rendered as a grid or a list. The captions travel as data β they can feed a Translator or a Speaker β and a tile click publishes that file for another card once Clicking an image is set to Send it to the board. - π One box, any wiki, validated β type a page name and the board follows: suggestions as you type, a β/β
verdict against the wiki you chose, and
en:/de:/commons:typed as a shortcut that moves the wiki picker. What the box stores is a reference, so the wiki travels with the page. - Example board β β¨ loads a showcase with every widget type; the guided demos are at
?config=/demos.json.
- Layout persistence β saved to
localStorage(wikibento-layout), survives refresh - Opening someone's link borrows their board β a
?config=or#/d/β¦link is shown, never written over yours: your own board stays saved and untouched, and a slim notice offers [Save this as mine] or [Back to my board]. Edit anything and the board becomes yours; the one it displaces stays recoverable for 24 hours (docs/URL-STATE.md) - Export / Import β β¬ downloads the whole board as
dashboard.json; β¬ loads one back (file or paste) with per-field validation, warnings, and nothing applied unless it is valid - Shareable links β π opens a Share panel with a QR code and a copyable link. An untouched board loaded
from
?config=shares that short, phone-friendly URL; anything else embeds the board itself β compressed (#/z/β¦), which is measured to make the difference between a QR code that works for 1 of 15 real boards and one that works for 13. The link always describes the board on screen rather than the file it started from, and a board too big for any QR says so and offers the paths that have no length limit (copy the link, or Export β AirDrop β Import on the phone) - β Settings β your wiki (the default the project picker offers first), the wikis you have used recently, and whether Present asks for fullscreen. Stored in this browser, inspectable in devtools, no account
- Reset β β About explains the tool; Reset returns the starter board or a blank one, in one dialog β and clears the URL params that pointed at the board you just discarded, so a refresh cannot bring it back
- π Reading a book, or a document β π IA Book reads a scanned archive.org book page by page from its IIIF manifest: turn, zoom, a thumbnail strip, search inside (each hit names its page and shows the matched words boxed on it), the page's OCR text, and facing pages β with right-to-left handled for Arabic/Hebrew/Yiddish scans. π Document Reader does the same for a Commons PDF or DjVu, and where Wikisource has transcribed the file its transcription shows from the start and follows you as you turn pages, headed by the proofreading grade (Validated, or Not proofread (uncorrected OCR) when that is the truth). Both share one viewer β docs/DOCUMENT-VIEWER.md.
- π°οΈ Timelines of lives β the SPARQL widget's
timelinerenderer puts dated rows on one shared axis, one lane per group, with a shaded band for the window every lane is documented in. Two alignments (calendar years β what happened at the same time β or age, aligning every lane at its first event), β / + zoom to 8Γ with horizontal panning, an overlap toggle, a light card theme, and an optional title (the only title in lean/present mode). Two presets ship:two-lives(Anne Frank Γ Martin Luther King Jr.) andcurie-pair(Marie Γ Pierre, where age alignment shows his lane ending at 46 while hers runs to 67). Try?config=/parallel-lives-demo.json. - β€ Export any widget β one menu, four formats: PDF (the browser's print engine β vector and
selectable, the best artifact for a report, slide or email), CSV (the widget's own data, as rows),
SVG (any widget that is not an iframe, with images inlined), and PNG wherever the pixels can reach a
canvas cleanly β a widget that draws itself as SVG, or one whose images come from a CORS-enabled host
(
iiif.archive.org,upload.wikimedia.org,thumb.wikimedia.org), which is how a book or document page exports as a real PNG. PNG of an arbitrary HTML/CSS card is deliberately not offered β docs/EXPORT.md says why, and what to do instead. - π¨ Print or save the board as PDF β three shapes, because a dashboard is not a document: Board (the grid you see, row for row), Poster (one page, sized to the board) and Document (a card per row, to read). The sheet waits for stats and images to arrive before it is taken, and it scales the board to the paper rather than reflowing it, so nothing overlaps and nothing is cropped. Chrome is removed; each card's β± freshness line is kept (docs/EXPORT.md).
- Freshness and time scope are visible β every live widget shows when it last fetched (β± "updated
2:34:05 PM Β· auto-refresh 1h") and the scope it resolved to ("2026-07"); the latter is enforced by
npm test - A rate-limit-aware HTTP layer β 4 concurrent fetches,
Retry-Afterhonoured as a global cool-down, one retry per 429 with an actionable "wait ~Ns, then Retry", and a shared TTL cache (two widgets hitting the same 195 KB CSV cost one request, with a 15 s timeout and backoff) - Per-widget error boundaries β a crash shows a themed fallback with Try Again instead of taking the dashboard down, and the grid reflows on resize
npm install
npm run dev # dev server β http://localhost:5173
npm run build # tests, then production build β dist/
npm run preview # serve dist/ at http://localhost:4173
npm test # unit tests + the cross-document consistency gates
npm run smoke # grid geometry + panel reachability (every β/β action at any panel size)
npm run smoke:boards # a board pasted through β¬ Import draws β one card per widget, no waiting state
npm run test:browsers # the same board in Chromium + Firefox + WebKit
npm run check:board -- board.json # what the app will say about a board from outside (or: pbpaste | npm run check:board)
npm run smoke:mcp # the MCP endpoint, spoken to as a client (handshake, four tools, every refusal)
npm run lint # oxlintAlso available: npm run docs-facts (the consistency gates alone; :live also checks what production serves),
smoke:map (the Map card in a browser β resolved name, relay failure, Try again), check:map-landmarks (the map
projection checked against real rendered maps), smoke:panels, smoke:qr, smoke:share, smoke:wayback, smoke:canvas and smoke:canvas-import
(board β JSON Canvas, both directions), smoke:spawn (the two-sided wiring panel),
guide:board and map:widgets (regenerate the served contract and the one-page widget map),
render:board -- board.json (render any board in a browser and report what each card actually shows),
check:layouts (no demo board overlaps itself), build:validator (bundle the validator /api/validate runs;
--check fails on a stale one), and update:cim-allow-list, and
audit:verify (re-derive the ImageNote audit's numbers offline β the matcher, the gold set and the calibration behind
RECONCILIATION).
For the mobile reflow β what actually moves when a card above the reader changes size, and the options for it:
node scripts/mobile-reflow-probe.mjs (the measured numbers are in
docs/research/MOBILE-RESIZE-STABILITY.md).
Browser runs need the engines installed with the repo's own playwright-core, and some hosts need a
remote-browser daemon or an explicit engine path β see docs/BROWSER-TESTING.md.
wikibento/
βββ index.html # entry HTML (inline SVG favicon)
βββ vite.config.js # Vite + React plugin
βββ public/ # static assets + dashboard.json (hosted sample config)
βββ docs/ # the documentation set (indexed below)
βββ pipeline/ video/ # the demo-video engine, and this project's script for it
βββ src/
βββ main.jsx # React 19 bootstrap
βββ App.jsx # grid, state, persistence, toolbar, URL boot
βββ App.css # dark theme + all component styles
βββ components/ # AddWidgetPanel, ImportPanel, SharePanel, AboutPanel, BoardNotice, ProjectField,
β # SettingsPanel, ErrorBoundary
βββ lib/
β βββ dashboardConfig.js # format v1: example dashboard + validateDashboard()
β βββ urlState.js # what the URL may claim: one reader, one writer, claim integrity
β βββ borrowedBoard.js # borrowed vs adopted boards, the recovery stash, the notice's rule
β βββ reference.js # a page plus its wiki (enwiki:Weddell Sea), and the one projectβhost mapping
β βββ projects.js # every wiki (364), ordered recency β default β curated β rest
β βββ wikiBox.js # rendering a wiki template: sanitise, scope, rewrite, what a click means
β βββ speech.js # the typed speech value ({type,text,lang}) + voice choice by language
β βββ paramSources.js # validated lookup sources (categories, galleries, articles, QIDs)
β βββ markdown.js # tiny zero-dep Markdown renderer (Text/Markdown widget)
β βββ share.js # URL loading/sharing (?config=, #/d/<base64>)
β βββ qr.js # URL β inline SVG QR code (qrcode-generator)
βββ widgets/
βββ index.js # WIDGET_TYPES registry (add a widget here)
βββ WidgetFrame.jsx # title bar, config panel, load/error/refresh lifecycle
βββ dataSources.js # API fetchers (one per widget, batched where needed)
Almost everything WikiBento shows is fetched by your browser, from Wikimedia directly β the Action and REST APIs, Commons, Wikidata, the Internet Archive all send CORS headers, so no server of ours is involved. A few things cannot work that way, and those go through our own Toolforge server; the full inventory, the reasons and the rules are in docs/PROXIES.md.
| Route | Upstream | Why it needs a server |
|---|---|---|
/api/staticmap |
Wikimedia's map service | it answers a Wikimedia User-Agent with a PNG and a browser with 403, which the browser then refuses as an image |
/api/proxy |
Wikimedia wikis, top.hatnote.com, web.archive.org |
sources that send no CORS headers, and asking for a desktop parse with our own User-Agent |
/api/resolve |
w.wiki |
expanding short URLs server-side; the target sends no CORS |
/api/petscan |
PetScan | quick-intersection ignores max and can return 39 MB β the byte cap and file budget live on the server |
/api/wayback-gallery |
web.archive.org CDX |
no CORS on the CDX side |
/api/ask |
LiftWing LLM | the API key, the system prompt and the spend live server-side |
/mcp |
(nothing β no upstream) | the Model Context Protocol endpoint: four read-only tools (get_catalog, get_board_guide, validate_board, make_board_url) so an agent can read the board contract, write a board, check it and hand back a link without a human carrying JSON between windows β docs/MCP.md |
/api/validate |
(nothing β no upstream) | the board doctor as a service: a board goes in, the same verdict β¬ Import gives comes out, so an outside model can check its own work. It exists server-side because the validator is the app's own code bundled, not a second implementation |
Every route is https, GET, cached where an answer repeats, and bounded: a host allowlist, a streamed byte
cap, a deadline, a per-client rate limit, an in-flight ceiling, and in-memory caches with a size as well as a
TTL. Nothing writes to disk. node scripts/relay-guard-e2e.mjs (also npm run smoke:relay, part of npm test) starts
the real server with tight limits and tries to break it β off-ladder map sizes, a non-allowlisted host, an oversized body,
a burst of 24 requests β and then checks the server is still answering for someone else and its memory is still sane.
| Layer | Choice |
|---|---|
| Framework | React 19.2 (StrictMode) |
| Build | Vite 8.2 |
| Grid | react-grid-layout 2.2.4 + react-resizable 4.0.2 |
| Linting | Oxlint (react + oxc plugins) |
| Charts | Hand-rolled SVG (no chart library used) |
| QR codes | qrcode-generator (client-side, zero-dep; SVG rendered in-app) |
| Pair-built with | The Pi coding agent on DeepSeek V4 |
A substantial part of this project was written that way: the widget implementations and their batched
fetchers, the rate-limit-aware HTTP layer, the docsβcode consistency gates (scripts/docs-facts.mjs), the
widget catalog and this documentation set, and the demo-video pipeline. The agent works in a repository under
review β every change has to pass the test suite and the docs gates, and deploys are made by hand
(Andrew Lih, User:Fuzheado), who directs the work and checks the result in a browser.
WikiBento is smoke-tested per widget in a real browser, against named live assets β the dated record, with
the failures each check was written for, is docs/VERIFIED-WORKING.md. The claims
are checked rather than asserted: npm test regenerates the widget manifest and enforces the cross-document
consistency gates, npm run smoke enforces grid geometry and that every β/β action is reachable at any panel
size, and npm run test:browsers loads a real board in all three engines. npm run test:browsers:demos sweeps every demo board in Chromium, Firefox and WebKit at desktop and phone widths, asserting
that each card renders, that none collapses, that no card shows a bare β in place of data and that no ranking is
empty β checks written for the bugs they then found (an empty box on iPhone, a pageviews card with no count, a 360Β°
widget that had never rendered; ISSUE-100).
An agent can write a board and check it itself. /mcp speaks the Model Context Protocol β four read-only tools
(get_catalog, get_board_guide, validate_board, make_board_url) β so Claude or ChatGPT can read the contract, write
a board, validate it against the app's own rules and hand back a link, with no JSON carried by hand
(docs/MCP.md); npm run smoke:mcp drives the protocol as a client (16 checks, in npm test).
A board written outside WikiBento is checked twice, because "validates" and "renders" are different claims:
npm run check:board -- board.json (and the served /api/validate, which is the same checker) reports what a board
from a chat will do β what blocks it, what the app will repair silently, what it will quietly ignore β and
npm run smoke:boards pastes boards through the app's own β¬ Import panel and requires a card per widget with nothing
left waiting for a value. Both exist because that path had a defect nothing checked: Import silently dropped a
board's params, so every {{param}} card sat waiting, and the demo sweep never noticed because no demo board arrives
that way. The
feature E2Es drive the reading and URL behaviour in a real browser (smoke:iabook, smoke:document, smoke:url,
and smoke:map β four ways into a map card, including geometry over the wire) β and the map geometry has its own
check against real rendered maps and a live Wikidata geoshape (check:map-landmarks: the projection, the card
transform, and a drawn shape classified inside and out). What each of them caught is in
docs/BROWSER-TESTING.md. The same suite holds the dataflow contract honest: an
emitter that publishes prose must declare where it came from, a widget with named channels must say what its bare
id means, a channel a board references must exist, and no widget may hard-code a language list.
Every document, grouped by what you would want it for. The long-form material lives here β the README is the front door.
- Start here β GUIDE (the model + cookbook) Β· TUTORIAL (build a board step by step, then store it on a wiki) Β· EXPORT (getting data, a PDF or the whole board out β and why PNG waits) Β· WIDGET-CATALOG (every widget type, with its API) Β· WIDGET-MAP (every type on one page, grouped by what you must supply β with the gates and the wiring; the picture is widget-map.pdf) Β· JSON-FORMAT (board spec v1 + schema) Β· SCREENSHOTS (dated snapshots of real boards)
- Let an agent do it β the same four capabilities are an MCP server at
/mcp(get_catalog,get_board_guide,validate_board,make_board_url), so Claude or ChatGPT can read the contract, write a board, check it and return a link β docs/MCP.md. And locally:npm run check:board -- board.jsonruns the app's own validation over a board that came from a chat, a notebook or another tool: what will block the import, what the app will repair silently, what it will quietly ignore (docs/JSON-FORMAT.md is the contract it checks). The same checker is served as/api/validate(GET with the board in the URL, or POST it), so a model can call it and fix its own board before handing it over; the contract it checks is/board-guide.md. - Build & extend β WIRING-BOARDS (what travels between cards: sources, consumers,
channels, references β the advanced guide after the GUIDE) Β· ARCHITECTURE Β· WIDGET-DEVELOPMENT
Β· BOARD-COMPOSITION (every widget, wired) Β· DATA-SOURCES
(every endpoint, cap and gotcha) Β· GEOMETRY (paths, polygons and time: GeoJSON in and out, the
intake rules, and why none of it is a dialect) Β· MODULARITY-AND-DATAFLOW Β·
MEDIA-DATAFLOW Β· SCALABILITY Β·
ASK-ARCHITECTURE Β· INTENT-BENCHMARK Β·
ZONES (shipped for flat images, 2026-10-09; the editor, the import and the 360Β° half remain β the
HyperCard button layer, the Commons and Wikidata formats that already carry the areas, the crop rule that never
hides a zone, how a file's own Commons notes become zones (and how their labels become targets, measured), tours
that move the card and choose where a jump lands, the editor, and the honest cost; the
sphere wireframe is a live Pannellum render and the arrival one is real equirectangular geometry; the build is
sequenced in five slices: A has shipped, BβE remain, each with its own gate)
Β· RECONCILIATION (turning a label into a target β and no longer only a plan: the corpus
measurements, a working four-stage matcher, a 100-note gold set and the calibrated publish gate are in the
repo, and
npm run audit:verifyre-derives every number from cached API responses, offline, in ~45 s. What is still to build is the review UI and the one resolver the page box, params and the Ask door would share) - Vocabulary β GLOSSARY (the words this project uses, and the ones it avoids β what
edge to edge means, why full bleed is only a synonym, and the retired spellings that still resolve)
- Why & research β WHY-WIKIBENTO (the case, and the measured ledger) Β·
PHILOSOPHY Β· PARADIGMS Β· WIDGET-MESSAGING
Β· PLUGIN-TRUST Β· TOOL-LANDSCAPE and its
synthesis Β· TOOLFLOW-ANALYSIS Β·
TAPESTRY-EVALUATION Β·
MOBILE-RESIZE-STABILITY Β·
TOOLFORGE-INCIDENT-2026-10-22 (the combined write-up: the PHP-image
block and the
cd //www/jscrashloop, how to tell them apart, and what is measured vs inferred) Β· TOOLFORGE-PHP-IMAGE-BLOCK-MEMO (the PHP half, archived verbatim from a sibling project) Β· IMAGENOTE-FEASIBILITY (the matcher design, measured) Β· IMAGENOTE-AUDIT (9,643 files / 30,032 notes, and a 100-note gold set) Β· GLAMORGAN-WIDGET Β· WAYBACK-REPLAY-LATENCY Β· INTERNET-ARCHIVE (the IA widget family: verified API surface, per-media file conventions, quotas, ranked proposal) Β· DOCUMENT-VIEWER (Commons PDFs/DjVu: page-thumbnail scheme, why framing a PDF is the fragile route, and the viewer shared with IA books)
- Why & research β WHY-WIKIBENTO (the case, and the measured ledger) Β·
PHILOSOPHY Β· PARADIGMS Β· WIDGET-MESSAGING
Β· PLUGIN-TRUST Β· TOOL-LANDSCAPE and its
synthesis Β· TOOLFLOW-ANALYSIS Β·
TAPESTRY-EVALUATION Β·
MOBILE-RESIZE-STABILITY Β·
TOOLFORGE-INCIDENT-2026-10-22 (the combined write-up: the PHP-image
block and the
- Behaviour β URL-STATE (what the address bar may claim, decided action by
action, and the audit that enforces it:
npm run smoke:url) - Ideas β WIDGET-IDEAS Β· DEMO-IDEAS Β· ROADMAP Β· ISSUES Β· LIFELINE-WIDGET (timelines of lives, and comparing two of them β measured Wikidata-vs-prose coverage, what shipped, what is next)
- Testing & ops β BROWSER-TESTING (the suites + engine-install traps) Β· VERIFIED-WORKING Β· DEPLOYMENT and the deploy log Β· AGENT-MEMO Β· BUG-REPORT-ios-safari-fetch Β· AUTHORS
- Tutorial video β TUTORIAL-VIDEO-TOOLING (the pipeline: state, how to re-run, what is missing; plus the ecosystem research behind it) Β· pipeline/README (the reusable engine)
- Bug reports & concrete feature requests β GitHub Issues β
templates for both. A board config (
?config=link, or the/dashboard.jsonexport) plus your browser helps more than anything else. - Ideas & brainstorming β Discussions β Ideas
- Boards you built β Discussions β Show and tell
Thumbnails are their own subject: sizes, the pre-rendered bucket ladder, and which bytes a card actually fetches β docs/THUMBNAILS.md, which also points at the sister project that measured it.
Requests that get picked up are tracked with design notes in docs/ISSUES.md and docs/ROADMAP.md, so you can watch an idea become a widget.
Wikimedia-oriented demo dashboard. Data comes from Wikimedia APIs (CC BY-SA 4.0 content licensing applies to any downstream use of article content; pageview and stat aggregates are public statistics).
