|
| 1 | +--- |
| 2 | +title: "chess-widget" |
| 3 | +date: 2026-05-28 |
| 4 | +description: "An embeddable chess game analysis widget. PGN in, annotated replay with Stockfish 18 eval, move classification and bookmarks out. Built to scratch a chess.com itch." |
| 5 | +tags: |
| 6 | + [ |
| 7 | + "chess", |
| 8 | + "stockfish", |
| 9 | + "rails", |
| 10 | + "postgres", |
| 11 | + "web-components", |
| 12 | + "no-build", |
| 13 | + "javascript", |
| 14 | + "cucumber", |
| 15 | + ] |
| 16 | +--- |
| 17 | + |
| 18 | +# Chess Widget |
| 19 | + |
| 20 | +I started playing chess "seriously". Decent against bots, still pretty low aura against humans, practicing. I wanted to embed annotated games on my page. The chess.com share widget is nice but not customizable and ties you to their player. I could not find a widget that did what I wanted, so I built my own. |
| 21 | + |
| 22 | +What I wanted from it: |
| 23 | + |
| 24 | +- Simple. Paste PGN, get a self-contained block I can drop into any HTML page. |
| 25 | +- Customizable via CSS. People should be able to embed it in their blog and have it match their theme. |
| 26 | +- Real analysis: blunders, mistakes, checkmates, move-by-move eval, sound on moves. |
| 27 | +- Jump to key moves. Bookmarks for every blunder and mistake so you skip the boring parts. |
| 28 | +- In control. No dependency on an external service at runtime, stateless, embeddable anywhere. |
| 29 | + |
| 30 | +Repository: [https://github.com/cb341/chess-widget](https://github.com/cb341/chess-widget) |
| 31 | + |
| 32 | +### chess.com's widget |
| 33 | + |
| 34 | + |
| 35 | + |
| 36 | +### lichess embed |
| 37 | + |
| 38 | + |
| 39 | + |
| 40 | +### ChessBase / Fritz embed |
| 41 | + |
| 42 | + |
| 43 | + |
| 44 | +### chess.cb341.dev widget (MINE) |
| 45 | + |
| 46 | + |
| 47 | + |
| 48 | +## Architecture |
| 49 | + |
| 50 | +Two parts, cleanly separated: |
| 51 | + |
| 52 | +- **Widget.** A custom element `<chess-widget>`, no build step, no framework. Reads a precomputed analysis JSON payload embedded in the page and renders the board, stepper, eval chart and bookmarks. Stateless. Makes zero API calls at runtime. Does not depend on `chess.cb341.dev`. The host page brings the payload, the widget renders it. |
| 53 | +- **Analysis.** A Rails service that takes a PGN, replays SAN to FENs, shells out to a local [Stockfish 18](https://github.com/official-stockfish/Stockfish) binary for per-position evaluation, classifies moves and emits the widget-ready JSON. |
| 54 | + |
| 55 | +PostgreSQL is purely for bookkeeping on the analysis side. Games are keyed by a deterministic SHA-256 prefix of the PGN, so submitting the same game twice is a no-op. The widget itself has no idea Postgres exists. |
| 56 | + |
| 57 | +The widget could eventually opt in to live calls against `chess.cb341.dev` (on-the-fly analysis from a PGN string), but that is not a dependency, it is a future enhancement. |
| 58 | + |
| 59 | +## Hosting |
| 60 | + |
| 61 | +- [chess.cb341.dev](https://chess.cb341.dev). Rails analysis app, hosted on [deplo.io](https://deplo.io) |
| 62 | +- [Stockfish 18](https://github.com/official-stockfish/Stockfish) runs alongside Rails in the same container |
| 63 | +- PostgreSQL for bookkeeping of analyzed games |
| 64 | + |
| 65 | +## Compression |
| 66 | + |
| 67 | +The analysis JSON for a 45-move game is not tiny. Embedding it raw in the HTML is wasteful, so the payload is gzipped, base64 encoded and dropped into a `<script type="application/x-gzip-json">`. The widget decompresses it in the browser using the built-in `DecompressionStream` API. No external libraries, works everywhere modern. |
| 68 | + |
| 69 | +```js |
| 70 | +async function decompressJson(base64) { |
| 71 | + const bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0)); |
| 72 | + const stream = new Blob([bytes]) |
| 73 | + .stream() |
| 74 | + .pipeThrough(new DecompressionStream("gzip")); |
| 75 | + const text = await new Response(stream).text(); |
| 76 | + return JSON.parse(text); |
| 77 | +} |
| 78 | +``` |
| 79 | + |
| 80 | +## Testing |
| 81 | + |
| 82 | +High-level Cucumber specs drive both the analysis service and the rendering paths. I do not care much about low-level unit tests here. The value is in the end-to-end shape of the response and the rendered output. |
| 83 | + |
| 84 | +```gherkin |
| 85 | +Feature: Server-side chess analysis |
| 86 | + The analysis app turns a pasted PGN into text analysis and widget-ready JSON. |
| 87 | +
|
| 88 | + Background: |
| 89 | + Given the sample Chess.com PGN from the project prompt |
| 90 | +
|
| 91 | + Scenario: Analyze the sample game |
| 92 | + When I submit the PGN to the analysis app |
| 93 | + Then I see progress while Stockfish analysis is running |
| 94 | + Then the response includes parsed game metadata |
| 95 | + And the response includes 46 board positions |
| 96 | + And the response includes 45 analyzed moves |
| 97 | + And move 11 for White is marked as castling |
| 98 | + And move 23 for White is marked as checkmate |
| 99 | + And every position includes an evaluation bar |
| 100 | +
|
| 101 | + Scenario: Render a plain text analysis |
| 102 | + When I submit the PGN to the analysis app |
| 103 | + Then the text analysis includes Unicode chess pieces |
| 104 | + And the text analysis includes compact annotations such as "!", "?!", "??", or "!!" |
| 105 | + And the text analysis includes a text evaluation bar |
| 106 | +
|
| 107 | + Scenario: Render a Markdown analysis response |
| 108 | + When I submit the PGN to the analysis app |
| 109 | + Then the Markdown analysis includes Unicode chess pieces |
| 110 | + And the Markdown analysis includes compact annotations such as "!", "?!", "??", or "!!" |
| 111 | + And the Markdown analysis includes a move table |
| 112 | + And the Markdown analysis includes a Markdown-safe evaluation bar |
| 113 | +
|
| 114 | + Scenario: Use Stockfish when available |
| 115 | + Given Stockfish 18 is available in the container |
| 116 | + When the analysis app evaluates a position |
| 117 | + Then it asks Stockfish for a UCI evaluation |
| 118 | + And it falls back to material evaluation if Stockfish fails or times out |
| 119 | +
|
| 120 | + Scenario: Review saved analyses |
| 121 | + Given at least one game has been analyzed |
| 122 | + When I open the analyses index |
| 123 | + Then I see the saved games |
| 124 | + And I can open a saved game |
| 125 | + And I can inspect its metadata, moves, evaluations, and board snapshots |
| 126 | +``` |
| 127 | + |
| 128 | +The Stockfish fallback scenario is the one I care about most. If the binary is missing or hangs, the service returns a material-only evaluation and flags the result as approximate, instead of failing the request outright. |
| 129 | + |
| 130 | +## Agentic Workflow |
| 131 | + |
| 132 | +First project I built almost entirely in an agentic manner. I wrote the spec in Markdown and mostly stayed high-level: architecture, route layout, widget UX, what the analysis payload should look like. I nudged the LLM into the decisions that mattered. The database schema, the shape of the widget's custom element API, how bookmarks should hang off the move list without coupling to it. I did not read every line of code. |
| 133 | + |
| 134 | +## Future Development |
| 135 | + |
| 136 | +- Comments in analysis mode. Annotate a move and the widget will display the comment when you land on that ply. |
| 137 | +- Threat lines. Show the engine's suggested continuation as an overlay on the board. |
| 138 | +- Puzzle mode. Pick a position from a game and let the reader try to find the best move before revealing it. |
| 139 | +- Piece and board image customization. Swap piece sets and board themes via CSS variables, no rebuild. |
| 140 | + |
| 141 | +## Demo |
| 142 | + |
| 143 | +Live demo at [chess.cb341.dev/about](https://chess.cb341.dev/). A game not particularly proud of but shows what the widget is about. |
0 commit comments