<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Bala Evani Blog</title>
        <link>https://bevani.dev/blog</link>
        <description>Bala Evani Blog</description>
        <lastBuildDate>Sun, 12 Apr 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <item>
            <title><![CDATA[30 minutes to add a custom 404 page. a RCA.]]></title>
            <link>https://bevani.dev/blog/docusaurus-custom-404</link>
            <guid>https://bevani.dev/blog/docusaurus-custom-404</guid>
            <pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[I wanted a custom 404 page. a little peepoLost gif and a "take me home" button.]]></description>
            <content:encoded><![CDATA[<p>I wanted a custom 404 page. a little peepoLost gif and a "take me home" button.
Thirty minutes later I had broken the dev server twice and learned more about Docusaurus internals than I planned to.</p>
<p>Here's what happened.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-i-tried-first-wrong">what i tried first (wrong)<a href="https://bevani.dev/blog/docusaurus-custom-404#what-i-tried-first-wrong" class="hash-link" aria-label="Direct link to what i tried first (wrong)" title="Direct link to what i tried first (wrong)" translate="no">​</a></h2>
<p>The obvious thing: create <code>src/pages/404.js</code>. It's a pages directory, 404 is a page, right?</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#9CDCFE;--prism-background-color:#1E1E1E"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#9CDCFE;background-color:#1E1E1E"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#9CDCFE"><span class="token comment" style="color:rgb(106, 153, 85)">// src/pages/404.js</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain"></span><span class="token keyword module" style="color:rgb(86, 156, 214)">export</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(86, 156, 214)">default</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(86, 156, 214)">function</span><span class="token plain"> </span><span class="token function maybe-class-name" style="color:rgb(220, 220, 170)">NotFound</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">  </span><span class="token keyword control-flow" style="color:rgb(86, 156, 214)">return</span><span class="token plain"> </span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&lt;</span><span class="token tag" style="color:rgb(78, 201, 176)">div</span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&gt;</span><span class="token plain-text">my custom 404</span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&lt;/</span><span class="token tag" style="color:rgb(78, 201, 176)">div</span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&gt;</span><span class="token punctuation" style="color:rgb(212, 212, 212)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><br></div></code></pre></div></div>
<p>In dev: it flashed briefly, then the default "Page Not Found" page took over.
In prod (Cloudflare Pages): same thing. Default page. My file completely ignored.</p>
<p><strong>Why it doesn't work:</strong></p>
<p><code>src/pages/404.js</code> creates a <em>route</em> at <code>/404</code>. It doesn't replace Docusaurus's internal
404 handler. When you navigate to an unknown URL, Docusaurus's React Router says
"no route found" and renders <code>@theme/NotFound</code> — the built-in component. Your
<code>pages/404.js</code> is a different route entirely, never triggered for actual 404s.</p>
<p>The host (Cloudflare) might also serve <code>404.html</code> from the build output, but
client-side navigation within the app always goes through Docusaurus's router,
not the file system.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-actually-works--swizzling">what actually works — swizzling<a href="https://bevani.dev/blog/docusaurus-custom-404#what-actually-works--swizzling" class="hash-link" aria-label="Direct link to what actually works — swizzling" title="Direct link to what actually works — swizzling" translate="no">​</a></h2>
<p>Docusaurus has a concept called <a href="https://docusaurus.io/docs/swizzling" target="_blank" rel="noopener noreferrer" class="">swizzling</a>:
you can override theme components by creating them in <code>src/theme/</code>.</p>
<p>The component that handles 404s is <code>@theme/NotFound</code>. So:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#9CDCFE;--prism-background-color:#1E1E1E"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#9CDCFE;background-color:#1E1E1E"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#9CDCFE"><span class="token function" style="color:rgb(220, 220, 170)">npm</span><span class="token plain"> run swizzle @docusaurus/theme-classic NotFound</span><br></div></code></pre></div></div>
<p>In Docusaurus v2 this created a single file: <code>src/theme/NotFound.js</code>. Simple.</p>
<p>In Docusaurus v3 it creates <strong>two</strong> files:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#9CDCFE;--prism-background-color:#1E1E1E"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#9CDCFE;background-color:#1E1E1E"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#9CDCFE"><span class="token plain">src/theme/NotFound/index.js         # page wrapper (provides Layout)</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">src/theme/NotFound/Content/index.js # the actual content ("Page Not Found" text)</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-i-actually-did-also-wrong">what i actually did (also wrong)<a href="https://bevani.dev/blog/docusaurus-custom-404#what-i-actually-did-also-wrong" class="hash-link" aria-label="Direct link to what i actually did (also wrong)" title="Direct link to what i actually did (also wrong)" translate="no">​</a></h2>
<p>I read that v3 split the component, so I deleted <code>NotFound/index.js</code> and only
created <code>NotFound/Content/index.js</code> thinking I just needed the content part.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#9CDCFE;--prism-background-color:#1E1E1E"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#9CDCFE;background-color:#1E1E1E"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#9CDCFE"><span class="token plain">✖ Module not found: Can't resolve '@theme/NotFound'</span><br></div></code></pre></div></div>
<p>Right. By deleting <code>index.js</code>, I deleted the component entirely. Docusaurus needs
<code>NotFound/index.js</code> to exist to resolve <code>@theme/NotFound</code>. The <code>Content</code> subcomponent
can't exist without the parent.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-fix-that-works">the fix that works<a href="https://bevani.dev/blog/docusaurus-custom-404#the-fix-that-works" class="hash-link" aria-label="Direct link to the fix that works" title="Direct link to the fix that works" translate="no">​</a></h2>
<p>You need both files. <code>index.js</code> is a passthrough wrapper:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#9CDCFE;--prism-background-color:#1E1E1E"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#9CDCFE;background-color:#1E1E1E"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#9CDCFE"><span class="token comment" style="color:rgb(106, 153, 85)">// src/theme/NotFound/index.js</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain"></span><span class="token keyword module" style="color:rgb(86, 156, 214)">import</span><span class="token plain"> </span><span class="token imports maybe-class-name">React</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(86, 156, 214)">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"react"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain"></span><span class="token keyword module" style="color:rgb(86, 156, 214)">import</span><span class="token plain"> </span><span class="token imports maybe-class-name">NotFound</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(86, 156, 214)">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"@theme-original/NotFound"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain"></span><span class="token keyword module" style="color:rgb(86, 156, 214)">export</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(86, 156, 214)">default</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(86, 156, 214)">function</span><span class="token plain"> </span><span class="token function maybe-class-name" style="color:rgb(220, 220, 170)">NotFoundWrapper</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token parameter">props</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">  </span><span class="token keyword control-flow" style="color:rgb(86, 156, 214)">return</span><span class="token plain"> </span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&lt;</span><span class="token tag class-name" style="color:rgb(78, 201, 176)">NotFound</span><span class="token tag" style="color:rgb(78, 201, 176)"> </span><span class="token tag spread punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token tag spread operator" style="color:rgb(212, 212, 212)">...</span><span class="token tag spread" style="color:rgb(78, 201, 176)">props</span><span class="token tag spread punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token tag" style="color:rgb(78, 201, 176)"> </span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">/&gt;</span><span class="token punctuation" style="color:rgb(212, 212, 212)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><br></div></code></pre></div></div>
<p>And <code>Content/index.js</code> is where you put your actual custom content:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#9CDCFE;--prism-background-color:#1E1E1E"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#9CDCFE;background-color:#1E1E1E"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#9CDCFE"><span class="token comment" style="color:rgb(106, 153, 85)">// src/theme/NotFound/Content/index.js</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain"></span><span class="token keyword module" style="color:rgb(86, 156, 214)">import</span><span class="token plain"> </span><span class="token imports maybe-class-name">React</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(86, 156, 214)">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"react"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain"></span><span class="token keyword module" style="color:rgb(86, 156, 214)">import</span><span class="token plain"> </span><span class="token imports maybe-class-name">Link</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(86, 156, 214)">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"@docusaurus/Link"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain"></span><span class="token keyword module" style="color:rgb(86, 156, 214)">export</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(86, 156, 214)">default</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(86, 156, 214)">function</span><span class="token plain"> </span><span class="token function maybe-class-name" style="color:rgb(220, 220, 170)">NotFoundContent</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">  </span><span class="token keyword control-flow" style="color:rgb(86, 156, 214)">return</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">    </span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&lt;</span><span class="token tag" style="color:rgb(78, 201, 176)">main</span><span class="token tag" style="color:rgb(78, 201, 176)"> </span><span class="token tag attr-name" style="color:rgb(156, 220, 254)">style</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:rgb(212, 212, 212)">=</span><span class="token tag script language-javascript punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token tag script language-javascript punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token tag script language-javascript" style="color:rgb(78, 201, 176)"> </span><span class="token tag script language-javascript literal-property property" style="color:rgb(78, 201, 176)">textAlign</span><span class="token tag script language-javascript operator" style="color:rgb(212, 212, 212)">:</span><span class="token tag script language-javascript" style="color:rgb(78, 201, 176)"> </span><span class="token tag script language-javascript string" style="color:rgb(206, 145, 120)">"center"</span><span class="token tag script language-javascript punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token tag script language-javascript" style="color:rgb(78, 201, 176)"> </span><span class="token tag script language-javascript literal-property property" style="color:rgb(78, 201, 176)">padding</span><span class="token tag script language-javascript operator" style="color:rgb(212, 212, 212)">:</span><span class="token tag script language-javascript" style="color:rgb(78, 201, 176)"> </span><span class="token tag script language-javascript string" style="color:rgb(206, 145, 120)">"4rem 1rem"</span><span class="token tag script language-javascript" style="color:rgb(78, 201, 176)"> </span><span class="token tag script language-javascript punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token tag script language-javascript punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&gt;</span><span class="token plain-text"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&lt;</span><span class="token tag" style="color:rgb(78, 201, 176)">img</span><span class="token tag" style="color:rgb(78, 201, 176)"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token tag" style="color:rgb(78, 201, 176)">        </span><span class="token tag attr-name" style="color:rgb(156, 220, 254)">src</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(212, 212, 212)">=</span><span class="token tag attr-value punctuation" style="color:rgb(212, 212, 212)">"</span><span class="token tag attr-value" style="color:rgb(206, 145, 120)">https://cdn3.emoji.gg/emojis/29604-peepolost.png</span><span class="token tag attr-value punctuation" style="color:rgb(212, 212, 212)">"</span><span class="token tag" style="color:rgb(78, 201, 176)"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token tag" style="color:rgb(78, 201, 176)">        </span><span class="token tag attr-name" style="color:rgb(156, 220, 254)">width</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(212, 212, 212)">=</span><span class="token tag attr-value punctuation" style="color:rgb(212, 212, 212)">"</span><span class="token tag attr-value" style="color:rgb(206, 145, 120)">80px</span><span class="token tag attr-value punctuation" style="color:rgb(212, 212, 212)">"</span><span class="token tag" style="color:rgb(78, 201, 176)"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token tag" style="color:rgb(78, 201, 176)">        </span><span class="token tag attr-name" style="color:rgb(156, 220, 254)">alt</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(212, 212, 212)">=</span><span class="token tag attr-value punctuation" style="color:rgb(212, 212, 212)">"</span><span class="token tag attr-value" style="color:rgb(206, 145, 120)">peepoLost</span><span class="token tag attr-value punctuation" style="color:rgb(212, 212, 212)">"</span><span class="token tag" style="color:rgb(78, 201, 176)"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token tag" style="color:rgb(78, 201, 176)">      </span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">/&gt;</span><span class="token plain-text"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&lt;</span><span class="token tag" style="color:rgb(78, 201, 176)">h1</span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&gt;</span><span class="token plain-text">404 — genuinely no idea where you are</span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&lt;/</span><span class="token tag" style="color:rgb(78, 201, 176)">h1</span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&gt;</span><span class="token plain-text"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&lt;</span><span class="token tag" style="color:rgb(78, 201, 176)">p</span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&gt;</span><span class="token plain-text">this page doesn't exist. you might be more lost than me.</span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&lt;/</span><span class="token tag" style="color:rgb(78, 201, 176)">p</span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&gt;</span><span class="token plain-text"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&lt;</span><span class="token tag class-name" style="color:rgb(78, 201, 176)">Link</span><span class="token tag" style="color:rgb(78, 201, 176)"> </span><span class="token tag attr-name" style="color:rgb(156, 220, 254)">to</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(212, 212, 212)">=</span><span class="token tag attr-value punctuation" style="color:rgb(212, 212, 212)">"</span><span class="token tag attr-value" style="color:rgb(206, 145, 120)">/</span><span class="token tag attr-value punctuation" style="color:rgb(212, 212, 212)">"</span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&gt;</span><span class="token plain-text">-&amp;gt; take me home</span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&lt;/</span><span class="token tag class-name" style="color:rgb(78, 201, 176)">Link</span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&gt;</span><span class="token plain-text"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain-text">    </span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&lt;/</span><span class="token tag" style="color:rgb(78, 201, 176)">main</span><span class="token tag punctuation" style="color:rgb(212, 212, 212)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="one-more-thing-swizzle-doesnt-hot-reload">one more thing: swizzle doesn't hot reload<a href="https://bevani.dev/blog/docusaurus-custom-404#one-more-thing-swizzle-doesnt-hot-reload" class="hash-link" aria-label="Direct link to one more thing: swizzle doesn't hot reload" title="Direct link to one more thing: swizzle doesn't hot reload" translate="no">​</a></h2>
<p>After getting the file structure right, changes still weren't showing in dev.
This is a <a href="https://github.com/facebook/docusaurus/issues/10238" target="_blank" rel="noopener noreferrer" class="">known Docusaurus issue (#10238)</a> —
swizzled components don't support hot reload. You have to fully restart the dev server
every time you change them. <code>Ctrl+C</code>, <code>npm start</code>, wait.</p>
<p>And if the Docusaurus cache (<code>.docusaurus/</code>) is stale, even a restart isn't enough.
Full clean required:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#9CDCFE;--prism-background-color:#1E1E1E"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#9CDCFE;background-color:#1E1E1E"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#9CDCFE"><span class="token plain">npx docusaurus </span><span class="token function" style="color:rgb(220, 220, 170)">clear</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">&amp;&amp;</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">npm</span><span class="token plain"> start</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="rca-summary">RCA summary<a href="https://bevani.dev/blog/docusaurus-custom-404#rca-summary" class="hash-link" aria-label="Direct link to RCA summary" title="Direct link to RCA summary" translate="no">​</a></h2>
<table><thead><tr><th>What I did</th><th>What went wrong</th></tr></thead><tbody><tr><td>Created <code>src/pages/404.js</code></td><td>Creates a route, doesn't override the 404 handler</td></tr><tr><td>Deleted <code>NotFound/index.js</code>, only kept <code>Content/</code></td><td>Broke <code>@theme/NotFound</code> resolution entirely</td></tr><tr><td>Restarted dev server</td><td>Swizzle cache didn't clear, old component still loaded</td></tr><tr><td><code>npx docusaurus clear</code> + restart</td><td>Finally worked</td></tr></tbody></table>
<p>The correct structure is both files. The parent <code>index.js</code> as a wrapper, the
<code>Content/index.js</code> with your actual UI. The <a href="https://github.com/facebook/docusaurus/discussions/6030" target="_blank" rel="noopener noreferrer" class="">GitHub discussion (#6030)</a>
has the right answer — just note that it was written for v2, and the file
structure changed in v3.</p>
<p>Anyway. peepoLost is on the 404 page now. thirty minutes well spent.</p>
<p>-- Bala</p>]]></content:encoded>
            <category>docusaurus</category>
            <category>debugging</category>
            <category>rca</category>
        </item>
        <item>
            <title><![CDATA[I over-engineered a personal website and I regret nothing]]></title>
            <link>https://bevani.dev/blog/how-i-built-this</link>
            <guid>https://bevani.dev/blog/how-i-built-this</guid>
            <pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Most people build a personal website in a weekend using Squarespace or Wix.]]></description>
            <content:encoded><![CDATA[<p>Most people build a personal website in a weekend using Squarespace or Wix.</p>
<p>I used Terraform.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-requirements">The requirements<a href="https://bevani.dev/blog/how-i-built-this#the-requirements" class="hash-link" aria-label="Direct link to The requirements" title="Direct link to The requirements" translate="no">​</a></h2>
<p>Like any good engineer, I started by writing requirements for a website that would hold
maybe three blog posts and a resume PDF:</p>
<ul>
<li class="">Must be fast</li>
<li class="">Must cost $0/month</li>
<li class="">Must be "production grade" (this one is my fault)</li>
<li class="">Must have infrastructure-as-code because apparently I have something to prove</li>
</ul>
<p>Normal people: <code>git push</code> → GitHub Pages → done.</p>
<p>Me: <em>opens a new Terraform file</em></p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-architecture">The architecture<a href="https://bevani.dev/blog/how-i-built-this#the-architecture" class="hash-link" aria-label="Direct link to The architecture" title="Direct link to The architecture" translate="no">​</a></h2>
<p>Here's the full system design. Yes, I drew this out. Yes, it's for a personal blog.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#9CDCFE;--prism-background-color:#1E1E1E"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#9CDCFE;background-color:#1E1E1E"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#9CDCFE"><span class="token plain">[ Browser ]</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">    |</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">    v</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">[ Cloudflare CDN ] &lt;-- ~300 edge locations globally</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">    |                   because my 3 readers deserve low latency</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">    v</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">[ Cloudflare Pages ] &lt;-- static site, auto-deploys on git push</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">    |</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">    v</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">[ Docusaurus build ] &lt;-- React + Markdown = blog + docs site</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">    |</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">[ Cloudflare R2 ] &lt;-- resume PDF, because S3 costs money</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">    |</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">[ Terraform ] &lt;-- manages all of the above</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">    |</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">[ GitHub Actions ] &lt;-- runs npm audit weekly (security theater, but mine)</span><br></div></code></pre></div></div>
<p>Total monthly cost: <strong>$0.</strong></p>
<p>The domain (<code>bevani.dev</code>) costs ~$10/year. That's it. Everything else is free tier.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-cloudflare-pages-over-github-pages">Why Cloudflare Pages over GitHub Pages?<a href="https://bevani.dev/blog/how-i-built-this#why-cloudflare-pages-over-github-pages" class="hash-link" aria-label="Direct link to Why Cloudflare Pages over GitHub Pages?" title="Direct link to Why Cloudflare Pages over GitHub Pages?" translate="no">​</a></h2>
<p>Honestly? I wanted a CDN and GitHub Pages uses Fastly which has fewer edge locations.
The real reason is that I wanted to learn Cloudflare's stack and this was a good excuse.</p>
<p>But the performance argument is real — if someone in Singapore opens my resume link,
Cloudflare serves it from an edge node nearby instead of a US datacenter.
Whether anyone in Singapore will ever read my blog is a separate question.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-0-stack-breakdown">The $0 stack breakdown<a href="https://bevani.dev/blog/how-i-built-this#the-0-stack-breakdown" class="hash-link" aria-label="Direct link to The $0 stack breakdown" title="Direct link to The $0 stack breakdown" translate="no">​</a></h2>
<table><thead><tr><th>Layer</th><th>Tool</th><th>Why</th><th>Monthly cost</th></tr></thead><tbody><tr><td>Framework</td><td>Docusaurus 3</td><td>Markdown-first, looks good out of the box</td><td>$0</td></tr><tr><td>Hosting</td><td>Cloudflare Pages</td><td>Free tier, global CDN included</td><td>$0</td></tr><tr><td>CDN</td><td>Cloudflare</td><td>Comes with Pages, <code>proxied = true</code></td><td>$0</td></tr><tr><td>Resume storage</td><td>Cloudflare R2</td><td>10GB free, S3-compatible</td><td>$0</td></tr><tr><td>Infra mgmt</td><td>Terraform</td><td>Because clicking in dashboards is temporary</td><td>$0</td></tr><tr><td>CI/CD</td><td>GitHub Actions</td><td><code>npm audit</code> on a cron schedule</td><td>$0</td></tr><tr><td>TF state</td><td>Cloudflare R2</td><td>Also free. Also R2. Yes I'm that guy</td><td>$0</td></tr><tr><td><strong>Domain</strong></td><td><strong>bevani.dev</strong></td><td><strong>The only real cost</strong></td><td><strong>~$0.83/mo</strong></td></tr></tbody></table>
<p>Grand total: <strong>83 cents a month.</strong></p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="things-that-went-wrong-a-partial-list">Things that went wrong (a partial list)<a href="https://bevani.dev/blog/how-i-built-this#things-that-went-wrong-a-partial-list" class="hash-link" aria-label="Direct link to Things that went wrong (a partial list)" title="Direct link to Things that went wrong (a partial list)" translate="no">​</a></h2>
<p><strong>1. Webpack and webpackbar had a domestic dispute</strong></p>
<p>Docusaurus ships with <code>webpackbar</code> for build progress. <code>webpackbar</code> needs <code>webpack</code>.
But <code>webpack</code> 5.97+ changed its options schema and <code>webpackbar</code> passed invalid options.
The fix: pin <code>webpack</code> to <code>5.96.1</code> and override all nested instances. Problem solved.
Time spent: longer than I'd like to admit.</p>
<p><strong>2. Terraform's R2 backend needs 6 "skip" flags</strong></p>
<p>Cloudflare R2 is S3-compatible but not <em>exactly</em> S3. When you use it as a Terraform
backend, you have to tell Terraform to skip every AWS-specific validation:</p>
<div class="language-hcl codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#9CDCFE;--prism-background-color:#1E1E1E"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-hcl codeBlock_bY9V thin-scrollbar" style="color:#9CDCFE;background-color:#1E1E1E"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#9CDCFE"><span class="token plain">skip_credentials_validation = true</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">skip_metadata_api_check     = true</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">skip_region_validation      = true</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">skip_requesting_account_id  = true</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">use_path_style              = true</span><br></div></code></pre></div></div>
<p>Five skip flags and one rename. That's how you know you're in compatibility layer territory.</p>
<p><strong>3. My <code>.npmrc</code> was poisoning the lockfile</strong></p>
<p>My global <code>~/.npmrc</code> had <code>legacy-peer-deps=true</code> set from some project years ago.
npm on my Mac: <em>uses relaxed peer dep resolution, generates lockfile</em>
npm on Cloudflare: <em>strict mode, tries to install from lockfile, explodes</em></p>
<p>The fix was a one-line <code>.npmrc</code> in the project. The real fix was asking why I had
that global setting in the first place. I did not investigate further.</p>
<p><strong>4. Cloudflare Pages GitHub App wasn't installed</strong></p>
<p>Terraform kept getting a 401 with error code <code>8000011</code> — "internal issue with your
Cloudflare Pages Git installation." The fix was going to the Cloudflare dashboard,
clicking through the project creation wizard <em>just enough</em> to install the GitHub App,
then backing out and letting Terraform do the actual work.</p>
<p>Clicking to authorize an OAuth app so that your IaC tool can do things: very normal.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="was-this-overkill">Was this overkill?<a href="https://bevani.dev/blog/how-i-built-this#was-this-overkill" class="hash-link" aria-label="Direct link to Was this overkill?" title="Direct link to Was this overkill?" translate="no">​</a></h2>
<p>Obviously.</p>
<p>But the infrastructure is reproducible from scratch with three commands:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#9CDCFE;--prism-background-color:#1E1E1E"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#9CDCFE;background-color:#1E1E1E"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#9CDCFE"><span class="token function" style="color:rgb(220, 220, 170)">make</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">install</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain"></span><span class="token function" style="color:rgb(220, 220, 170)">make</span><span class="token plain"> tf-init</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain"></span><span class="token function" style="color:rgb(220, 220, 170)">make</span><span class="token plain"> deploy</span><br></div></code></pre></div></div>
<p>And every content change is just a Markdown file edit + <code>git push</code>. Cloudflare handles
the build and deploy automatically. The whole thing costs less than a coffee per month.</p>
<p>Would I do it again? Yes. Will I blog consistently now that I have this? Ask me in a year.</p>
<p>-- Bala</p>]]></content:encoded>
            <category>meta</category>
            <category>infrastructure</category>
            <category>cloudflare</category>
            <category>terraform</category>
            <category>docusaurus</category>
        </item>
        <item>
            <title><![CDATA[First post. I'm still alive.]]></title>
            <link>https://bevani.dev/blog/welcome</link>
            <guid>https://bevani.dev/blog/welcome</guid>
            <pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[So I finally have a blog. Whether this gets updated regularly is between me and my future self,]]></description>
            <content:encoded><![CDATA[<p>So I finally have a blog. Whether this gets updated regularly is between me and my future self,
who I have consistently let down with promises like "I'll document this later."</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-a-blog">Why a blog?<a href="https://bevani.dev/blog/welcome#why-a-blog" class="hash-link" aria-label="Direct link to Why a blog?" title="Direct link to Why a blog?" translate="no">​</a></h2>
<p>There's something satisfying about having a place on the internet that is definitively
mine — no algorithmic timeline, no engagement metrics, no sponsored posts.</p>
<p>Just text. Mine.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-to-expect">What to expect<a href="https://bevani.dev/blog/welcome#what-to-expect" class="hash-link" aria-label="Direct link to What to expect" title="Direct link to What to expect" translate="no">​</a></h2>
<p>Whatever comes to mind. No theme, no schedule, no promises.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-stack">The stack<a href="https://bevani.dev/blog/welcome#the-stack" class="hash-link" aria-label="Direct link to The stack" title="Direct link to The stack" translate="no">​</a></h2>
<p>This site is built with <a href="https://docusaurus.io/" target="_blank" rel="noopener noreferrer" class="">Docusaurus</a>, deployed to Cloudflare Pages,
with infrastructure managed by Terraform. Simpler than it sounds.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#9CDCFE;--prism-background-color:#1E1E1E"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#9CDCFE;background-color:#1E1E1E"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#9CDCFE"><span class="token plain">$ </span><span class="token function" style="color:rgb(220, 220, 170)">make</span><span class="token plain"> dev</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain"></span><span class="token comment" style="color:rgb(106, 153, 85)"># http://localhost:3000 -- and we're live</span><br></div></code></pre></div></div>
<p>See you around.</p>
<p>-- Bala</p>]]></content:encoded>
            <category>meta</category>
        </item>
    </channel>
</rss>