How it's built
A static Astro site with a few React islands, styled with Tailwind and served from Cloudflare. All of the content is checked before a single page is built.
The stack
- Astro 6, building a fully static site.
- React for the parts that need state in the browser.
- Tailwind CSS 4 for styling.
- TypeScript in strict mode throughout.
- Cloudflare Workers to serve the built files.
Every page is built ahead of time
Running the build turns the content into plain HTML files: the home page, every subject, category and deck page, every study screen, the Play pages, these docs, and a 404 page. There’s no server-side code, no database and no API. Nothing is fetched from other sites while you browse.
Most of each page is plain HTML. A few parts are React islands, components that come alive in the browser:
- the study screen, with its swipe deck and settings sheet
- the Play page’s picker, and the mixed session that deals hands
- the Add to favorites and Add to mix buttons on deck pages
- the search palette
- the light and dark button
- the Learn through chips
Smaller jobs use a few lines of plain script instead: the word-list filter, the header’s glass effect when the page scrolls, the home page’s demo, the header’s mix badge, the home page’s Continue link, and the Cleared badges on tiles.
Content is checked at build time
Cards, decks, categories, subjects, languages and packs are JSON files in Astro content collections (see Subjects, languages and decks). Each collection has a schema written with Zod, so a missing field, a wrong type or a reference to a file that doesn’t exist stops the build with a message saying where.
Some rules span several files, such as “every back is in a language its subject offers”, so a second pass checks those once everything is loaded. It also checks spelling rules like the ʻokina, and the words allowed in a part of speech. Any problem fails the build. See Checks at build time for the list.
These pages are content too: Markdown files in their own collection, rendered with the same layout as the rest of the site.
Search without a server
The build also writes /search-index.json: every studyable deck and card in one
file, with short field names to keep it small. The search palette fetches it the
first time it opens and does all of its matching and ranking in the browser. The
word-list filter uses the same matching rules. See Search.
Play without a server
Every playable deck is also written out on its own, as a small JSON file such as
/data/decks/chinese/hsk-1/deck-01.json. The Play page and mixed sessions are
built with a light list of every subject, category, deck and pack, with no cards
in it. A session then fetches only the decks its current hand draws from. Your
mix, packs, favorites and best scores are read from your browser. See
Play and packs.
Moving between pages
Astro’s client router swaps pages without a full reload, with a short fade and rise. The header stays in place across swaps, and a tile’s title morphs into the heading of the page it opens. Light or dark mode and the answer language are applied by a small script before each page draws, so neither flashes.
The home page demo
The “How it works” demo on the home page is driven by your scroll position. A small script turns the scroll into two numbers, and CSS works out every card’s position, flip and swipe from them. There’s no animation library. With reduced motion turned on, or without JavaScript, the demo is a static row of the same five steps.
Hosting on Cloudflare
The built files are served by Cloudflare Workers as static assets, with no Worker script in front of them.
- Addresses have no trailing slash, such as
/chinese/hsk-1. - An unknown address gets the 404 page, with a 404 status.
- Scripts, styles and fonts are fingerprinted, so they’re cached for a year.
- Every branch gets its own preview address before it’s merged.
Fonts
Text is set in Geist, with Geist Mono for readings and labels, and Noto Serif SC for Chinese characters. Astro downloads them during the build and serves them from the site itself.