Rendering Content & the Kanji Edge Case

Website Rebuild with Next.js

Chapter 7 · Rendering Content & the Kanji Edge Case

Two of this chapter's three usual questions are already answered by earlier chapters of this course, unchanged. The third needs a real, careful answer here — both Astro Rebuild's and Rails Rebuild's own already-published Chapter 7s explicitly cite "whatever conclusion the Next.js rebuild's own Chapter 7 reached" before this chapter itself existed to reach it.

Routing & Rendering: Already Solved

Kanji's two original constraints — a fixed-depth hierarchy, and no self-contained-fragment convention — never applied to this course's own design in the first place. Chapter 2's arbitrary-depth Prisma schema has no special-cased depth limit, and Chapter 4's dangerouslySetInnerHTML={{ __html: page.body }} renders a kanji page's own inline markup through the exact same path as any other page.

A wrinkle Django faced that this course never does
Django Rebuild's own Chapter 7 found a genuinely new problem: SlugField's default validator rejects non-ASCII characters outright, requiring an explicit allow_unicode=True fix. Chapter 2's own Prisma schema has no equivalent validator at all — slug String is a plain column with no built-in format constraint, so a literal "水" was never rejected in the first place. Nothing to configure here, not because the problem was solved, but because it never existed on this side.

String Safety: UTF-16 Code Units vs. Code Points

JavaScript strings are sequences of UTF-16 code units, not Unicode code points. For most characters — including every kanji in the site's own real, currently-used content, like , , and — the two are the same thing, because those characters live in Unicode's Basic Multilingual Plane (BMP), which fits entirely within a single 16-bit code unit.

console.log('水'.length); // 1 — one code unit, one character console.log([...'水'].length); // 1 — agrees

But some far rarer, historical kanji live outside the BMP, in Unicode's Supplementary Ideographic Plane (CJK Unified Ideographs Extension B and beyond, starting at U+20000) — and those require a surrogate pair, two UTF-16 code units representing one real character.

const rareKanji = '𠀋'; // U+2000B, a real CJK Extension B character console.log(rareKanji.length); // 2 — two code units for one character console.log([...rareKanji].length); // 1 — the spread operator is codepoint-aware console.log(rareKanji.slice(0, 1)); // a lone, invalid surrogate half — genuinely corrupted
A real, documented risk — for a specific, narrow category of character
Naive index-based slicing (.slice(), .substring(), direct indexing like str[0]) operates on code-unit boundaries, not character boundaries. For an astral-plane character, slicing at the wrong offset produces a lone surrogate — not a smaller valid string, a genuinely broken one. This is real and worth knowing, but it only ever applies to the rare category of character that needs a surrogate pair in the first place.

Verified: This Course's Own Pipeline Never Slices a Path String

Low risk isn't luck — it's checked directly against the real code
Chapter 3's path.join('/') operates on an array of already-split URL segments — it concatenates whole segments with a separator, and never touches the internal byte or code-unit structure of any single segment. Chapter 2's prisma.page.findUnique({ where: { fullPath } }) is a whole-string equality comparison — Prisma and the underlying database both treat fullPath as an opaque unit, never reading or writing a sub-range of it by position. Neither operation used anywhere in this course's own routing or rendering pipeline could ever split a surrogate pair, for any kanji — common or rare. This is the specific, verified conclusion Astro Rebuild's and Rails Rebuild's own Chapter 7s already assume: low risk for typical CJK content, and — checked directly here, not just assumed — genuinely no risk at all in this particular pipeline, since nothing in it performs the one kind of operation that could ever trigger the problem.

utf8mb4: Automatic Here Too

Prisma's own generated MySQL migrations default to utf8mb4 with utf8mb4_unicode_ci collation automatically — no manual charset configuration needed anywhere in this course's own schema or connection setup.

A different mechanism than Rails, the same good outcome
Rails Rebuild's own Chapter 7 found rails new --database=mysql has defaulted config/database.yml's own encoding to utf8mb4 automatically since Rails 5.2 — a connection-level default. Prisma reaches the identical outcome through a different mechanism: its own CREATE TABLE migrations bake DEFAULT CHARACTER SET utf8mb4 directly into the table definition itself. Astro Rebuild's own Chapter 7, by contrast, found Drizzle's mysql2 driver needs this set explicitly in the connection config — a real, honest difference between otherwise-similar JavaScript tooling sharing the same underlying database driver.

String Safety, Compared Across the Series

JavaScriptPython (Django)PHP (Laravel)Ruby (Rails)
Default string unitUTF-16 code unitsUnicode code pointsRaw bytesCharacters, per the string's own encoding
Slicing risk for common kanjiLow — most CJK ideographs are one UTF-16 unitNoneReal — substr() can split a multi-byte characterNone
Needed a safe alternative function?No, for typical CJK contentNoYes — mb_substr()No

Stroke-Animation Assets: Still Self-Hosted

Next.js's own equivalent to Django's static-file convention
Real kanji reference pages carry their own self-hosted stroke-order animation assets (per this site's own K1 rule — no third-party CDN dependency). Folded into this course's own project, those assets live under Next.js's own public/ folder, served directly at a stable URL with zero build-step processing needed — the same "keep it simple, keep it self-hosted" outcome Django Rebuild's own Chapter 7 reached via its own static-file convention, reached here through Next.js's own simplest built-in mechanism instead.

Hands-On Exercises

Exercise 1

Using a real astral-plane character (e.g. '𠀋', U+2000B), demonstrate that .length reports 2, that [...str].length correctly reports 1, and that .slice(0, 1) produces a genuinely corrupted lone surrogate. Then repeat the same three checks against a common kanji like '水' and explain why the results differ.

📄 View solution
Exercise 2

Explain, specifically, why path.join('/') and Prisma's where: { fullPath } lookup could never split a surrogate pair — even for a page whose slug is a rare astral-plane kanji requiring one.

📄 View solution
Exercise 3

Run a fresh Prisma migration and inspect the generated SQL migration file. Confirm the CREATE TABLE statement includes DEFAULT CHARACTER SET utf8mb4 automatically, with no manual configuration added anywhere in schema.prisma or the database connection.

📄 View solution

Chapter 7 Quick Reference

  • Routing & rendering — already solved by Chapter 2's arbitrary depth and Chapter 4's dangerouslySetInnerHTML; no Django-style slug validator to work around either
  • UTF-16 code units vs. code points — common kanji are one unit; rare astral-plane kanji need a surrogate pair (two units)
  • Verified, not assumed — this course's own routing pipeline never slices a path string by index anywhere, so the surrogate-pair risk never actually triggers here
  • utf8mb4, automatic — Prisma's own migrations set it by default, via table-level DEFAULT CHARACTER SET rather than Rails' connection-level default
  • Next chapter: API Routes & Server Actions