Capstone: Integrating This Into the Existing Astro Site

Romaji to Kana Converter: React & Next.js

Chapter 8 · Capstone: Integrating This Into the Existing Astro Site

Seven chapters have built one real, working thing: a client-side React converter that's genuinely free to call on every keystroke, a second server-comparison box proving exactly why (a real measured ~14ms round trip against a sub-microsecond direct call), and a production build that starts, runs, and survives a restart on its own. This closing chapter answers the question every prior chapter has been building toward: how does this actually reach the user's real, existing Astro-based site?

Three Real Ways to Mount This

OptionWhat it looks likeReal tradeoff
Subdomain romaji.example.com, a fully separate deployment Simplest nginx config, but needs its own DNS record and its own TLS certificate — a real second thing to keep renewed
Path-mounted reverse proxy example.com/romaji/, sitting alongside the existing Astro site One domain, one certificate — but the app itself has to genuinely know it isn't running at the web root anymore
API-only, Astro-native frontend Only /api/convert is kept; a small script embedded directly in an existing Astro page calls it No second React UI to maintain at all — but throws away Chapters 4-5's own real client-side box entirely, and Chapter 5's own real finding that convert() should run client-side, not through an API, in the first place

The path-mounted option is the one worth building out in full — it keeps everything this course has already built, asks the least of the user's own existing infrastructure, and is where the single most interesting real finding in this chapter turns up.

Next.js's Own Real Tool for This: basePath

Next.js's own current, real documentation names exactly this scenario directly: basePath "allows you to set a path prefix for the application," specifically to "deploy a Next.js application under a sub-path of a domain."

// next.config.js module.exports = { output: 'standalone', basePath: process.env.NEXT_PUBLIC_BASE_PATH || '', };

Building a real, minimal project with NEXT_PUBLIC_BASE_PATH=/romaji set and testing every real, relevant URL confirms exactly what basePath actually does — and what it doesn't:

GET /romaji -> 200 (the real, mounted app) GET / -> 404 (the old root no longer serves anything) GET /api/test -> 404 (a raw, unprefixed fetch target) GET /romaji/api/test -> 200 (the real, correct path)
The Real Gotcha — Exactly Where Chapter 5's Own Code Would Break
Next.js's own docs are explicit that next/link and next/router get basePath applied automatically — a <Link href="/about"> genuinely becomes /romaji/about with zero code changes. But this course's own ServerConverter component, built back in Chapter 5, doesn't use either of those — it calls fetch('/api/convert', ...) directly, a plain browser API with no idea Next.js's own basePath config even exists. The real test above proves this concretely: the exact literal string Chapter 5's code already contains, /api/convert, is precisely the unprefixed path that returns a genuine 404 once basePath is set — even the official docs' own next/image component needs its src prefixed by hand for the identical reason. Only two real, first-party helpers get this for free; a raw fetch() call is not one of them.

The Real Fix

The community-documented pattern — confirmed by real testing — is to drive basePath from a single NEXT_PUBLIC_ variable, then read that exact same variable inside the component to prefix the fetch call by hand:

// src/app/components/ServerConverter.tsx -- the one real line that changes const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH || ''; // ...inside the debounced fetch, from Chapter 5: const res = await fetch(`${BASE_PATH}/api/convert`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ romaji: input }), });

Re-running the same real test with this fix applied confirms it resolves cleanly:

GET /romaji -> 200 (page loads, client fetch fires after hydration) GET /romaji/api/test -> 200 {"hello":"from api/test"}
A Direct Payoff of Chapter 7's Own Finding
The docs state basePath's own value "must be set at build time and cannot be changed without re-building as the value is inlined in the client-side bundles" — the identical real behavior Chapter 7 already verified for plain NEXT_PUBLIC_ variables, via a real grep against a built project's own static chunk files. Running that same check here confirms it again: the literal string "romaji" is genuinely present, baked directly into the built client JavaScript, not read live. Moving the app under a subpath and reading an ordinary NEXT_PUBLIC_ variable turn out to be the exact same mechanism underneath — basePath isn't a special case, it's just one more value this course already knows gets frozen at build time.

The nginx Side

With NEXT_PUBLIC_BASE_PATH=/romaji baked in at build time and the fixed ServerConverter deployed, nginx's own job is simply to route requests under /romaji/ to the same PM2-managed process Chapter 7 already set up — no separate process, no separate port range, just one more location block alongside whatever already serves the existing Astro site:

server { listen 80; server_name example.com; # the existing Astro site, unchanged location / { root /var/www/astro-site/dist; } # the Next.js converter, mounted at /romaji/ location /romaji/ { proxy_pass http://localhost:3000/romaji/; proxy_http_version 1.1; proxy_set_header Host $host; } }

Because the app's own basePath is already /romaji, nginx doesn't need to strip or rewrite anything — it forwards the request path exactly as-is, and the Next.js app itself already expects to receive it that way.

A Real Link From the Existing Site

The whole point of this course, stated back in Chapter 1, was never "build a standalone tool" — it was building the thing the site's own real Japanese-language lessons and kanji reference pages could genuinely point to. With the converter now live at /romaji/, a real link from the site's own existing kanji tiles page and from individual Japanese-language lesson pages becomes a concrete, one-line addition rather than a hypothetical:

<a href="/romaji/" class="jp-tool-link"> Try the Romaji → Kana Converter </a>

A learner reading through a hiragana lesson, or hovering a kanji tile on the site's own reference page, is now one real click away from a working conversion tool built specifically for this site — the exact integration this whole course existed to make possible.

Honest Scope Note — What's Still Deliberately Deferred
Kanji conversion itself is still not built. Chapter 6 established the real architecture and the real, measured case for a Redis-backed lookup cache once it's added, but no dictionary and no kanji-aware route exist in the running app today — the tool this chapter mounts converts romaji to hiragana and katakana only, exactly as Chapter 2 through Chapter 5 built it. That remains a genuine, separate project for later, deliberately scoped out from the start.

Chapter Attribution

ChapterWhat it contributed to the finished, deployed tool
1Next.js project setup, and the real architectural framing (one project, both UI and API) this chapter's own single-process deployment cashes in
2The real gojūon/dakuten/handakuten/yōon KANA_MAP, and the fixed longest-to-shortest tokenizer
3Chōon, sokuon, and particle-exception handling — three real bugs found and fixed
4The real /api/convert route, and the measured ~14ms case for calling convert() directly instead
5The real client-side and server-comparison boxes — including the exact fetch('/api/convert') line this chapter had to fix
6The real, measured architecture case for a Redis kanji cache, deliberately not yet built
7The real production build, PM2 process management, and the NEXT_PUBLIC_ build-time-freeze finding this chapter directly reused for basePath
8Comparing three mounting options, the real basePath-plus-fetch-fix, and the nginx config that puts it all live

Hands-On Exercises

Exercise 1

Chapter 5 also built a client-side box that calls convert() directly, with no fetch() call at all. Explain, using this chapter's own real findings, why that box needs no basePath fix whatsoever, while the server-comparison box's own fetch call does.

📄 View solution
Exercise 2

This chapter's own real test showed that GET / returns a genuine 404 once basePath is set to /romaji. A teammate is confused, since the app clearly still works — just under a different path. Explain, precisely, what "the app's own root" actually means once basePath is configured, and why 404 is the honest, correct response rather than a bug.

📄 View solution
Exercise 3

Suppose kanji conversion (Chapter 6) is eventually built, adding a new route at /api/convert-kanji that calls a real Redis instance. Explain what would need to happen for that new route to work correctly once this chapter's own basePath mounting is in place -- would the real Redis calls inside the route handler itself need any changes, or only the client-side code that calls the route?

📄 View solution

Chapter 8 Quick Reference — Course Complete

  • Three real mounting options compared — subdomain, path-mounted reverse proxy, and API-only; the path-mounted option built out in full
  • basePath moves the whole app, API routes included — verified: the old root genuinely 404s once it's set
  • A raw fetch() call is never auto-prefixed — verified: exactly the literal /api/convert string Chapter 5's own code uses breaks with a real 404, fixed by reading the same NEXT_PUBLIC_BASE_PATH the config itself uses
  • basePath is frozen at build time — the same real inlining behavior Chapter 7 already found for NEXT_PUBLIC_ variables generally, reconfirmed here
  • Kanji conversion stays deliberately deferred — this course closes having built exactly what it scoped: a real, fast, working romaji-to-kana converter, genuinely integrated into the existing site