Contents
NipponDailyHow it is built

Chapter 3

Architecture

NipponDaily is a Nuxt 4 app (Vue 3, TypeScript, pnpm) with a small Nitro API. The words are JSON in the repo, served by date and never before their day arrives; the pages are rendered on the server, so the HTML a crawler gets already holds the word.

Overview

  • The entries are in-repo data: data/words/YYYY-MM.json, one entry per day, no gap between the first day and the last, running May 2005 to February 2027 (7959 words). Only the headline is hand-written; everything else is generated from sources (Data integrity).
  • A day is open once midnight in Japan has passed. The API, the sitemap and every derived page read only open days.
  • The server stores nothing. The words are in-repo data and the season is worked out from the date in the browser, so the app needs no datastore and no configuration.
  • Nothing about a reader is stored by the app. No accounts, and no request to a third party: the fonts are served from the app. The server sees a request only to answer it. The rate limiter counts addresses in process memory only, 60 seconds at a time, and writes them nowhere.
Server onlyHTML$fetchin-processopen days onlynever importsReaderPagesapp/ (Nuxt, SSR)APIserver/ (Nitro)Modulesshared/words, parts…Entriesdata/words/*.json
A page asks the API in-process; only the API reads the entries, so no upcoming word reaches the browser

Layout

app/        pages/, components/, composables/, utils/, data/, assets/css/tailwind.css
shared/     code imported by both app/ and server/ (the ~~/shared alias); not auto-imported
server/     api/ (handlers), routes/ (sitemap, robots, share images), middleware/ (rate limit), plugins/ (caching), utils/, assets/og/ (share-image fonts)
scripts/    data builders run with node: reference snapshots, etymology pins, example sentences, pitch accents, kanji records, strokes, kanji shapes, entry generator
data/       word-plan/ (hand-written), words/ (generated), reference/ (generated evidence)
types/      shared TypeScript shapes (index.ts)
test/       the Vitest projects (see Development)
e2e/        Playwright browser tests of the built app
e2e-dev/    Playwright run against nuxt dev, failing on a Vue warning

The shared modules

ModuleRole
words.ts
server and tests only
The catalogue (WORD_ENTRIES), JST date logic (todayJst…), entryForDate, lapEntryForDate, payloadFor, calendarForMonth.
parts.ts
server and tests only
partsIndex(today) and partDetail(text, today): the morpheme index, derived from the entries' morphemes.
kanji.ts
server and tests only
kanjiIndex(today) and kanjiDetail(char, today): KANJIDIC2's record of each kanji an open word is written with (data/reference/kanji.json), its KanjiVG strokes where the counts agree (data/reference/strokes.json), the shapes KRADFILE lists it as containing (data/reference/components.json) and the open words that use it.
explore.ts
server and tests only
exploreWords(filters, today, limit?): search and filters (several choices, any or all) with facet counts, listing only the newest limit matches. exploreCalendar(month, filters, today): one calendar month marked against the same filters, with per-month counts.
related.ts
server and tests only
relatedWords(entry, today): the open words most like one entry. referencesFor(entry, today): where the words JMdict points its sense at stand among the open words.
patterns.ts
server and tests only
patternsFor(today): counts across the open words, including rendaku.
sitemap.ts
server and tests only
sitemapXml() and robotsTxt(): open days only.
og-card.tsogCard(entry, glyphs): the share image of one word as a tree satori draws, in its day's season. Takes the entry as an argument, so it is data-free.
catalogue.tsThe written range and count, computed from entry dates. Data-free.
explore-query.tsReads and writes the Explore filters as a URL query (comma-joined lists). Data-free.
word-labels.tsWORD_STRATA, WORD_PROCESSES, POS_GROUPS and FREQUENCY_GROUPS labels and definitions, posGroupsOf(), frequencyOf() and isHedged(). Data-free.
meanings.tsservedVocab(): the corrections and enrichments applied to a word-list entry (with the row overrides in scripts/lib/word-list.mjs).
seasons.tsThe seasonal presets: SEASONS, SEASON_IDS, DEFAULT_SEASON, seasonForDate().
sources.tsData sources, licences and credit lines.
routes.tsROUTES and API_PREFIX: every path the server answers, for the composables, the rate limiter, the cache rule and robots.txt.
endpoints.tsWhat each route returns, the rate limit and the CDN cache rules.
docs.tsThe chapters of this book.
server-only.tsThe import rule: SERVER_ONLY_MODULES, the modules app/ may not import because they read every entry. A test derives the list from the imports and fails when it is wrong.
japanese.tsWhat counts as a kanji (KANJI_CHARS, kanjiIn(), hasKanji()) and katakanaToHiragana(), for the kanji records, the search and the data scripts.
jlpt.tsJLPT_LEVELS, JLPT_RANGE and the names of the level snapshots (referenceFile()).
stack.tsstackOf(): the Node range and framework versions, read from package.json.
count.tscountWord() and andList(): a count (“four”) and a list of names as prose says them, so a chapter can say how many, and which, without typing them.
audio.tsThe music's loudness and crossfade: BGM_LOUDNESS_LUFS, BGM_CROSSFADE_MS.
limits.tsThe numbers the rules turn on: MAX_PART_LENGTH, MAX_QUERY_LENGTH, the Explore page size, the related-words limit, weight and minimum score, MAX_EXAMPLES, STROKE_SQUARE and the Patterns combination sizes and example limit.
jst.tsJapan's calendar: JST_OFFSET_MS, DAY_MS, toJst().
storage-keys.tsSTORAGE_KEYS, the only keys in a reader's localStorage.

The app

  • Pages: / (today's word), /words (month calendar), /words/<date>, /explore, /patterns, /parts, /parts/<text>, /kanji, /kanji/<character>, /kana, /docs/* (this book) and a catch-all that answers a real 404 and is noindex. Old links to /game, /learn/** and /vocab/** redirect to / (routeRules in nuxt.config.ts).
  • Composables are useAsyncData-based, so the server renders with data. useApiData is the one place that fetches, turns a failure into a message and, on the server, into a 404; useDailyWord, useWordCalendar, usePartsIndex, usePart, useKanjiIndex, useKanji, useExplore, usePatterns and useCatalogue are each a call to it. useRelatedWords alone fetches by itself, because its failure must never make the page a 404. Besides these are usePageSeo (with useSiteUrl, the canonical origin), useSiteTheme, useBgm and useDropdown (the open state of the header's season and music panels).
  • Components: WordEntryView (one entry, with its pitch accent), KanjiStrokes (a kanji's strokes, one frame each), RelatedWords, WordReferences (the words JMdict points a sense at), SoundAlikes (the words read alike, with their accents), SameDayWords (the words of the same day in other years), WordFilters (the filter form shared by Explore and the calendar), MonthPicker (the calendar's year and month jump), AppHeader, AppFooter, AppShell (the header, backdrop and footer every page sits in), DocsBook (a docs page), DocDiagram, SeasonButton, BgmControl, SeasonalEffects, TrendingFallback (the generic fetch-error card), the education charms of /kana (OmamoriCharm and EmaPlaque), the book's own parts (DocsContents, CatalogueRange, RichText for the light markup in tables, ColorSwatchBadge and ErrorStateHeading), and small local U*.vue primitives that mimic the @nuxt/ui API (the project does not depend on it).
  • SEO: every page calls usePageSeo (title, description, canonical, Open Graph, noindex for error states). URLs are absolute: NUXT_PUBLIC_SITE_URL, else the request origin.
  • SSR errors: a server fetch that fails with 400/404 makes the HTTP response a 404. A skeleton shows only while a client-side fetch is pending; TrendingFallback shows on failure (all the states).

Caching

The site has no per-reader state, so every successful page and API answer is the same for everyone until the next midnight in Japan, when the day's word opens. server/plugins/day-cache.ts marks each successful GET with s-maxage set to the seconds left until then (server/utils/day-cache.ts), so a CDN serves it without running the app and the copy expires at the moment a new word opens. It sets no stale-while-revalidate, which would show yesterday's word after the day had turned, and it never marks an error. The exception is the share image, which never changes once its day has opened and so keeps longer; its rule is in shared/endpoints.ts and the API chapter. The season is applied in the browser, never in the HTML, which is why a cached page is safe. Without a CDN that honours s-maxage every request runs the app, which is correct but slower.

This saves invocations, not computation: with every entry in memory a patterns, parts or explore answer takes a few milliseconds, so no in-process cache is kept.

Conventions

  • No service layer. The server holds no state of its own and calls nothing outside the process: the daily words are in-repo data, so handlers read them through shared/.
  • Configuration is one optional setting, the public site URL, read from runtimeConfig.public by siteOrigin() in server/utils/site-url.ts. The data scripts run under plain node and need no credentials.
  • Types live in types/index.ts.
  • State is local (ref, computed); there is no global store. Only color-theme, season-choice, bgm-volume ever sit in a reader's localStorage.
  • One implementation. Before writing a second copy of a handler, composable, page shell or script step, use or extend the helper that already does it: ok, notFound and parseQuery for API answers (server/utils/), useApiData for fetching, AppShell for a page's header, backdrop and footer, and scripts/lib/ for the data scripts. A number or name the rules turn on goes in the shared/ module that owns it (see below). test/server/single-source.test.ts fails when one of those facts is typed again beside its owner.
  • Package manager is pnpm only.

Single sources of truth

A fact is written once, in code, and everything else reads it. Prose that explains something stays hand-written, once per audience, and points here instead of restating the fact. A number, a version, a count, a size or a list of names in prose is a fact, so it is generated, not typed (the Development chapter says how). The rule (often called DRY) applies in these places:

  • Code. One implementation per job, and a constant the rules turn on lives in the shared/ module that owns it (see One implementation above).
  • Data. Entries in data/words/ and data/reference/ are generated, never edited by hand. A wrong value is fixed in its source or in the parser, then regenerated (Adding and fixing words), and a test fails when a committed entry differs from what the generator derives (Data integrity).
  • Facts the pages state. Sources, routes, seasons, the word range, the Node range and framework versions, the limits and the music's loudness are read from code or data when the page renders, or when pnpm docs:sync fills the README, as the table below shows. A figure whose owner cannot be imported, such as a download size pinned in a script, is typed once and checked against that owner by a test.
  • Docs. One chapter owns each topic and the others link to it (Development).
FactWritten once inRead by
Data sources, licences, credit lines, the revision permalinkshared/sources.tsThe footer, each entry's citation, Data integrity, the README
HTTP routes: the pathsshared/routes.tsThe composables, the rate limiter, the cache rule, robots.txt, the share-image link and shared/endpoints.ts
HTTP routes: what each returnsshared/endpoints.tsThe API chapter
Word range and countdata/words/GET /api/catalogue (via shared/catalogue.ts), which every chapter that quotes the range reads
Season palettes, months, motifsshared/seasons.tsThe CSS (a test keeps it in step), Seasons, Colour and shape
Japan's calendar: the offset and the length of a dayshared/jst.tstodayJst(), seasonForDate(), the cache lifetime in server/utils/day-cache.ts
The rate limit and the CDN cache rulesshared/endpoints.tsThe rate limiter, nuxt.config.ts, the share-image route, the API and Seasons chapters
The numbers the rules turn on (related words, examples, part length)shared/limits.tsThe code that applies them, the tests and the Daily words chapter
The `localStorage` keysshared/storage-keys.tsThe buttons and composables that write them, the inline script in nuxt.config.ts, the docs
The Node range and the framework versionspackage.json (read through `shared/stack.ts`)The Architecture and Development chapters, the README
The music's loudness and crossfadeshared/audio.tsThe player, the Seasons chapter, the test that measures the tracks
The JLPT levelsshared/jlpt.tsThe Explore filters, the data scripts, every chapter that names the range
Chapters, titles, summaries, ordershared/docs.tsThe contents, each chapter's heading and SEO, the home page, the sitemap

To add a data source, add it to shared/sources.ts; it then appears in Data integrity and the README. The footer names only the two sources whose licence calls for a visible credit, so add a line there by hand if a new one needs it. To add an endpoint, add the handler under server/api/ (or server/routes/), its path in shared/routes.ts and an entry in shared/endpoints.ts; a test fails until both exist. How the docs are kept in step is in Development.

© 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