<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Haruki Tanaka</title>
    <description>The latest articles on Forem by Haruki Tanaka (@kaomojiya).</description>
    <link>https://forem.com/kaomojiya</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3765864%2Ff5f3e0da-074b-44bf-9dc7-e1560ac13b86.png</url>
      <title>Forem: Haruki Tanaka</title>
      <link>https://forem.com/kaomojiya</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/kaomojiya"/>
    <language>en</language>
    <item>
      <title>Bringing Microsoft SAM Back to Life: How SAPI4 TTS Works in the Browser</title>
      <dc:creator>Haruki Tanaka</dc:creator>
      <pubDate>Tue, 24 Feb 2026 20:58:18 +0000</pubDate>
      <link>https://forem.com/kaomojiya/bringing-microsoft-sam-back-to-life-how-sapi4-tts-works-in-the-browser-3ej7</link>
      <guid>https://forem.com/kaomojiya/bringing-microsoft-sam-back-to-life-how-sapi4-tts-works-in-the-browser-3ej7</guid>
      <description>&lt;h2&gt;
  
  
  Do You Remember That Voice?
&lt;/h2&gt;

&lt;p&gt;If you grew up with Windows 2000 or XP, you probably remember &lt;a href="https://samtts.com" rel="noopener noreferrer"&gt;Microsoft SAM&lt;/a&gt; — the robotic, slightly eerie text-to-speech voice that could say anything you typed. It was the default voice of the Microsoft Speech API 4.0 (SAPI4), and for an entire generation, it was the first encounter with speech synthesis.&lt;/p&gt;

&lt;p&gt;Kids would type absurd sentences into the Narrator tool just to hear SAM butcher them in the most entertaining way possible. And then there was BonziBUDDY — that infamous purple gorilla desktop companion — which used the same underlying engine to "talk" to you.&lt;/p&gt;

&lt;p&gt;Fast forward to 2026, and these voices have become internet legend. Memes, YouTube compilations, and nostalgic threads keep them alive. But what if you could actually &lt;em&gt;run&lt;/em&gt; these voices again — not through a dusty Windows VM, but directly in your browser?&lt;/p&gt;

&lt;h2&gt;
  
  
  How SAPI4 Actually Worked
&lt;/h2&gt;

&lt;p&gt;Before we get into the modern implementation, let's understand what made SAPI4 tick.&lt;/p&gt;

&lt;p&gt;Microsoft's Speech API 4.0 (released around 1998) was a COM-based framework that sat between applications and speech engines. The architecture looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application → SAPI4 COM Interface → TTS Engine (e.g., SAM) → Audio Output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The TTS engine itself was a &lt;strong&gt;formant synthesizer&lt;/strong&gt; — it didn't use recorded speech samples like modern neural TTS. Instead, it generated sound by manipulating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Formant frequencies&lt;/strong&gt; — resonant frequencies that shape vowel sounds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pitch contours&lt;/strong&gt; — the rise and fall of the voice&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration rules&lt;/strong&gt; — how long each phoneme is held&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Noise generation&lt;/strong&gt; — for consonants like "s" and "f"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why SAM sounds robotic: it's literally &lt;em&gt;constructing&lt;/em&gt; speech from mathematical parameters, not stitching together recordings.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Text-to-Phoneme Pipeline
&lt;/h2&gt;

&lt;p&gt;When you type "Hello World" into a SAPI4 engine, here's what happens:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Text Normalization
&lt;/h3&gt;

&lt;p&gt;Numbers, abbreviations, and symbols get expanded:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Dr." → "Doctor"&lt;/li&gt;
&lt;li&gt;"123" → "one hundred twenty three"&lt;/li&gt;
&lt;li&gt;"$5" → "five dollars"&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Grapheme-to-Phoneme Conversion
&lt;/h3&gt;

&lt;p&gt;English text is converted to phoneme sequences using a combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dictionary lookup&lt;/strong&gt; — common words have stored pronunciations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Letter-to-sound rules&lt;/strong&gt; — fallback rules for unknown words&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example: &lt;code&gt;"Hello"&lt;/code&gt; → &lt;code&gt;/HH EH L OW/&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Prosody Generation
&lt;/h3&gt;

&lt;p&gt;The engine applies pitch and timing rules based on sentence structure. Questions get rising intonation. Periods trigger falling pitch. Commas insert pauses.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Waveform Synthesis
&lt;/h3&gt;

&lt;p&gt;Finally, the formant synthesizer generates raw PCM audio by:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified formant synthesis concept&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateFormant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frequency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bandwidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amplitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;samples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;sampleRate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;amplitude&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;frequency&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;sampleRate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;applyBandpassFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;frequency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bandwidth&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real implementation is far more complex, with multiple formants blended together, but this gives you the idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bringing It to the Browser
&lt;/h2&gt;

&lt;p&gt;The original SAPI4 engine was written in C/C++ and tightly coupled to Windows COM. Getting it to run in a browser required a few key steps:&lt;/p&gt;

&lt;h3&gt;
  
  
  Emscripten / WebAssembly Compilation
&lt;/h3&gt;

&lt;p&gt;The core synthesis engine can be compiled to WebAssembly, allowing it to run at near-native speed in any modern browser. The key challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No COM dependency&lt;/strong&gt; — the COM interface layer has to be replaced with direct function calls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio output&lt;/strong&gt; — Windows audio APIs are swapped for the Web Audio API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory management&lt;/strong&gt; — WASM has its own linear memory model&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Web Audio API Integration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;audioContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AudioContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;playTTSBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pcmData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sampleRate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pcmData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sampleRate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getChannelData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pcmData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createBufferSource&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Result
&lt;/h3&gt;

&lt;p&gt;The entire synthesis happens &lt;strong&gt;client-side&lt;/strong&gt;. No server calls, no API keys, no rate limits. You type text, it becomes speech — all in your browser, just like it's 2001 again.&lt;/p&gt;

&lt;p&gt;You can try this yourself at &lt;a href="https://samtts.com" rel="noopener noreferrer"&gt;SAM TTS&lt;/a&gt;, which brings together multiple classic SAPI4 voices in one free web tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft SAM&lt;/strong&gt; — the iconic default voice&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BonziBUDDY (Adult Male #2)&lt;/strong&gt; — the purple gorilla's voice&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TruVoice (Adult Male #1)&lt;/strong&gt; — smoother, more natural&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BetterSAM&lt;/strong&gt; — enhanced version with cleaner output&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Formant Synthesis Still Matters
&lt;/h2&gt;

&lt;p&gt;In a world of neural TTS models that sound nearly human (looking at you, ElevenLabs and OpenAI), why bother with 25-year-old formant synthesis?&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Zero Latency
&lt;/h3&gt;

&lt;p&gt;Formant synthesis is computationally trivial. No GPU needed, no model loading, no network round-trip. Input text → output audio in milliseconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Runs Anywhere
&lt;/h3&gt;

&lt;p&gt;Since it's pure computation with no model weights, the entire engine fits in a few hundred KB of WASM. It works on a Raspberry Pi, a budget phone, or an airplane with no WiFi.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Full Privacy
&lt;/h3&gt;

&lt;p&gt;Everything runs locally. Your text never leaves your device. For applications where privacy matters, this is a genuine advantage over cloud-based TTS.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Creative / Meme Use Cases
&lt;/h3&gt;

&lt;p&gt;Let's be honest — sometimes you &lt;em&gt;want&lt;/em&gt; the robotic voice. For memes, game mods, retro-themed projects, or just making your friends laugh, neural TTS is too "normal." The charm is in the jank.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Educational Value
&lt;/h3&gt;

&lt;p&gt;Understanding formant synthesis gives you insight into how speech actually works — the physics of vocal tracts, the linguistics of phonemes, and the engineering of real-time audio. It's a great rabbit hole.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;If you want to hear &lt;a href="https://samtts.com" rel="noopener noreferrer"&gt;Microsoft SAM TTS&lt;/a&gt; in action, the easiest way is to visit the web tool directly. Type any text, pick a voice, and hit play. No sign-up, no downloads.&lt;/p&gt;

&lt;p&gt;Some fun things to try:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Type &lt;code&gt;"ROFLcopter goes soi soi soi soi"&lt;/code&gt; (classic meme)&lt;/li&gt;
&lt;li&gt;Switch between SAM and BonziBUDDY to hear the difference&lt;/li&gt;
&lt;li&gt;Try extremely long words — the grapheme-to-phoneme engine handles them surprisingly well&lt;/li&gt;
&lt;li&gt;Type &lt;code&gt;"aeiou"&lt;/code&gt; repeatedly for the iconic sound&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Classic speech synthesis is a fascinating intersection of linguistics, signal processing, and software engineering. The fact that we can now run a 1998 Windows speech engine inside a modern browser — with no plugins, no installation — is a testament to how far web technology has come.&lt;/p&gt;

&lt;p&gt;If you're interested in retro computing, audio programming, or just want to hear that nostalgic robot voice again, give &lt;a href="https://samtts.com" rel="noopener noreferrer"&gt;SAM TTS&lt;/a&gt; a spin. It's free, it's instant, and it's a fun piece of computing history preserved for the modern web.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's your favorite retro software that you wish had a modern web version? Drop a comment below!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>nostalgia</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Why Kaomoji Still Matter in 2026: A Developer's Guide to Japanese Text Emoticons</title>
      <dc:creator>Haruki Tanaka</dc:creator>
      <pubDate>Mon, 23 Feb 2026 04:03:22 +0000</pubDate>
      <link>https://forem.com/kaomojiya/why-kaomoji-still-matter-in-2026-a-developers-guide-to-japanese-text-emoticons-3b2o</link>
      <guid>https://forem.com/kaomojiya/why-kaomoji-still-matter-in-2026-a-developers-guide-to-japanese-text-emoticons-3b2o</guid>
      <description>&lt;p&gt;We live in an age of emoji pickers and animated stickers. So why would anyone still use plain-text emoticons made from punctuation marks?&lt;/p&gt;

&lt;p&gt;That's what I thought too — until I started building &lt;a href="https://www.kaomojiya.org" rel="noopener noreferrer"&gt;Kaomojiya&lt;/a&gt;, a free kaomoji collection site, and realized just how alive Japanese text emoticons still are in 2026.&lt;/p&gt;

&lt;p&gt;Let me share what I've learned, why kaomoji deserve a spot in every developer's toolkit, and how you can use them to make your apps, docs, and messages more expressive.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Are Kaomoji, Exactly?
&lt;/h2&gt;

&lt;p&gt;If you've ever seen &lt;code&gt;(╥﹏╥)&lt;/code&gt; or &lt;code&gt;٩(◕‿◕｡)۶&lt;/code&gt; floating around in a chat, you've encountered &lt;strong&gt;kaomoji&lt;/strong&gt; — Japanese-style emoticons built entirely from Unicode characters.&lt;/p&gt;

&lt;p&gt;Unlike Western emoticons (&lt;code&gt;:-)&lt;/code&gt; read sideways), kaomoji are read face-on. And unlike emoji, they don't depend on any rendering engine, OS version, or font. They're &lt;strong&gt;pure text&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's a quick comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Needs Rendering?&lt;/th&gt;
&lt;th&gt;Cross-Platform?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Emoji&lt;/td&gt;
&lt;td&gt;😀&lt;/td&gt;
&lt;td&gt;Yes (OS-dependent)&lt;/td&gt;
&lt;td&gt;Varies by device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Western Emoticon&lt;/td&gt;
&lt;td&gt;:-)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kaomoji&lt;/td&gt;
&lt;td&gt;(◕‿◕)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes, everywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last column is what makes kaomoji interesting for developers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Developers Should Care
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. They Work Everywhere — Literally
&lt;/h3&gt;

&lt;p&gt;Kaomoji are plain Unicode text. They render identically in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terminal output and CLI tools&lt;/li&gt;
&lt;li&gt;Git commit messages&lt;/li&gt;
&lt;li&gt;Code comments&lt;/li&gt;
&lt;li&gt;Markdown files&lt;/li&gt;
&lt;li&gt;Slack, Discord, email — you name it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No image assets. No emoji font dependencies. No broken squares on older systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. They Add Personality to Developer Tools
&lt;/h3&gt;

&lt;p&gt;I've seen kaomoji used in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Error messages&lt;/strong&gt;: &lt;code&gt;(╯°□°)╯︵ ┻━┻ Something went wrong!&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loading states&lt;/strong&gt;: &lt;code&gt;(　-_-)旦~ Brewing your coffee...&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Success notifications&lt;/strong&gt;: &lt;code&gt;ヾ(＾∇＾) Deploy complete!&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;404 pages&lt;/strong&gt;: &lt;code&gt;(；⌣̀_⌣́) Page not found&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They make your tools feel human without adding any asset overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Great for Internationalization
&lt;/h3&gt;

&lt;p&gt;If you're building for a Japanese audience (or any audience that values cute/expressive text), kaomoji feel native. They're part of everyday Japanese digital communication — used in LINE, X (Twitter), blogs, and even business emails.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building Kaomojiya: What I Learned
&lt;/h2&gt;

&lt;p&gt;When I started &lt;a href="https://www.kaomojiya.org" rel="noopener noreferrer"&gt;Kaomojiya&lt;/a&gt;, the goal was simple: build the most comprehensive, searchable kaomoji collection on the web.&lt;/p&gt;

&lt;p&gt;Here's what the project looks like today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;500+ pages&lt;/strong&gt; covering every emotion and situation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thousands of kaomoji&lt;/strong&gt; organized by category (happy, sad, angry, love, animals, food, and dozens more)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-click copy&lt;/strong&gt; — tap any kaomoji and it's on your clipboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Completely free&lt;/strong&gt; — no login, no paywall, no ads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blazing fast&lt;/strong&gt; — built on Next.js 14 with Edge Runtime, deployed on Cloudflare&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Surprising Challenge: Categorization
&lt;/h3&gt;

&lt;p&gt;The hardest part wasn't building the site — it was organizing the kaomoji. Japanese internet culture has produced an incredible variety of expressions, and many kaomoji don't fit neatly into a single category.&lt;/p&gt;

&lt;p&gt;Is &lt;code&gt;( ˘ω˘ )&lt;/code&gt; sleepy or relaxed? Is &lt;code&gt;(ノ´ー&lt;/code&gt;)ノ` a shrug or dismissal?&lt;/p&gt;

&lt;p&gt;We ended up with a two-layer system: &lt;strong&gt;main categories&lt;/strong&gt; (like "happy" or "angry") and &lt;strong&gt;subcategories&lt;/strong&gt; (like "sparkle," "gentle," or "intense") that let users browse naturally.&lt;/p&gt;

&lt;h3&gt;
  
  
  The SEO Angle
&lt;/h3&gt;

&lt;p&gt;Here's something interesting for fellow devs: kaomoji pages get surprisingly good search traffic. People Google things like "shrug kaomoji" or "cat emoticon copy paste" all the time. By building individual, well-structured pages for each category, we've been able to capture that long-tail traffic organically.&lt;/p&gt;

&lt;p&gt;If you're interested in the technical details of how we handle 500+ pages with a single dynamic route, I covered that in my &lt;a href="https://dev.to/kaomojiya/how-i-built-500-seo-optimized-pages-with-nextjs-14-and-edge-runtime-2mpg"&gt;previous article on programmatic SEO with Next.js&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Use Kaomoji in Your Projects
&lt;/h2&gt;

&lt;p&gt;Here are some practical ways to sprinkle kaomoji into your work:&lt;/p&gt;

&lt;h3&gt;
  
  
  In Your README
&lt;/h3&gt;

&lt;p&gt;Add a kaomoji to your project title or status badge:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;My Awesome Project&lt;/strong&gt; ٩(◕‿◕｡)۶ — Status: Working (☞ﾟヮﾟ)☞&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  In Git Commits
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git commit -m "fix: resolve race condition in auth flow (╥﹏╥)"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git commit -m "feat: add dark mode support ヾ(＾∇＾)"&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  In Error Handling
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;404&lt;/strong&gt;: &lt;code&gt;'(；⌣̀_⌣́) We could not find what you were looking for.'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;500&lt;/strong&gt;: &lt;code&gt;'(╯°□°)╯︵ ┻━┻ Our server is having a bad day.'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default&lt;/strong&gt;: &lt;code&gt;'(・_・ヾ Something unexpected happened.'&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  In Notifications / Toasts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Success&lt;/strong&gt;: &lt;code&gt;toast.success('File uploaded! ٩(◕‿◕｡)۶')&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error&lt;/strong&gt;: &lt;code&gt;toast.error('Upload failed (╥﹏╥) Please try again.')&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Info&lt;/strong&gt;: &lt;code&gt;toast.info('(　-_-)旦~ Processing your request...')&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  My Favorite Kaomoji for Developers
&lt;/h2&gt;

&lt;p&gt;Here's a quick cheat sheet you can bookmark:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Kaomoji&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Success / Celebration&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ヾ(＾∇＾)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error / Frustration&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(╯°□°)╯︵ ┻━┻&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thinking / Loading&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(　-_-)旦~&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approval / Thumbs Up&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(☞ﾟヮﾟ)☞&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confusion&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(・_・ヾ&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sadness&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(╥﹏╥)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Love / Like&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(◕‿◕✿)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shrug&lt;/td&gt;
&lt;td&gt;&lt;code&gt;¯\_(ツ)_/¯&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cat&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(=^・^=)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bear&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ʕ•ᴥ•ʔ&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Want more? Browse the full collection at &lt;a href="https://www.kaomojiya.org" rel="noopener noreferrer"&gt;Kaomojiya&lt;/a&gt; — thousands of kaomoji, searchable by mood, category, or keyword. Just tap to copy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Kaomoji aren't going anywhere. In a world where emoji look different on every device and require specific Unicode support, these little text faces remain the most portable, lightweight, and universally compatible way to add emotion to text.&lt;/p&gt;

&lt;p&gt;Whether you're writing documentation, building a CLI tool, or just want to make your Slack messages more fun — give kaomoji a try.&lt;/p&gt;

&lt;p&gt;And if you need a solid collection to pull from, &lt;a href="https://www.kaomojiya.org" rel="noopener noreferrer"&gt;Kaomojiya&lt;/a&gt; has you covered. No signup, no cost, just copy and paste.&lt;/p&gt;

&lt;p&gt;Happy coding! &lt;code&gt;٩(◕‿◕｡)۶&lt;/code&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>beginners</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How I Built 500+ SEO-Optimized Pages with Next.js 14 and Edge Runtime</title>
      <dc:creator>Haruki Tanaka</dc:creator>
      <pubDate>Fri, 20 Feb 2026 03:50:59 +0000</pubDate>
      <link>https://forem.com/kaomojiya/how-i-built-500-seo-optimized-pages-with-nextjs-14-and-edge-runtime-2mpg</link>
      <guid>https://forem.com/kaomojiya/how-i-built-500-seo-optimized-pages-with-nextjs-14-and-edge-runtime-2mpg</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe4dm64xuk166egayjhg9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe4dm64xuk166egayjhg9.png" alt="How I Built 500+ SEO-Optimized Pages with Next.js 14 and Edge Runtime" width="800" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A practical guide to programmatic SEO with Next.js App Router, hybrid rendering, and Cloudflare Pages — based on real production experience and trade-offs.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;When I set out to build a kaomoji (Japanese emoticon) website, I knew I'd need hundreds of individual pages — one for each emotion category. Manually creating 500+ pages wasn't an option. Instead, I built a system that programmatically generates SEO-optimized pages from structured data, deploys them on Edge Runtime, and handles everything from metadata to Schema.org markup automatically.&lt;/p&gt;

&lt;p&gt;Here's exactly how I did it — including the trade-offs and gotchas I ran into along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;Japanese kaomoji like &lt;code&gt;(╥﹏╥)&lt;/code&gt; and &lt;code&gt;(づ｡◕‿‿◕｡)づ&lt;/code&gt; are searched thousands of times per month. Each emotion category (cry, cute, angry, etc.) needs its own page with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unique title, description, and keywords&lt;/li&gt;
&lt;li&gt;Structured kaomoji data grouped by subcategories&lt;/li&gt;
&lt;li&gt;Schema.org markup (FAQ, ItemList, BreadcrumbList)&lt;/li&gt;
&lt;li&gt;Consistent layout with sidebar navigation&lt;/li&gt;
&lt;li&gt;Fast load times globally (especially Japan)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Doing this for 500+ categories by hand would be unmaintainable. Programmatic SEO was the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data/kaomoji/
├── cry.ts          # KaomojiItem[] data
├── cute.ts
├── smile.ts
└── ... (524 files)

public/data/
├── kaomoji/*.json  # Runtime data (fetched, not bundled)
└── config/*.json   # Page configs (title, description, FAQ, etc.)

app/[locale]/(default)/[slug]/page.tsx   # Single dynamic route
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: &lt;strong&gt;one dynamic route serves all 524 pages&lt;/strong&gt;. The &lt;code&gt;[slug]&lt;/code&gt; parameter (e.g., &lt;code&gt;cute-kaomoji&lt;/code&gt;) determines which data and config to load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Define Your Data Schema
&lt;/h2&gt;

&lt;p&gt;Every kaomoji item follows a strict TypeScript interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;KaomojiItem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;kaomoji&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// "(╥﹏╥)"&lt;/span&gt;
  &lt;span class="nl"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// "cry"&lt;/span&gt;
  &lt;span class="nl"&gt;subcategory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// "basic" | "sparkle" | "dramatic"&lt;/span&gt;
  &lt;span class="nl"&gt;popularity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// 0-100&lt;/span&gt;
  &lt;span class="nl"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;        &lt;span class="c1"&gt;// ["悲しい", "泣く", "涙"]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each category file exports an array of these items. For example, &lt;code&gt;data/kaomoji/cry.ts&lt;/code&gt; contains 50-100 kaomoji grouped by subcategory.&lt;/p&gt;

&lt;p&gt;The data files are compiled to JSON at build time and served from &lt;code&gt;/public/data/kaomoji/&lt;/code&gt;. This is critical — more on why below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Hybrid Rendering Strategy
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting. With 524 pages, pre-rendering everything at build time would be slow and wasteful. Instead, I use a &lt;strong&gt;hybrid approach&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/[locale]/(default)/[slug]/page.tsx&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;edge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;revalidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// ISR: revalidate every hour&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dynamicParams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateStaticParams&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;topPages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cute&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;smile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;happy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sad&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;angry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;surprised&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;love&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sleepy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;please&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;thank&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sorry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rabbit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bear&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;animal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;star&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wink&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kiss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;punch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tired&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;confused&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mendokusai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nemui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gorogoro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uttori&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;topPages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-kaomoji`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ja&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;30 top pages&lt;/strong&gt; are pre-rendered at build time (instant load)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;494 remaining pages&lt;/strong&gt; are generated on-demand at first request&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dynamicParams = true&lt;/code&gt; allows paths not in &lt;code&gt;generateStaticParams&lt;/code&gt; to be handled at runtime&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Important nuance:&lt;/strong&gt; &lt;code&gt;dynamicParams = true&lt;/code&gt; alone doesn't guarantee caching. For the on-demand pages to be cached and reused (ISR behavior), the route must be ISR-compatible — meaning you need &lt;code&gt;export const revalidate&lt;/code&gt; set, and you must avoid Dynamic APIs like &lt;code&gt;cookies()&lt;/code&gt; or &lt;code&gt;headers()&lt;/code&gt; in the render path. Without this, each request triggers a fresh server render.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Dynamic Data Loading (Not Static Imports)
&lt;/h2&gt;

&lt;p&gt;This was the biggest performance win. Initially, I imported kaomoji data directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BAD: This bundles ALL data into the page&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cryKaomoji&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/data/kaomoji/cry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With 524 categories, this approach bloated the JavaScript bundle to &lt;strong&gt;25MB+&lt;/strong&gt;. The solution: load data via &lt;code&gt;fetch&lt;/code&gt; at runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// GOOD: Fetch only what's needed&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getKaomojiData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/data/kaomoji/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.json`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A note on what &lt;code&gt;revalidate&lt;/code&gt; controls here:&lt;/strong&gt; The &lt;code&gt;{ next: { revalidate } }&lt;/code&gt; option in &lt;code&gt;fetch&lt;/code&gt; controls &lt;strong&gt;Next.js Data Cache&lt;/strong&gt; — the framework's own server-side persistent cache. It does NOT automatically set CDN cache headers or Cloudflare cache policies. Those are separate concerns (more on this in the caching section).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Results:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bundle size dropped from 25MB to under 5MB&lt;/li&gt;
&lt;li&gt;Each page only loads its own category data&lt;/li&gt;
&lt;li&gt;Edge Runtime compatible (no Node.js &lt;code&gt;fs&lt;/code&gt; needed)&lt;/li&gt;
&lt;li&gt;Next.js Data Cache handles revalidation at the framework level&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Automated SEO Metadata
&lt;/h2&gt;

&lt;p&gt;Each page needs unique metadata. I store page configs as JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"泣く顔文字 500選【コピペ可】"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"泣く・悲しい顔文字を500個以上収録..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"keywords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"泣く 顔文字"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"悲しい 顔文字"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"涙 顔文字"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hero"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"泣く顔文字"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"subtitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"悲しい時に使える顔文字コレクション"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"faqItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"泣く顔文字の使い方は？"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"answer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;generateMetadata&lt;/code&gt; function loads this config and returns proper Next.js metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Metadata&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Metadata&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/-kaomoji$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPageConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;config.metadata.title,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;config.metadata.description,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="na"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;openGraph&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;config.metadata.title,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;config.metadata.description,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;website&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;alternates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://www.kaomojiya.org/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Schema.org Structured Data
&lt;/h2&gt;

&lt;p&gt;Each page includes structured data merged into a single &lt;code&gt;@graph&lt;/code&gt;. Choose your schema types based on what your page actually represents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schemas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mergeSchemas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;generateBreadcrumbSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;generateFAQSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;faqItems&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;generateItemListSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kaomojiData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells search engines what each page contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BreadcrumbList&lt;/strong&gt; — navigation hierarchy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FAQPage&lt;/strong&gt; — common questions (rich snippet eligible). Make sure the Q&amp;amp;A content is also visible on the page itself — Google requires this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ItemList&lt;/strong&gt; — the kaomoji collection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Pick schema types that match your page's actual content. For a collection/reference page, &lt;code&gt;CollectionPage&lt;/code&gt; + &lt;code&gt;ItemList&lt;/code&gt; is often more appropriate than &lt;code&gt;WebApplication&lt;/code&gt;. Using the wrong type won't necessarily hurt, but it won't help either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Caching — Two Separate Concerns
&lt;/h2&gt;

&lt;p&gt;This is where things get nuanced. There are &lt;strong&gt;two distinct caching layers&lt;/strong&gt;, and they're often conflated:&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: Next.js Data Cache (framework level)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This controls Next.js server-side Data Cache&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// Revalidate after 1 hour&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Next.js to cache the fetch result server-side and serve stale data while revalidating in the background. It has nothing to do with CDN behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: CDN / Edge Cache (infrastructure level)
&lt;/h3&gt;

&lt;p&gt;On Cloudflare, CDN caching is controlled by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Cache-Control&lt;/code&gt; response headers&lt;/li&gt;
&lt;li&gt;Cloudflare Page Rules / Cache Rules&lt;/li&gt;
&lt;li&gt;Cloudflare Cache API (programmatic)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are &lt;strong&gt;not&lt;/strong&gt; automatically set by Next.js &lt;code&gt;revalidate&lt;/code&gt;. You need to configure them separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  My caching config
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CACHE_CONFIG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;pageConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// Next.js Data Cache: 1 hour&lt;/span&gt;
  &lt;span class="na"&gt;kaomojiData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// Next.js Data Cache: 1 hour&lt;/span&gt;
  &lt;span class="na"&gt;sitemap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// Next.js Data Cache: 24 hours&lt;/span&gt;
  &lt;span class="na"&gt;inMemory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// App-level memory cache: 1 hour (ms)&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A warning about in-memory cache on Edge:&lt;/strong&gt; Edge/Workers memory is per-isolate and can be evicted at any time. It's not shared across edge locations. Treat it as a best-effort hot cache, not a reliable persistence layer — it's closer to a request-local optimization than Redis.&lt;/p&gt;

&lt;p&gt;In development, all caches are disabled (&lt;code&gt;isDev ? 0 : ...&lt;/code&gt;) for instant feedback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Deploy to Cloudflare Pages
&lt;/h2&gt;

&lt;p&gt;My app runs on Cloudflare Pages with Edge Runtime using &lt;code&gt;@cloudflare/next-on-pages&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# wrangler.toml&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"kaomojiya"&lt;/span&gt;
&lt;span class="py"&gt;compatibility_date&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2024-07-29"&lt;/span&gt;
&lt;span class="py"&gt;compatibility_flags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"nodejs_compat"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;pages_build_output_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;".vercel/output/static"&lt;/span&gt;

&lt;span class="nn"&gt;[vars]&lt;/span&gt;
&lt;span class="py"&gt;NEXT_PUBLIC_WEB_URL&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://www.kaomojiya.org"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;next build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx @cloudflare/next-on-pages
wrangler pages deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important note (2026):&lt;/strong&gt; Cloudflare's official recommendation has shifted toward deploying Next.js apps to &lt;strong&gt;Cloudflare Workers&lt;/strong&gt; using the &lt;a href="https://opennext.js.org/cloudflare" rel="noopener noreferrer"&gt;OpenNext adapter&lt;/a&gt;. The &lt;code&gt;@cloudflare/next-on-pages&lt;/code&gt; package was archived in late 2025 and is in maintenance-only mode. My setup still works on Pages, but if you're starting a new project, evaluate the Workers/OpenNext path — it has better long-term support and broader feature coverage.&lt;/p&gt;

&lt;p&gt;I chose Pages at the time because it was the stable option, and migrating a working production app has costs. The architecture patterns in this article (hybrid rendering, runtime data fetching, structured metadata) apply regardless of which Cloudflare deployment target you choose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;After deploying this system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;524 pages&lt;/strong&gt; indexed by Google within weeks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundle size&lt;/strong&gt;: 25MB → under 5MB (80% reduction)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build time&lt;/strong&gt;: ~2 minutes for 30 pre-rendered pages (vs. 20+ minutes for all 524)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTFB&lt;/strong&gt;: under 200ms globally via Edge Runtime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero manual page creation&lt;/strong&gt; — add a data file, and the page exists&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Don't pre-render everything&lt;/strong&gt; — Use hybrid rendering. Pre-render your top pages, let the rest generate on demand with ISR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fetch data at runtime&lt;/strong&gt; — Static imports kill your bundle size at scale. Use JSON files + &lt;code&gt;fetch&lt;/code&gt; with caching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate your cache layers&lt;/strong&gt; — Next.js Data Cache and CDN cache are different things. Configure both intentionally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be precise about ISR&lt;/strong&gt; — &lt;code&gt;dynamicParams = true&lt;/code&gt; allows on-demand rendering, but you need &lt;code&gt;revalidate&lt;/code&gt; and must avoid Dynamic APIs for pages to actually be cached.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge Runtime has trade-offs&lt;/strong&gt; — Great for latency, but in-memory state is per-isolate and ephemeral. Design accordingly.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/app" rel="noopener noreferrer"&gt;Next.js App Router Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://opennext.js.org/cloudflare" rel="noopener noreferrer"&gt;OpenNext Cloudflare Adapter&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/search/docs/appearance/structured-data" rel="noopener noreferrer"&gt;Schema.org Structured Data Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes" rel="noopener noreferrer"&gt;Next.js Dynamic Routes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kaomojiya.org" rel="noopener noreferrer"&gt;Kaomojiya&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built with Next.js 14, TypeScript, Tailwind CSS, and deployed on Cloudflare Pages. This article reflects my production experience and trade-offs as of early 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>seo</category>
      <category>cloudflare</category>
    </item>
  </channel>
</rss>
