<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Building YUNOVA]]></title><description><![CDATA[Building YUNOVA]]></description><link>https://yunova.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Thu, 12 Nov 2026 07:22:07 GMT</lastBuildDate><atom:link href="https://yunova.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Our Next.js 16 site broke for signed-out visitors: "Page changed from static to dynamic at runtime"]]></title><description><![CDATA[ISR pages that read headers() or cookies() crashed with 500s for signed-out users after a restart. Root cause, the fix, and lessons.]]></description><link>https://yunova.hashnode.dev/our-next-js-16-site-broke-for-signed-out-visitors-page-changed-from-static-to-dynamic-at-runtime</link><guid isPermaLink="true">https://yunova.hashnode.dev/our-next-js-16-site-broke-for-signed-out-visitors-page-changed-from-static-to-dynamic-at-runtime</guid><category><![CDATA[Next.js]]></category><category><![CDATA[debugging]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[supabase]]></category><dc:creator><![CDATA[Harish Saraf]]></dc:creator><pubDate>Wed, 11 Nov 2026 20:09:12 GMT</pubDate><content:encoded><![CDATA[<p>I run <a href="https://yunova.in">YUNOVA</a>, a free learning platform that turns the best YouTube lessons into step-by-step skill paths (Excel, Tally, Python, exam prep and more) in Hindi and English. It is a Next.js 16 app with a self-hosted Supabase, all on one Oracle Cloud VM in Docker.</p>
<p>This week our most important pages quietly returned <strong>500 Internal Server Error</strong> to every signed-out visitor for about a day. Signed-in users (including me) saw nothing wrong. Here is what happened, how we found it, and the fix.</p>
<h2>The symptom</h2>
<p>Learners started messaging that "the website is stuck". The home page, Explore and Teachers loaded fine. But every skill page, lesson page and exam-prep page failed:</p>
<pre><code class="language-plaintext">/              200
/skills         200
/skills/excel   500
/skills/tally   500
/study/...      500
</code></pre>
<p>It failed on every request, not intermittently, so it was not load or memory.</p>
<h2>The log line</h2>
<p>The container logs had thousands of these:</p>
<pre><code class="language-plaintext">Error: Page changed from static to dynamic at runtime /skills/excel, reason: headers
see more here https://nextjs.org/docs/messages/app-static-to-dynamic-error
</code></pre>
<h2>The root cause</h2>
<p>Those routes were set up for ISR (cache the rendered page for an hour):</p>
<pre><code class="language-ts">export const revalidate = 3600;
export function generateStaticParams() { return []; }
</code></pre>
<p>But the same pages are personalised. They show a "Get a free certificate" card only to signed-out visitors, and a Continue button to signed-in learners. To know who is visiting, a shared helper reads the request:</p>
<pre><code class="language-ts">export const getViewer = cache(async () =&gt; {
  const bearer = readBearerHeader(await headers()); // mobile app token
  ...
  const claims = await claimsFromCookies((await cookies()).getAll());
  ...
});
</code></pre>
<p>A page cannot be both "render once and reuse for an hour" and "read this visitor's headers". Next.js detects that conflict during a background render and throws, so the request gets a 500.</p>
<p>Why did it work for weeks? Previously cached copies kept being served while regeneration failed in the background. After a deploy and a container restart, the cache was cold, so every signed-out visit triggered a fresh render, and every fresh render threw.</p>
<h2>The fix</h2>
<p>The pages are personalised, so they should be dynamic. On the 30 affected routes:</p>
<pre><code class="language-diff">- export const revalidate = 3600;
+ export const dynamic = "force-dynamic";
</code></pre>
<p>After deploying, every page returned 200 in 0.3–0.7 s, and the error count in the logs dropped to zero.</p>
<h2>One more trap: our deploy script undid the hotfix</h2>
<p>I committed the hotfix directly on the server and ran the deploy script. The site stayed broken. The script does:</p>
<pre><code class="language-bash">git fetch origin "$BRANCH"
git reset --hard "origin/$BRANCH"
</code></pre>
<p>The server's origin is a git bundle uploaded from my machine. The reset threw away the local commit and rebuilt the old code. The fix was to rebuild the bundle from the hotfix commit, then deploy. Lesson: know exactly what your deploy script resets to before you hotfix on the box.</p>
<h2>What we changed so it doesn't happen silently again</h2>
<ol>
<li><strong>Uptime monitoring on a deep page, not just the home page.</strong> The home page was 200 the whole time. We now check a skill page every 5 minutes too.</li>
<li><strong>Personalised pages are dynamic by default.</strong> If we want the cache back, the signed-in parts must move to the client, so the server HTML is the same for everyone.</li>
<li><strong>Test as a signed-out visitor after every deploy.</strong> Founders are always signed in, so we never see what new visitors see.</li>
</ol>
<h2>Takeaways</h2>
<ul>
<li>If an ISR page (or anything it renders) calls `headers()` or `cookies()`, Next.js can fail at runtime, not at build time.</li>
<li>"Works for me" means little when you are signed in.</li>
<li>Monitor the pages your users actually land on.</li>
</ul>
<hr />
<p><em>I'm building</em> <a href="https://yunova.in"><em>YUNOVA</em></a> <em>in public: a free platform for learning job skills from the best YouTube teachers, with practice questions and free certificates. If you are learning Excel, start here:</em> <a href="https://yunova.in/skills/excel"><em>yunova.in/skills/excel</em></a><em>.</em></p>
]]></content:encoded></item></channel></rss>