<?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[Debug The Why]]></title><description><![CDATA[Debug The Why]]></description><link>https://blog.talhasattar.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 06 Jun 2026 23:16:46 GMT</lastBuildDate><atom:link href="https://blog.talhasattar.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[I Built an LLM Product, Not Just Another Chatbot]]></title><description><![CDATA[I spent a weekend building an AI tutor that refuses to give you direct answers.
You ask it about recursion, and it asks what you already think recursion means. You ask for the derivative of x², and it]]></description><link>https://blog.talhasattar.dev/i-built-an-llm-product-not-just-another-chatbot</link><guid isPermaLink="true">https://blog.talhasattar.dev/i-built-an-llm-product-not-just-another-chatbot</guid><dc:creator><![CDATA[Talha Sattar]]></dc:creator><pubDate>Tue, 19 May 2026 19:51:50 GMT</pubDate><content:encoded><![CDATA[<p>I spent a weekend building an AI tutor that refuses to give you direct answers.</p>
<p>You ask it about recursion, and it asks what you already think recursion means. You ask for the derivative of x², and it asks what the power rule does to exponents. You say your exam is in ten minutes and beg it to just tell you, and it still asks you a smaller question instead.</p>
<p>That constraint made the project interesting.</p>
<p>The goal was not to build another chatbot UI around an LLM API. The goal was to build a tutor with a specific behaviour: guide the learner without leaking the final answer too early.</p>
<p>Three things ended up mattering most:</p>
<ol>
<li><p>The system prompt</p>
</li>
<li><p>Cost protection</p>
</li>
<li><p>Choosing the right backend pattern</p>
</li>
</ol>
<p>These are also the things that separate an LLM demo from something closer to a real product.</p>
<h2>The system prompt is the product</h2>
<p>What makes a Socratic tutor different from a generic chatbot is not only the model. It is the instruction layer around the model.</p>
<p>In my project, the most important file is not the API route. It is <code>consts/prompt.ts</code>.</p>
<p>That file controls the tutor’s behaviour. It tells the model not to give direct answers, how to respond when the user pressures it, how to ask smaller guiding questions, and how to keep the learner inside the problem instead of escaping to the final solution.</p>
<p>The prompt had to defend against patterns like authority pressure, emotional pressure, hypothetical framing, decomposition, and roleplay laundering.</p>
<p>A weak prompt says:</p>
<blockquote>
<p>Do not give the answer.</p>
</blockquote>
<p>A stronger prompt explains what to do instead:</p>
<blockquote>
<p>Ask a smaller question.<br />Check the learner’s current understanding.<br />Give a hint only when needed.<br />Move one step at a time.<br />Refuse answer-seeking attempts without sounding robotic.</p>
</blockquote>
<p>That was the first big lesson: prompt writing is not just wording. It is behaviour design.</p>
<p>The model is still the same model. But the product feels different because the prompt creates a decision path for each user message.</p>
<p>Three pages of prompt. A small API call. But the prompt is what turns the API call into a tutor.</p>
<h2>Cost protection is real engineering</h2>
<p>A naive LLM route is easy to write:</p>
<ol>
<li><p>Receive the message</p>
</li>
<li><p>Call the model</p>
</li>
<li><p>Return the answer</p>
</li>
</ol>
<p>That is fine for a local experiment. It is not enough for a public AI app.</p>
<p>The moment the app is live, every request has a real cost attached to it. If someone abuses the route, it is not just a bug. It can become a bill.</p>
<p>So the API route became more than a wrapper around Anthropic. It became a control layer.</p>
<p>Before the model is called, the request goes through Zod validation, so malformed message history never reaches the API.</p>
<p>Then I identify the request source using the forwarded IP header:</p>
<pre><code class="language-ts">const userIp = req.headers.get("x-forwarded-for")?.split(",")[0].trim();
</code></pre>
<p>That lets me create a daily Redis key per IP:</p>
<pre><code class="language-ts">const ipUsageKey = `ipUsage:\({userIp}:\){today}`;
</code></pre>
<p>I used Redis because this state is temporary, fast-changing, and does not need to live in a permanent database.</p>
<p>The IP limiter uses <code>INCR</code> first:</p>
<pre><code class="language-ts">const newIpCount = await redis.incr(ipUsageKey);
</code></pre>
<p>That matters because Redis increments are atomic.</p>
<p>A naive read-then-write limiter can break under concurrency. Two requests can read the same old value, both pass the check, and both move forward.</p>
<p>With <code>INCR</code>, Redis handles the count safely.</p>
<p>If the user goes over the daily limit, I refund the count:</p>
<pre><code class="language-ts">if (newIpCount &gt; DAILY_IP_REQUEST_LIMIT) {
  await redis.decr(ipUsageKey);
}
</code></pre>
<p>That <code>DECR</code> looks small, but it matters. Without it, even rejected requests would keep increasing the count and could lock the user out unfairly.</p>
<p>I also added global daily token caps. Instead of only limiting requests, I track input and output tokens separately:</p>
<pre><code class="language-ts">const InputTokenKey = `DailyInputToken:Global:${today}`;
const OutputTokenKey = `DailyOutputToken:Global:${today}`;
</code></pre>
<p>This gives the app a hard daily ceiling.</p>
<p>After the model responds, I update the counters using the actual token usage returned by Anthropic:</p>
<pre><code class="language-ts">await Promise.all([
  incrementDailyTokenUsage(InputTokenKey, inputToken),
  incrementDailyTokenUsage(OutputTokenKey, outputToken)
]);
</code></pre>
<p>I used <code>Promise.all</code> because these two Redis writes do not depend on each other.</p>
<p>None of this makes the UI look more impressive. The user still sees a simple chat box. But this is the backend work that decides whether the project can safely stay online.</p>
<h2>API routes vs Server Actions, and why I did not stream</h2>
<p>I also had to decide how the frontend should talk to the backend.</p>
<p>Server Actions are great for many things in Next.js: forms, mutations, CRUD operations, and reducing client-side fetch boilerplate.</p>
<p>But for this project, I used a plain API route because I wanted clear control over request validation, response status codes, rate limiting, token accounting, and error handling.</p>
<p>I also deliberately chose not to stream the response.</p>
<p>That might sound strange because streaming is common in AI apps. But engineering is not about adding a feature because everyone else is doing it. It is about asking whether the user can actually feel the difference.</p>
<p>In this project, responses usually come back quickly enough that a loading bubble communicates the state clearly.</p>
<p>So instead of adding streaming complexity early, I kept the flow simple:</p>
<ol>
<li><p>User sends a message</p>
</li>
<li><p>UI shows a thinking indicator</p>
</li>
<li><p>API route validates and checks limits</p>
</li>
<li><p>Model responds</p>
</li>
<li><p>UI appends the tutor message</p>
</li>
</ol>
<p>The code stays easier to reason about, and token accounting stays cleaner because the full response is handled in one place.</p>
<p>If response time becomes a real UX issue later, streaming can be added. But it did not need to be the first version.</p>
<p>That was the lesson:</p>
<p>Do not cargo-cult complexity. Add it when the user can feel its absence.</p>
<h2>The frontend still matters</h2>
<p>The backend makes the app safe. The frontend makes it feel usable.</p>
<p>I added small UX decisions that are easy to ignore but matter in practice.</p>
<p>The chat automatically scrolls to the newest message. The input clears immediately after sending. The app disables sending while the tutor is thinking. Errors show as toast messages instead of silently failing.</p>
<p>On desktop, the input refocuses after sending so the user can keep typing naturally. On mobile, I avoid forcing focus back into the input because that can reopen the keyboard and make the experience annoying.</p>
<p>That detail is small, but it is product thinking.</p>
<p>A polished AI app is not only about the model response. It is also about everything around the response.</p>
<h2>What's in the box</h2>
<p>So this was not just:</p>
<blockquote>
<p>I built a chatbot.</p>
</blockquote>
<p>It was closer to:</p>
<blockquote>
<p>I built a constrained LLM product with a researched system prompt, validated request handling, Redis-backed usage limits, global token caps, real token accounting, graceful error handling, and a mobile-aware chat UI.</p>
</blockquote>
<p>The final product is simple on purpose.</p>
<p>One chat interface. One API route. One model.</p>
<p>But behind that simplicity are the decisions that make the app safer, cheaper, and more reliable.</p>
<p>If you are a junior developer building an AI portfolio project, my advice is this:</p>
<p>Do not just build a chatbot.</p>
<p>Build a product with constraints. Pick a behaviour. Make the prompt enforce that behaviour. Protect the route. Track cost. Validate input. Handle failure. Then explain those decisions clearly.</p>
<p>That is where the real engineering shows up.</p>
<p>Demo: <a href="https://socratic-tutor-eight.vercel.app/">https://socratic-tutor-eight.vercel.app/</a></p>
<p>Code: <a href="https://github.com/itstalhasattar/socratic-tutor">https://github.com/itstalhasattar/socratic-tutor</a></p>
<p>The system prompt is in <code>consts/prompt.ts</code>.</p>
<p>It is worth reading because that is where most of the product behaviour lives.</p>
]]></content:encoded></item><item><title><![CDATA[Whitelisting an API With a Static IP Using Nginx]]></title><description><![CDATA[The Problem
We integrate with SuperControl, a property management API. Their security model is the old-school kind: they don't issue API keys you carry in a header. Instead, you give them a list of IP]]></description><link>https://blog.talhasattar.dev/whitelisting-an-api-with-a-static-ip-using-nginx</link><guid isPermaLink="true">https://blog.talhasattar.dev/whitelisting-an-api-with-a-static-ip-using-nginx</guid><dc:creator><![CDATA[Talha Sattar]]></dc:creator><pubDate>Wed, 13 May 2026 01:02:31 GMT</pubDate><content:encoded><![CDATA[<hr />
<h2>The Problem</h2>
<p>We integrate with <strong>SuperControl</strong>, a property management API. Their security model is the old-school kind: they don't issue API keys you carry in a header. Instead, you give them a list of IP addresses, and they only accept requests coming from those IPs. Anything else gets dropped at the edge.</p>
<p>That works perfectly if your backend is one server with one IP. It falls apart the moment you deploy on Vercel.</p>
<p>Vercel runs your code on serverless functions. Every invocation can come from a different machine in a different data center, with a different outbound IP. The whole pool changes over time — they add new edges, retire old ones, scale up during traffic spikes. There's no published list of stable IPs to whitelist, and even if there were, it would be huge and changing.</p>
<p>So I had two options. Either ask SuperControl to whitelist "the internet" (not going to happen), or put something in the middle that has <em>one</em> IP that never changes.</p>
<h2>The Managed Alternatives (and Why I Didn't Use Them)</h2>
<p>This problem is common enough that every major platform has a paid product for it. The question is whether the price tag matches the integration.</p>
<ul>
<li><p><strong>Vercel Static IPs</strong> — $100/month per project on the Pro plan, plus regional Private Data Transfer fees on top. Available on Pro and Enterprise; not on Hobby.</p>
</li>
<li><p><strong>AWS NAT Gateway</strong> — \(0.045/hour (~\)32.85/month) per gateway, plus \(0.045/GB processed, plus standard data transfer out. A single-AZ setup at low traffic lands around \)35–40/month; multi-AZ production setups easily run $100+ before data charges.</p>
</li>
<li><p><strong>Third-party proxies like QuotaGuard</strong> — around $19/month for entry tiers. Cheaper than the cloud-native options, but it's still a subscription to a black box.</p>
</li>
</ul>
<p>All of them solve the problem. Justifiable at scale, when the cost is a rounding error against engineering hours saved. Overkill for a single integration where the upstream API itself doesn't justify enterprise spend.</p>
<p>The DIY answer: a $5/month VPS from any provider (Hetzner, DigitalOcean, Vultr) with a permanent IP and Nginx installed. Same outcome, a fraction of the cost. The trade-off is that you're now responsible for keeping the box patched and the cert renewed — Certbot's auto-renewal handles the second part; weekly <code>apt update &amp;&amp; apt upgrade</code> handles the first.</p>
<p>For a single low-volume integration with one upstream, the trade is worth it. The moment I'm running ten of these or doing high-volume traffic, I'd reconsider.</p>
<h2>Quick Detour: What's a Reverse Proxy? What's Nginx?</h2>
<p>Skip this section if you already know.</p>
<p>A <strong>reverse proxy</strong> is a server that sits in front of another server and forwards traffic to it. The client thinks it's talking to the proxy; the proxy actually talks to the real backend on the client's behalf. "Reverse" because a normal proxy hides the client from the server (think VPN), while a reverse proxy hides the server from the client. Reverse proxies are how big sites do load balancing, caching, SSL termination, rate limiting, and — in our case — IP consolidation.</p>
<p><strong>Nginx</strong> (pronounced "engine-x") is the software you run on a server to make it a reverse proxy. It's one of the most-used web servers on the internet, free, fast, and configured through plain text files. You describe what should happen to incoming requests, reload it, and it does that. No code, just rules.</p>
<p>Put together: I'm running Nginx on a VPS, configured so that requests hitting my proxy domain get forwarded to SuperControl's API.</p>
<h2>The Solution</h2>
<pre><code class="language-plaintext">App on Vercel (dynamic IPs)
            ↓
Nginx on VPS (one static IP — whitelisted with SuperControl)
            ↓
SuperControl API
</code></pre>
<p>The VPS costs a few dollars a month and has a fixed IP. I gave that one IP to SuperControl. Every API call from the app now hits the VPS first, the VPS forwards it upstream, and SuperControl sees the same trusted address every time.</p>
<p>The catch: that subdomain is now publicly reachable. If I left it open, anyone who guessed the URL could use my VPS as a free, pre-authorized gateway into SuperControl's API. So the proxy needs its own auth layer — a shared secret in a custom header that the app sends and the VPS verifies before forwarding anything.</p>
<h2>The Config</h2>
<pre><code class="language-nginx">server {
    listen 443 ssl;
    server_name proxy.example.com;

    # Certbot-managed SSL certs here

    location = /health {
        return 200 "ok\n";
    }

    location / {
        if ($http_x_proxy_auth_key != "REPLACE_WITH_SECRET") {
            return 401;
        }

        proxy_pass https://api.upstream.example.com;
        proxy_ssl_server_name on;

        proxy_set_header Host api.upstream.example.com;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header x-proxy-auth-key "";
    }
}

server {
    listen 80;
    server_name proxy.example.com;
    return 301 https://\(host\)request_uri;
}
</code></pre>
<p>Forty lines doing six things:</p>
<ol>
<li><p><strong>HTTPS termination</strong> with a Certbot/Let's Encrypt cert, and HTTP→HTTPS redirect for anything that knocks on port 80.</p>
</li>
<li><p><code>/health</code> as an exact-match endpoint for uptime monitors — no auth, no logging noise, just <code>200 ok</code>.</p>
</li>
<li><p><strong>Auth check</strong> on every other path: if the <code>x-proxy-auth-key</code> header doesn't match the shared secret, return 401 and forward nothing.</p>
</li>
<li><p><code>proxy_pass</code> forwards the request to SuperControl, preserving the path, query string, method, and body.</p>
</li>
<li><p><code>Host</code> <strong>header rewrite</strong> so the upstream sees its own domain instead of our proxy subdomain — most APIs reject requests with the wrong <code>Host</code>.</p>
</li>
<li><p><strong>Strip the secret</strong> before forwarding. The auth key was for us; SuperControl has no business seeing it.</p>
</li>
</ol>
<h2>Three Things Worth Knowing</h2>
<p><strong>The header-to-variable rule.</strong> Nginx exposes every incoming header as a variable. The conversion: lowercase, replace hyphens with underscores, prefix with <code>\(http_</code>. So <code>x-proxy-auth-key</code> becomes <code>\)http_x_proxy_auth_key</code>, <code>Authorization</code> becomes <code>\(http_authorization</code>, <code>Content-Type</code> becomes <code>\)http_content_type</code>. Once you know the rule, every header is readable inside the config.</p>
<p><code>location = /path</code> <strong>vs</strong> <code>location /path</code><strong>.</strong> The <code>=</code> makes it an exact match — <code>/health</code> and nothing else. Without <code>=</code>, it's a prefix match, so <code>/</code> catches everything underneath it. This isn't just style: exact matches are checked first and short-circuit the rest of the routing, which is why they're the right choice for things like health checks.</p>
<p><code>proxy_ssl_server_name on</code> is the gotcha that ate an hour of my time. SuperControl, like most modern APIs, uses SNI to pick which certificate to present during the TLS handshake. Without this directive, Nginx opens the connection without telling the upstream which hostname it's asking for, and the handshake fails with errors that look like generic 502s. The fix is one line; finding it isn't.</p>
<h2>The Test Loop</h2>
<pre><code class="language-bash">sudo nginx -t                          # validate config syntax
sudo systemctl reload nginx            # apply if valid

# Health check — should always return 200
curl -i https://proxy.example.com/health

# Authed call — should return whatever the upstream returns
curl -i https://proxy.example.com/endpoint \
  -H "x-proxy-auth-key: REPLACE_WITH_SECRET"

# No header — should return 401, never touch the upstream
curl -i https://proxy.example.com/endpoint
</code></pre>
<p><code>nginx -t</code> is the most important habit. It validates the whole file before you reload — catches misplaced braces, missing semicolons, and unreachable blocks before they take down a live config. Never reload without it.</p>
<h2>Takeaway</h2>
<p>Vercel and friends abstract away IPs, DNS, SSL, and routing so well that you can forget the network layer exists — until a third-party constraint forces you back into it. Forty lines of Nginx and a $5 VPS turned an unsolvable problem into a solved one. The skill isn't memorizing the syntax; it's recognizing when the abstraction has run out and being willing to drop a layer down to fix it.</p>
]]></content:encoded></item><item><title><![CDATA[Stop Guessing. Here's When to Use SSR, CSR, SSG and ISR in Next.js]]></title><description><![CDATA[The web is evolving fast. New frameworks are shipping optimized solutions every month to make the web faster and more secure for everyone. But with all these options comes confusion especially for beg]]></description><link>https://blog.talhasattar.dev/stop-guessing-here-s-when-to-use-ssr-csr-ssg-and-isr-in-next-js</link><guid isPermaLink="true">https://blog.talhasattar.dev/stop-guessing-here-s-when-to-use-ssr-csr-ssg-and-isr-in-next-js</guid><dc:creator><![CDATA[Talha Sattar]]></dc:creator><pubDate>Thu, 09 Apr 2026 01:43:11 GMT</pubDate><content:encoded><![CDATA[<p>The web is evolving fast. New frameworks are shipping optimized solutions every month to make the web faster and more secure for everyone. But with all these options comes confusion especially for beginners.</p>
<p>Next.js alone gives you SSR, CSR, SSG and ISR. Four different rendering strategies. Each one sounds great until you try to figure out which one your project actually needs.</p>
<p>I'm going to break all of them down. Not just what they are but when to use each one and how they affect your SEO because sometimes what looks like the obvious choice is actually the wrong one for your situation.</p>
<p>Let's get into it.</p>
<h2>SSR: Server-Side Rendering</h2>
<p>When a user visits a page that uses SSR the server runs your React code right at that moment. It generates the full HTML and sends it to the browser. The user sees a complete page almost instantly.</p>
<p>But here's the thing. That page isn't interactive yet. It looks ready but buttons don't work and forms don't respond. In the background React is "hydrating" the page which means it's attaching all the event listeners and state management to the HTML that's already on screen. Once hydration finishes everything becomes interactive.</p>
<p>The psychology behind this matters. Users perceive your site as fast because they see content immediately. The brief moment before hydration completes is almost never noticed.</p>
<p>The two metrics that matter for SSR are First Contentful Paint (how quickly the user sees something) and Time to Interactive (how quickly they can actually use it). SSR optimizes heavily for the first one.</p>
<p><strong>SEO impact:</strong> SSR is great for SEO. When Google's crawler hits your page it gets fully rendered HTML with all the content right there. No waiting for JavaScript to execute. Your meta tags, headings, text content and structured data are all present in the initial response. This is why most content heavy sites that care about search rankings go with SSR.</p>
<p>One tradeoff to know: you don't have access to browser APIs like window or localStorage during SSR because the code runs on the server. If your component needs those you'll need to handle that with client-side checks or move that logic to a client component.</p>
<h2>CSR: Client-Side Rendering</h2>
<p>CSR is the traditional React approach. The browser downloads your JavaScript bundle then the V8 engine executes it and React builds the entire page on the client side.</p>
<p>This means two things matter more than anything: the user's device performance and their network speed.</p>
<p>Here's where it gets interesting. Think about who your users actually are.</p>
<p>If you're building a luxury helicopter rental platform for clients in New York those users almost certainly have the latest iPhones fast home internet and 5G connections. For them CSR and SSR will feel almost identical in speed. The JavaScript bundle downloads in milliseconds on their connection and their device processes it instantly.</p>
<p>Now imagine you're building a government services portal used by people across rural areas with older phones and slower networks. That same JavaScript bundle that loaded instantly in New York might take 5 to 8 seconds to download and another few seconds to execute on a budget Android device. Suddenly SSR isn't just a nice optimization. It's the difference between a usable site and one that people abandon.</p>
<p>The rendering strategy you choose should be based on who is using your product not just what the technology can do.</p>
<p><strong>SEO impact:</strong> CSR is the worst option for SEO. When Google's crawler visits a CSR page it initially sees an empty div or a loading spinner. Google can execute JavaScript and eventually see your content but it's a second pass and not guaranteed. Your pages may get indexed slower or with missing content. If you don't care about SEO like an admin dashboard or internal tool then CSR is totally fine. But if you need organic traffic from search don't use CSR for those pages.</p>
<h2>SSG: Static Site Generation</h2>
<p>SSG generates your pages at build time. When you run <code>next build</code> it pre-renders every page into static HTML files. These files sit on a CDN and when someone visits they just get served instantly. No server processing. No waiting.</p>
<p>This is the fastest option because there's literally nothing to compute at request time. The HTML already exists.</p>
<p>Use SSG when the content doesn't change frequently and isn't different per user. Think marketing pages, blog posts, documentation, landing pages and about pages. Your content is the same for everyone and it only changes when you deploy.</p>
<p>The downside is obvious. If your data changes you need to rebuild and redeploy. For a blog with 50 posts that's fine. For an e-commerce site with 100,000 products that update prices every hour that's not practical.</p>
<p><strong>SEO impact:</strong> SSG is the best option for SEO. The HTML is pre-built and sitting on a CDN so Google gets it instantly. Page speed is as fast as it gets which Google directly uses as a ranking factor. Your content is fully rendered with all meta tags and structured data baked in. If your pages don't need to change per user and you want the best possible search rankings SSG is the answer.</p>
<h2>ISR: Incremental Static Regeneration</h2>
<p>ISR is the hybrid approach that tries to give you the speed of SSG with the freshness of SSR.</p>
<p>It works like this. You statically generate the page at build time just like SSG. But you set a revalidation time. After that time passes the next visitor triggers a background regeneration of the page. They still get the cached version instantly but the next visitor after them gets the freshly regenerated page.</p>
<p>Think of it as SSG with an expiration date.</p>
<p>This is perfect for content that changes but not in real time. Product pages where prices update daily. A news site where articles are published every few hours. A dashboard that shows data that refreshes every 30 minutes. The content needs to be relatively fresh but it doesn't need to be live to the second.</p>
<p><strong>SEO impact:</strong> ISR gives you almost the same SEO benefits as SSG. The page is pre-rendered so Google sees full HTML on the first crawl. And because the page regenerates in the background your content stays fresh for subsequent crawls without sacrificing speed. This is the sweet spot for sites that need both good SEO and regularly updated content. E-commerce product pages and news sites use this a lot.</p>
<h2>So Which One Do You Actually Use?</h2>
<p>Here's the decision framework:</p>
<p><strong>Use SSG</strong> when your content rarely changes and is the same for all users. Blogs. Docs. Marketing sites. Best SEO. Fastest performance. This should be your default starting point.</p>
<p><strong>Use ISR</strong> when your content changes periodically but doesn't need to be real-time. Product catalogs. News articles. Nearly as good for SEO as SSG but your data stays fresh.</p>
<p><strong>Use SSR</strong> when the content is different for every user or every request. Personalized dashboards. Search results. Pages that depend on cookies or authentication. Great for SEO when you need dynamic content that search engines should still index.</p>
<p><strong>Use CSR</strong> when the page is behind authentication anyway and SEO doesn't matter. Admin panels. Internal tools. Complex interactive apps where the initial load time is less important than the runtime experience.</p>
<h2>Quick Reference</h2>
<table>
<thead>
<tr>
<th>Strategy</th>
<th>Built When</th>
<th>SEO</th>
<th>Speed</th>
<th>Best For</th>
</tr>
</thead>
<tbody><tr>
<td>SSG</td>
<td>Deploy time</td>
<td>Best</td>
<td>Fastest</td>
<td>Blogs docs landing pages</td>
</tr>
<tr>
<td>ISR</td>
<td>Deploy + revalidates</td>
<td>Great</td>
<td>Fast</td>
<td>Products news catalogs</td>
</tr>
<tr>
<td>SSR</td>
<td>Every request</td>
<td>Great</td>
<td>Good</td>
<td>Personalized dynamic pages</td>
</tr>
<tr>
<td>CSR</td>
<td>In browser</td>
<td>Poor</td>
<td>Depends on device</td>
<td>Admin panels internal tools</td>
</tr>
</tbody></table>
<h2>The Real Lesson</h2>
<p>The framework gives you these options. It doesn't tell you which one to pick. That's your job as an engineer. Understanding your users their devices their networks your SEO requirements and your content update frequency is what makes the difference.</p>
<p>A rendering strategy isn't a technical decision. It's a product decision.</p>
<p>And that's the kind of thinking that no tutorial and no AI tool is going to do for you.</p>
]]></content:encoded></item></channel></rss>