The shared chess kit in content/shared/chess turns a PGN into a replayable
game viewer and into captioned position diagrams. One parsed game feeds every
figure, so the moves, positions, arrows, and exported PGN cannot drift apart.
Use a Lichess embed instead when readers
need variations, engine analysis, or a Study's own annotations.
Write the game once
Keep each game in a post-local module. parseGame runs during generation: it
replays every move under the PGN's Variant (standard chess when absent) and
fails the build with the first illegal move, such as
Illegal or unsupported move 7.Kxd3 in antichess.
// content/posts/<slug>/scripts/games.ts
import { parseGame } from '../../../../../shared/chess';
export const E4_B5 = parseGame(`
[Variant "Antichess"]
[Result "0-1"]
1. e4 b5 2. Bxb5 Nf6 { The knight attacks e4. [%cal Gf6e4] } 3. Bxd7 Nxe4 0-1
`);Supported PGN input:
- Tags. Any tags are kept.
FENandSetUpstart from a position, and placeholder values such as?are dropped from the published PGN. - Result. If the final position is decisive, a declared result must agree
with it. A result of
*is filled in from that position. - Comments. Comment text appears in the move list.
- Shapes. Lichess
[%cal]arrows and[%csl]squares are drawn on the board while that move is shown. - Not yet supported. Variations fail the build. NAGs are ignored, and so are variants that need pockets, such as crazyhouse.
parseGame(pgn, { orientation: 'black', locale: 'en' }) flips the board and
switches the interface labels from Chinese to English.
Show the game
Import the kit from a post component. The kit's styles and browser controller then load with that article automatically, and only with it. See the architecture contract.
import { ChessGame } from '../../../../../shared/chess';
import { E4_B5 } from './games';
const MainLine = () => (
<ChessGame
id="e4-b5"
game={E4_B5}
title="1.e4 b5 主线"
description="逐步查看黑方如何引导清扫。"
caption="1.e4 b5 主线:从初始局面逐步查看双方走法。"
result="黑方无棋可走,获胜"
/>
);
export const POST_COMPONENTS = { MainLine };result explains the final result next to its token. ply chooses the first
position shown; the default is the starting position. The article no longer
needs its own PGN block, because the viewer includes one.
What readers get
| Situation | Layout and behavior |
|---|---|
| Column at least 480px wide | The board is on the left. The panel on the right matches the board's height: a scrolling move table with comments across both columns, the result row, and a control bar along the bottom. |
| Phones | The board comes first, then the control bar, then a single line of moves that scrolls sideways. The current move's comment appears below that line. |
| Stepping | The current move stays centered inside the move pane, which fades at its scrolled edges. The page itself never scrolls. |
| Keyboard | Clicking the board focuses it. ←/→ step one move, ↑/↓ a full move, and Home/End jump to the ends, from anywhere in the viewer. The move list is a single tab stop. |
| PGN | The PGN button swaps the move pane for the PGN, which uses the site's code block and its Copy button. |
| No JavaScript, print | The static board, the full move list, and a PGN disclosure. |
| Markdown export | The board description with its FEN, then the PGN as a fenced pgn block. |
Draw positions from the same game
ChessDiagram shows the position after a number of half-moves (ply). Number
them as the game does: ply={9} is the position after 5.Bxf7, and
moveText(game, 2, 9) prints the moves between positions 2 and 9 for a
caption. moveArrows(game, plies) draws the moves that led to the listed
positions; with more than one arrow, each is labeled with its move number.
<ChessDiagram
id="bishop-run"
game={E4_B5}
ply={9}
title="白方 f1 象被迫连续吃子的路线"
description="白象依次吃 b5、d7、e8、f7。"
arrows={moveArrows(E4_B5, [3, 5, 7, 9])}
marks={[{ square: 'b5', role: 'target' }]}
caption={`${moveText(E4_B5, 2, 9)}:白象每吃一次,就停在下一个必须吃的位置上。`}
/>Pass fen="8/p7/8/8/8/8/P7/8 w - - 0 1" instead of game and ply for a
standalone position.
Arrow roles differ in pattern or weight as well as color, and match the Lichess colors:
| Role | Lichess | Drawing | Typical use |
|---|---|---|---|
path |
green | solid sage | moves played, in order |
option |
blue | dashed blue | alternatives that were available |
threat |
red | thin solid red | captures or attacks to watch |
idea |
yellow | dotted ochre | plans and ideas |
Square marks are target, a red outline (Lichess red), and area, an ochre
tint (other colors).
Check the result
After changing a game or a diagram, run pnpm generate:data to replay the
PGN. Then inspect the served article in both themes, at desktop width and at
phone width. Finally, run the checks from testing; the kit's
unit tests are in scripts/lib/chess-kit.spec.mts, and the article's browser
tests are in e2e/antichess.spec.ts.