Contents
NipponDailyWorking on it

Chapter 11

Development

Everything for working on the code: setup, environment, commands, tests, and the rules that keep these docs true. Use pnpm only.

Setup

Node 22.22.3, 24.15 or 26 and newer (Nuxt's supported range) and pnpm.

pnpm install
cp .env.example .env
pnpm dev          # http://localhost:3000

The site needs no configuration: the daily words are in-repo data and the season is worked out in the browser from the date. The data scripts (scripts/build-*.mjs) run through the data:* commands under plain node and need no credentials.

Environment

See .env.example. There is one optional variable; the site URL is read from runtimeConfig.public.

VariableUsed for
NUXT_PUBLIC_SITE_URLOptional canonical origin for canonical and Open Graph URLs and the sitemap; otherwise each request's own origin.

Commands

CommandDescription
pnpm dev / build / start / previewDev server, production build, run the build, preview it.
pnpm test / test:run / test:coverageVitest in watch mode, once, or with coverage.
pnpm lint / format / type-checkESLint and Prettier (both rewrite files: --fix, --write), and tsc --noEmit.
pnpm test:e2eBuild, then run the Playwright browser tests in e2e/ in Chromium, Firefox and WebKit.
pnpm test:e2e:devRun the app with nuxt dev and fail on a Vue warning (e2e-dev/).
pnpm check-qaLint, format, type-check, build and test.
pnpm docs:sync / docs:checkFill, or verify, the README's generated regions.

The data:* commands and pnpm assets:og-font are listed once, with what they read and write, in Adding and fixing words. A test fails if a script in package.json is in neither table.

Tests

Vitest, in the projects vitest.config.ts defines, and Playwright for the browser tests in e2e/.

  • test/unit (happy-dom): components and pages. test/setup.ts mocks #app (useRoute, useRouter, a working useAsyncData, no-op useSeoMeta and useHead); add to it and to test/mocks/app.ts when a page needs another composable.
  • test/server (node; no project name, so scope by path: pnpm exec vitest run test/server): API handlers, the shared/ modules, the data scripts and the server utilities. The word endpoints need no mock because they read in-repo data; their tests freeze the clock with vi.useFakeTimers().
  • test/content (fully offline): every entry and every word-list word against the committed snapshots (Data integrity).
  • test/integration (node): builds Nuxt into a throwaway directory, starts the built server and requests every route over HTTP. It checks that the home page and a day's page hold their word in the HTML, that every part, kanji and chapter page answers, that a day that has not arrived is a 404 and the old links redirect, that the share image is a PNG, and that the cache header is on an answer and not on an error. It has no setup file, because test/setup.ts replaces the global fetch with a mock. To run it alone: pnpm exec vitest run --project integration.
  • e2e (Playwright; the desktop browsers and phones playwright.config.ts lists): pnpm test:e2e builds the app, starts it on its own port and drives it in each. It checks that every page renders with no uncaught error or console error and cannot be scrolled sideways, that today's word opens from the home page, that the colour mode and season picks change the page and are remembered, that the Explore chips narrow the list and write the URL, that any/all agrees with the API, that the calendar moves between months and that a kanji page draws one frame per stroke. The music switch is driven too: it never starts by itself, asks for the season's track (and the next one when the season changes), and keeps the volume in the browser; the tracks are served as a short tone so no browser's codecs matter. Each season's falling or rising particles, glyphs and corner graphic are read back as the browser computes them, in light and dark, and are gone under reduced motion. Playwright needs its browsers (pnpm exec playwright install chromium firefox webkit) and the system libraries behind them; it is not part of pnpm check-qa. The API is rate limited per address and every test comes from one, so keep the tests few.
  • e2e-dev (Playwright, Chromium): pnpm test:e2e:dev starts nuxt dev and visits every page, a word, a kanji and a part, and each season in dark mode, failing on any Vue warning, hydration mismatch, console error or uncaught exception. A production build prints none of these, so this is where a template naming something its script never defines is found. The hints:hydration notes from @nuxt/hints are not counted: they also flag what the app changes on purpose once mounted. The unit test for the docs chapters makes the same check without a browser.

The integration test builds the app, so pnpm test:run passing does prove it builds and serves; run pnpm build on its own only to see the build's output. CI (.github/workflows/webpage-test.yml; every job sets up through .github/actions/setup, which names the pnpm version once and takes Node's from engines in package.json) runs pnpm run test, pnpm test:e2e and pnpm test:e2e:dev as separate jobs on pushes to main and on pull requests targeting it.

Tests that guard against drift:

  • no-future-leak: nothing under app/ imports the entries.
  • seasons-css-sync: shared/seasons.ts against the CSS.
  • icons: every i-heroicons-* name used has a path in app/data/icons.ts.
  • og-font: the share-image fonts can draw every word, reading and meaning, and nearly every headline.
  • contrast: text on colour in every season and mode against WCAG AA (fills, shaded text and the muted grey of captions and labels, on the canvas, a card, a tint and a badge), that no class sets text in a 500 step or in a neutral step below AA, that the focus ring, the search box's border, a chip's border, the music switch's off track and a pressed chip's border reach 3:1 and that the classes shipping are the ones measured, and that text reaches AA under each seasonal backdrop wash.
  • single-source: a fact owned by one module (Japan's offset, the localStorage keys, the JSON envelope, the JLPT levels, the page chrome) is not written out again beside it.
  • principles: the principles no other test sees. Nothing leaves the app (no other origin, script, frame, download, beacon or server write) and no reader data (no cookie, no storage but the named localStorage keys, no address read but by the rate limiter).
  • server-only-modules: the import rule's list in shared/server-only.ts against the imports, so a new module that reads the entries cannot go unlisted.
  • docs-sync: the docs against the code (see below), including the Architecture chapter's lists of modules, components, pages and redirects.
  • docs-claims: the figures the docs quote against their source: the Node range against Nuxt's, the loudness in shared/audio.ts against ffmpeg's measure of the tracks (skipped without ffmpeg), the download sizes in Adding and fixing words against the pins, a chapter that lists the same backers as another, a link to the Roadmap while it is empty, and a word written twice.

Releases

A release is a git tag on main plus a GitHub Release; the tag is the only record of the version, so package.json carries none. The Releases page is the changelog, and there is no CHANGELOG.md.

  • Versions are SemVer, read for a product. Major: what the app is, or something readers could use, changed or was removed. Minor: new features or data. Patch: fixes only.
  • Notes are written by theme, starting from gh release create --generate-notes and trimmed to Features, Data, Docs and Maintenance. Dependency bumps become one Maintenance line.
  • A major opens with a Breaking or Pivot line saying what was removed and which earlier tag still has it. The tags are the archive, not long-lived branches.
  • Published tags are never moved or deleted. Fix a mistake in the notes with gh release edit.
  • Run pnpm check-qa on the commit being tagged.

Keeping the docs true

These docs are the pages under app/pages/docs/. There is no separate copy in the repo, so a chapter is the only place its topic is written.

  • Add a chapter by writing app/pages/docs/<slug>.vue around <DocsBook slug="…"> and listing it in shared/docs.ts. Title, summary, path, contents, previous and next links, the home page and the sitemap all follow from that one entry. Figures are DocDiagram specs: boxes on a grid joined by arrows, drawn as inline SVG in the season's colours.
  • One owner per topic. A chapter either owns a topic or links to the one that does: Core theme owns the principles; Architecture the layout, the import rule and the single sources of truth; Daily words the entry, dates and derived pages; API the endpoints; Data integrity the evidence and checks; Adding and fixing words the procedures; this chapter setup, commands and tests.
  • Facts live in code. Attribution is shared/sources.ts, routes are shared/endpoints.ts, seasons are shared/seasons.ts, the limits are shared/limits.ts, the Node range and framework versions are package.json, and the word range comes from data/words/ (GET /api/catalogue). The pages read them. Never type a source URL, a licence name, a route list, a word count, a version, a limit or a size into a chapter or the README. When a chapter needs a fact, in this order: read it from the module that owns it (move the constant into shared/ first if it is private, and use countWord() for a count in words); generate it where the page is Markdown, with a pnpm docs:sync region; and only when the owner cannot be imported (a pin in a script, a measurement of a file) type it once and add a case to test/server/docs-claims.test.ts that checks it against the owner.
  • pnpm docs:sync fills the generated regions of the README (the attribution, the Node range, the stack line, the JLPT levels, the vocabulary layers and the chapter titles, between <!-- docs:begin … --> markers, never edited by hand). pnpm docs:check fails if it is stale.
  • Change a behaviour, change its chapter in the same commit. test/server/docs-sync.test.ts catches a hand-typed range or count, a source URL or licence, a route that doesn't exist, a pnpm command that isn't a script, a path in <code> that isn't in the repo, a chapter list that disagrees with the page files, a script, environment variable or composable the book does not mention.
  • Wrap every command, path and identifier in <code> (or backticks in the strings a table renders through RichText), never bare text.

© 2025 - 2026 NipponDaily. Released under the Apache-2.0 License.

Origins quoted from Wiktionary (CC BY-SA 4.0) via Kaikki.org; meanings & kanji from JMdict, KANJIDIC2 & KRADFILE (EDRDG, CC BY-SA 4.0) — how we check it