<?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>DEV Community: Vishal Solanki</title>
    <description>The latest articles on DEV Community by Vishal Solanki (@vishall).</description>
    <link>https://dev.to/vishall</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F709938%2Fcfafc20f-a852-4dd5-bfca-e30c0a147cba.png</url>
      <title>DEV Community: Vishal Solanki</title>
      <link>https://dev.to/vishall</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vishall"/>
    <language>en</language>
    <item>
      <title>Core Web Vitals in 2026: fixing LCP, INP and CLS in Next.js</title>
      <dc:creator>Vishal Solanki</dc:creator>
      <pubDate>Mon, 26 Oct 2026 19:57:14 +0000</pubDate>
      <link>https://dev.to/vishall/core-web-vitals-in-2026-fixing-lcp-inp-and-cls-in-nextjs-2do7</link>
      <guid>https://dev.to/vishall/core-web-vitals-in-2026-fixing-lcp-inp-and-cls-in-nextjs-2do7</guid>
      <description>&lt;p&gt;Most slow sites don't have one big problem. They have many small waits, stacked in the wrong order. On one production app I cut the JavaScript bundle by about half, and the biggest gains showed up exactly where they matter most: on low-end phones on ordinary mobile connections.&lt;/p&gt;

&lt;p&gt;This is the playbook I use to find and fix those waits in a Next.js site, organised around the three numbers Google actually measures.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three metrics, and what "good" means
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;th&gt;Good&lt;/th&gt;
&lt;th&gt;Poor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;LCP&lt;/strong&gt;, Largest Contentful Paint&lt;/td&gt;
&lt;td&gt;How long until the biggest thing on screen (usually a hero image or headline) appears&lt;/td&gt;
&lt;td&gt;2.5 s or less&lt;/td&gt;
&lt;td&gt;over 4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;INP&lt;/strong&gt;, Interaction to Next Paint&lt;/td&gt;
&lt;td&gt;How long the page takes to visibly respond after a tap, click or key press&lt;/td&gt;
&lt;td&gt;200 ms or less&lt;/td&gt;
&lt;td&gt;over 500 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;CLS&lt;/strong&gt;, Cumulative Layout Shift&lt;/td&gt;
&lt;td&gt;How much the layout jumps around while loading&lt;/td&gt;
&lt;td&gt;0.1 or less&lt;/td&gt;
&lt;td&gt;over 0.25&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Google judges each one at the &lt;strong&gt;75th percentile of real visits&lt;/strong&gt;, separately for mobile and desktop. INP replaced First Input Delay in March 2024, and it's the one most sites now fail, because it covers every interaction, not just the first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Field data versus lab data
&lt;/h3&gt;

&lt;p&gt;PageSpeed Insights shows two different things on one page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The top section is field data&lt;/strong&gt;: real Chrome users over the last 28 days (the Chrome UX Report). This is what Google uses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Lighthouse score underneath is lab data&lt;/strong&gt;: one simulated load on a throttled device. It's great for debugging, varies from run to run, and is not what ranks you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Lighthouse 100 with failing field data is still failing. A Lighthouse 70 with green field data is fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure first
&lt;/h2&gt;

&lt;p&gt;Guessing wastes days. Find out which metric fails, on which pages, before you change anything.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Search Console → Core Web Vitals&lt;/strong&gt; groups your URLs by problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PageSpeed Insights&lt;/strong&gt; names the LCP element and the worst layout shifts on a given page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chrome DevTools → Performance&lt;/strong&gt; shows the LCP element, each interaction's INP and every layout shift, live, as you use the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your own real-user data&lt;/strong&gt;, sent to analytics from a small client component:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&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;useReportWebVitals&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/web-vitals&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;function&lt;/span&gt; &lt;span class="nf"&gt;WebVitals&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useReportWebVitals&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Send LCP, INP and CLS to your analytics, e.g. as a GA4 event or to your own endpoint.&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataLayer&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="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;web_vitals&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;rating&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rating&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="kc"&gt;null&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;
  
  
  Fixing LCP
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Find out what the LCP element is.&lt;/strong&gt; PageSpeed Insights tells you. The fix is completely different for an image than for a heading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. If it's an image,&lt;/strong&gt; serve it with &lt;code&gt;next/image&lt;/code&gt;, tell the browser to fetch it early, and give it accurate &lt;code&gt;sizes&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Image&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/image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt;
  &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/hero.jpg"&lt;/span&gt;
  &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"The living room at dusk"&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1600&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;sizes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"100vw"&lt;/span&gt;
  &lt;span class="na"&gt;preload&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Next.js 16, &lt;code&gt;preload&lt;/code&gt; replaced the old &lt;code&gt;priority&lt;/code&gt; prop. Use it on one image per page, the LCP one. Enable modern formats once in &lt;code&gt;next.config.ts&lt;/code&gt; with &lt;code&gt;images: { formats: ["image/avif", "image/webp"] }&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. If it's text,&lt;/strong&gt; the font is usually the bottleneck. &lt;code&gt;next/font&lt;/code&gt; self-hosts and preloads it. Keep only the weights and subsets you actually use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Don't hide the LCP element behind an intro animation.&lt;/strong&gt; Text that starts at &lt;code&gt;opacity: 0&lt;/code&gt; and waits for JavaScript to fade it in can't count as painted until it's visible. On this site the hero intro only plays on desktop. Phones get the hero immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Check the server response.&lt;/strong&gt; Statically generated pages served from a CDN are fastest. If time to first byte is slow, everything after it is slow too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing INP
&lt;/h2&gt;

&lt;p&gt;INP is the delay between a user's input and the next frame the browser paints. It's almost always caused by too much JavaScript running on the main thread at the wrong moment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Ship less JavaScript.&lt;/strong&gt; Keep components on the server unless they need state, effects or browser APIs. Push &lt;code&gt;"use client"&lt;/code&gt; as far down the tree as you can, and check what each route actually ships in your bundle analysis. Less code means less to download, parse and hydrate before the page can respond.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Show feedback first, then do the heavy work.&lt;/strong&gt; Break long tasks up so the browser can paint in between:&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;// Let the browser paint before continuing a long task.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;yieldToMain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;globalThis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="k"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;?.()&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;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onApplyFilters&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// visible feedback, painted right away&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;yieldToMain&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;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;expensiveFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allItems&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// the slow part&lt;/span&gt;
  &lt;span class="nf"&gt;setItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&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;3. Mark expensive re-renders as non-urgent.&lt;/strong&gt; For things like filtering a long list as someone types, wrap the update in &lt;code&gt;startTransition&lt;/code&gt; (from &lt;code&gt;useTransition&lt;/code&gt;) or render from &lt;code&gt;useDeferredValue&lt;/code&gt;, so typing stays responsive while the list catches up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Keep animation off the main thread where you can.&lt;/strong&gt; Animate &lt;code&gt;transform&lt;/code&gt; and &lt;code&gt;opacity&lt;/code&gt; rather than &lt;code&gt;top&lt;/code&gt;, &lt;code&gt;width&lt;/code&gt; or &lt;code&gt;height&lt;/code&gt;. Don't read layout (&lt;code&gt;getBoundingClientRect&lt;/code&gt;, &lt;code&gt;offsetHeight&lt;/code&gt;) inside scroll handlers. Load heavy animation code only for the sections that use it, and honour &lt;code&gt;prefers-reduced-motion&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Load third-party scripts late.&lt;/strong&gt; Tag managers, chat widgets and heatmaps often cause the worst INP. On this site, Google Tag Manager loads on the first interaction, or after a few seconds if there isn't one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing CLS
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Reserve space for everything that loads later.&lt;/strong&gt; Give images &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt;. Give embeds, videos and ads a fixed &lt;code&gt;aspect-ratio&lt;/code&gt; box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Stop fonts from shifting text.&lt;/strong&gt; &lt;code&gt;next/font&lt;/code&gt; generates a fallback font with matching metrics, which removes most of the jump when the real font arrives. For long body text you can go further with &lt;code&gt;display: "optional"&lt;/code&gt;: on a slow first visit the fallback simply stays, rather than swapping mid-read. This site uses that for its body font.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Never insert content above what's already on screen.&lt;/strong&gt; Cookie bars, promo banners and "related" blocks should overlay the page or have space reserved for them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Animate with &lt;code&gt;transform&lt;/code&gt;.&lt;/strong&gt; Changing &lt;code&gt;height&lt;/code&gt;, &lt;code&gt;margin&lt;/code&gt; or &lt;code&gt;top&lt;/code&gt; moves everything below. A &lt;code&gt;transform&lt;/code&gt; or an &lt;code&gt;opacity&lt;/code&gt; fade doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 30-minute audit routine
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open Search Console → Core Web Vitals. Note which metric fails, on mobile or desktop, and which URL groups.&lt;/li&gt;
&lt;li&gt;Run PageSpeed Insights on one URL from each failing group. Write down the LCP element and the largest layout shifts.&lt;/li&gt;
&lt;li&gt;Open the page in Chrome DevTools with CPU throttling at 4× and a "Fast 4G" network. Record a Performance trace while loading the page and tapping the main controls.&lt;/li&gt;
&lt;li&gt;In the trace, look for long tasks (over 50 ms) right after load and right after each tap. Note what code they belong to.&lt;/li&gt;
&lt;li&gt;Fix the single biggest item, deploy, and wait. Field data trails your changes by up to 28 days, so give each fix time to show.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do Core Web Vitals affect Google rankings?
&lt;/h3&gt;

&lt;p&gt;Yes, as part of Google's page experience signals. Relevance and content quality still matter more, so a fast page with the wrong content won't outrank a slower page that answers the question. Speed is what keeps people on the page once they arrive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does PageSpeed Insights give a different score every time?
&lt;/h3&gt;

&lt;p&gt;The Lighthouse score is a single simulated run, and it varies with server response time and test conditions. Judge progress by the field data at the top of the report and in Search Console.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need a Lighthouse score of 100?
&lt;/h3&gt;

&lt;p&gt;No. Aim to pass all three Core Web Vitals in field data, on mobile, for your important pages. Past that point, your time is usually better spent on content.&lt;/p&gt;




&lt;p&gt;Is your site slow on phones, or failing Core Web Vitals in Search Console? &lt;a href="https://www.vishalsolanki.online/#contact" rel="noopener noreferrer"&gt;I find the cause and fix it&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.vishalsolanki.online/blog/core-web-vitals-nextjs-lcp-inp-cls" rel="noopener noreferrer"&gt;vishalsolanki.online&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>corewebvitals</category>
      <category>performance</category>
      <category>nextjs</category>
      <category>inp</category>
    </item>
    <item>
      <title>Next.js as a backend: Server Actions, Route Handlers or a separate Node.js API?</title>
      <dc:creator>Vishal Solanki</dc:creator>
      <pubDate>Mon, 26 Oct 2026 19:56:57 +0000</pubDate>
      <link>https://dev.to/vishall/nextjs-as-a-backend-server-actions-route-handlers-or-a-separate-nodejs-api-2fgh</link>
      <guid>https://dev.to/vishall/nextjs-as-a-backend-server-actions-route-handlers-or-a-separate-nodejs-api-2fgh</guid>
      <description>&lt;p&gt;"Do we need a separate backend?" is one of the first questions on almost every new project I take on. Usually the honest answer is: &lt;strong&gt;not yet.&lt;/strong&gt; Next.js can do most of what a small product's backend needs. But there are clear signs that a separate Node.js API is worth the extra moving parts, and it's much cheaper to spot them early.&lt;/p&gt;

&lt;p&gt;This post covers what each server-side feature in Next.js is actually for, where each one falls short, and when to add Express, NestJS or another service next to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four server tools in Next.js
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it's for&lt;/th&gt;
&lt;th&gt;Who calls it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Server Components&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reading data and rendering it to HTML&lt;/td&gt;
&lt;td&gt;Your own pages, during render&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Server Actions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mutations from your own UI: forms, buttons&lt;/td&gt;
&lt;td&gt;Your own client code, over POST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Route Handlers&lt;/strong&gt; (&lt;code&gt;route.ts&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Public HTTP endpoints that return anything: JSON, files, XML&lt;/td&gt;
&lt;td&gt;Anyone: webhooks, mobile apps, partners, other services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Proxy&lt;/strong&gt; (&lt;code&gt;proxy.ts&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Code that runs before a request: redirects, rewrites, headers&lt;/td&gt;
&lt;td&gt;Every matching request&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In Next.js 16, Middleware was renamed to &lt;strong&gt;Proxy&lt;/strong&gt;. It does the same job; the new name says more clearly what it's for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading data: go straight to the source
&lt;/h2&gt;

&lt;p&gt;In a Server Component, read from the database or the upstream service directly, ideally through one data access layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/orders/page.tsx&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;getOrdersFor&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;@/lib/data/orders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&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;getSession&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;@/lib/auth&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;default&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;Orders&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;session&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;getSession&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;orders&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;getOrdersFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;OrderList&lt;/span&gt; &lt;span class="na"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A common mistake is calling your own Route Handler from a Server Component, like &lt;code&gt;fetch("/api/orders")&lt;/code&gt;. That adds an HTTP round trip to your own server, and for pages pre-rendered at build time it fails outright, because there's no server running during the build. The Next.js docs call this out explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mutations from your own UI: Server Actions
&lt;/h2&gt;

&lt;p&gt;For forms and buttons in your app, Server Actions are the simplest option: no endpoint to name, progressive enhancement for forms, and you can revalidate the page in the same round trip.&lt;/p&gt;

&lt;p&gt;Two things to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;They're public POST endpoints.&lt;/strong&gt; Check the session, ownership and input inside every action. My &lt;a href="https://www.vishalsolanki.online/blog/secure-nextjs-server-actions" rel="noopener noreferrer"&gt;Server Actions security checklist&lt;/a&gt; covers this step by step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They're queued.&lt;/strong&gt; Actions from the same client run one after another. That's right for mutations, but it makes them a poor choice for fetching data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Public endpoints: Route Handlers
&lt;/h2&gt;

&lt;p&gt;Reach for a Route Handler when something &lt;strong&gt;other than your own UI&lt;/strong&gt; needs to talk to your app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks:&lt;/strong&gt; payment providers, CRMs and messaging tools post events to a URL you give them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other clients:&lt;/strong&gt; a mobile app, a partner integration or an internal tool that needs JSON.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OAuth and payment callbacks&lt;/strong&gt;, where a third party redirects the user back to you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-HTML responses:&lt;/strong&gt; RSS, CSV exports, generated files.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/webhooks/payments/route.ts&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;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;verifySignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signature&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;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid signature&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;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&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;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handlePaymentEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// idempotent: the provider may retry&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;204&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;Treat every Route Handler like any public API: verify signatures or tokens, validate the payload, rate-limit, and never put a mutation behind GET.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before the request: Proxy
&lt;/h2&gt;

&lt;p&gt;Proxy is good at cheap, request-level decisions: redirecting old URLs, rewriting a path, adding security headers, sending signed-out users to the login page early. It is &lt;strong&gt;not&lt;/strong&gt; a complete auth layer. The Next.js docs are explicit about this: still verify the session where the data is read or changed, because a misconfigured matcher can let a request through.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a separate Node.js API is worth it
&lt;/h2&gt;

&lt;p&gt;Next.js on its own starts to strain when one or more of these is true:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Several clients share the same business logic.&lt;/strong&gt; A web app, a mobile app and partner integrations all need the same rules. A standalone API, documented and versioned, keeps those rules in one place and lets each client move at its own pace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You have long-running or background work.&lt;/strong&gt; Queues, scheduled jobs, large imports, PDF generation, anything that runs for minutes. Serverless functions have execution time limits, and request handlers aren't a good home for a job runner anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need persistent connections.&lt;/strong&gt; WebSockets and long-lived streams for chat, live tracking or collaborative editing need a server that keeps connections open, or a managed real-time service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The work is CPU-heavy.&lt;/strong&gt; Image or video processing and heavy calculations shouldn't slow down page rendering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teams or scaling are separate.&lt;/strong&gt; A dedicated backend team, separate deploys, or very different traffic patterns between the website and the API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance requires it.&lt;/strong&gt; Some industries need sensitive processing in a specific network, region or audited service.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If none of these apply, a separate API mostly adds a second deploy, a second set of logs, CORS settings and an extra network hop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The middle path: Next.js as the backend for your frontend
&lt;/h2&gt;

&lt;p&gt;You don't have to choose all or nothing. A common, solid setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Core services&lt;/strong&gt; (Node.js with Express or NestJS, or anything else) own the business logic, the database and the background jobs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js&lt;/strong&gt; is the backend for the frontend. Server Components read from those services. Server Actions call them for mutations. Route Handlers expose only what outside clients need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The browser never talks to the core services directly.&lt;/strong&gt; Tokens and service credentials stay on the Next.js server, and the user's session lives in an &lt;code&gt;httpOnly&lt;/code&gt; cookie.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is roughly how I approach sensitive flows on real products: moving operations that used to run in the browser onto the server, behind secure cookies, so tokens and business rules are never exposed to client-side JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep one data access layer either way
&lt;/h2&gt;

&lt;p&gt;Whichever setup you choose, put data access behind one &lt;code&gt;server-only&lt;/code&gt; module and call it from Server Components, Server Actions and Route Handlers alike:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  app/
    orders/page.tsx                 ← reads via lib/data
    orders/actions.ts               ← "use server", thin, calls lib/data
    api/webhooks/payments/route.ts  ← public endpoint, calls lib/data
  lib/
    auth.ts                         ← getSession()
    data/
      orders.ts                     ← import "server-only"; queries + permission checks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you later move to a separate API, only &lt;code&gt;lib/data&lt;/code&gt; changes: it calls the API instead of the database. The pages, actions and handlers stay as they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  A decision checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;If yes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Is the only client your Next.js web app?&lt;/td&gt;
&lt;td&gt;Start with Next.js alone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Will a mobile app or partners need the same logic soon?&lt;/td&gt;
&lt;td&gt;Plan a standalone API, or at least a clean data layer you can move&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Any jobs that run longer than a normal request?&lt;/td&gt;
&lt;td&gt;Add a worker or queue service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-time features (chat, live updates)?&lt;/td&gt;
&lt;td&gt;Add a WebSocket server or a managed real-time service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heavy CPU work?&lt;/td&gt;
&lt;td&gt;Separate service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Separate backend team or very different scaling needs?&lt;/td&gt;
&lt;td&gt;Separate API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;None of the above?&lt;/td&gt;
&lt;td&gt;Next.js alone; revisit when one becomes true&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can Next.js replace Express?
&lt;/h3&gt;

&lt;p&gt;For many web apps, yes. Server Components, Server Actions and Route Handlers cover reading, writing and public endpoints. Express or NestJS still wins for long-running jobs, persistent connections, multiple clients sharing one versioned API, and teams that want the backend deployed separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I build REST endpoints for my own frontend?
&lt;/h3&gt;

&lt;p&gt;Usually not. Read data in Server Components and write with Server Actions. Build Route Handlers when a client other than your own UI needs them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a Next.js backend production-ready?
&lt;/h3&gt;

&lt;p&gt;Yes, with the same discipline as any backend: validation, authentication and authorisation on every entry point, rate limits on public ones, logging and monitoring, and keeping the framework patched.&lt;/p&gt;




&lt;p&gt;Planning a new product, or untangling an existing one? I design and build both sides, from interface to API. &lt;a href="https://www.vishalsolanki.online/#contact" rel="noopener noreferrer"&gt;Tell me what you're building&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.vishalsolanki.online/blog/nextjs-backend-server-actions-route-handlers-or-nodejs-api" rel="noopener noreferrer"&gt;vishalsolanki.online&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>node</category>
      <category>architecture</category>
      <category>fullstack</category>
    </item>
    <item>
      <title>How to get your website cited by ChatGPT, Perplexity and Google AI Mode</title>
      <dc:creator>Vishal Solanki</dc:creator>
      <pubDate>Mon, 26 Oct 2026 19:56:36 +0000</pubDate>
      <link>https://dev.to/vishall/how-to-get-your-website-cited-by-chatgpt-perplexity-and-google-ai-mode-p82</link>
      <guid>https://dev.to/vishall/how-to-get-your-website-cited-by-chatgpt-perplexity-and-google-ai-mode-p82</guid>
      <description>&lt;p&gt;More people now ask an assistant "who's a good web developer in Noida?" or "how much does a website cost?" instead of scrolling ten blue links. When the answer names three businesses, being one of them is the new page one.&lt;/p&gt;

&lt;p&gt;This is often called &lt;strong&gt;GEO&lt;/strong&gt; (generative engine optimisation) or &lt;strong&gt;AEO&lt;/strong&gt; (answer engine optimisation). Most of it is ordinary good SEO, plus a few things that now matter more than they used to.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;An honest caveat:&lt;/strong&gt; none of the AI companies publish exactly how they choose sources. What follows is what their crawler documentation says, plus what keeps showing up in 2026 industry studies. Treat precise percentages you see online, including in vendor reports, as rough signals.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where each assistant gets its answers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Assistant&lt;/th&gt;
&lt;th&gt;Where live answers come from&lt;/th&gt;
&lt;th&gt;What that means for you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google AI Overviews and AI Mode&lt;/td&gt;
&lt;td&gt;Google's own index (Googlebot)&lt;/td&gt;
&lt;td&gt;Normal Google SEO. The page must be indexed and allowed in snippets.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini app&lt;/td&gt;
&lt;td&gt;Google Search&lt;/td&gt;
&lt;td&gt;Same as above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT search&lt;/td&gt;
&lt;td&gt;OpenAI's crawler (&lt;code&gt;OAI-SearchBot&lt;/code&gt;) plus Bing results&lt;/td&gt;
&lt;td&gt;Allow &lt;code&gt;OAI-SearchBot&lt;/code&gt; and get indexed in Bing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft Copilot&lt;/td&gt;
&lt;td&gt;Bing&lt;/td&gt;
&lt;td&gt;Bing Webmaster Tools and Bing Places&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perplexity&lt;/td&gt;
&lt;td&gt;Its own crawler (&lt;code&gt;PerplexityBot&lt;/code&gt;) and other indexes&lt;/td&gt;
&lt;td&gt;Fresh, well-structured pages it can reach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude&lt;/td&gt;
&lt;td&gt;Web search plus Anthropic's crawlers (&lt;code&gt;Claude-SearchBot&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Being findable and mentioned on trusted sites&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When an assistant answers &lt;strong&gt;without&lt;/strong&gt; searching, it falls back on what it learned in training, which comes from the public web: GitHub, Reddit, Stack Overflow, news sites, popular blogs. Getting into that takes being mentioned in those places over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Make sure the crawlers can reach you
&lt;/h2&gt;

&lt;p&gt;Start with &lt;code&gt;robots.txt&lt;/code&gt;. "Allow everyone" is the simplest setting that keeps you visible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-Agent: *
Allow: /

Sitemap: https://www.example.com/sitemap.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you ever add rules, know which bot does what:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bot&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;th&gt;Used for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Googlebot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;Search, including AI Overviews and AI Mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Google-Extended&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;Gemini training and grounding outside Search. Blocking it does not remove you from AI Overviews.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bingbot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Microsoft&lt;/td&gt;
&lt;td&gt;Bing and Copilot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OAI-SearchBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;Showing sites in ChatGPT search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GPTBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;Model training&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PerplexityBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Perplexity&lt;/td&gt;
&lt;td&gt;Perplexity's search index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ClaudeBot&lt;/code&gt;, &lt;code&gt;Claude-SearchBot&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;Training, and search&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then check the layer &lt;code&gt;robots.txt&lt;/code&gt; can't see: your CDN or hosting firewall. Some bot-protection presets block AI crawlers by default. Test it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s2"&gt;"Mozilla/5.0 (compatible; OAI-SearchBot/1.0; +https://openai.com/searchbot)"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://www.example.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want &lt;code&gt;200&lt;/code&gt;. A &lt;code&gt;403&lt;/code&gt; means something in front of your site is turning the crawler away.&lt;/p&gt;

&lt;p&gt;Finally, make sure your important text is in the HTML the server sends. Most AI crawlers don't run JavaScript, so content that only appears after hydration may as well not exist for them. My &lt;a href="https://www.vishalsolanki.online/blog/nextjs-seo-checklist" rel="noopener noreferrer"&gt;Next.js SEO checklist&lt;/a&gt; has a one-line test for this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Get indexed by Bing, not just Google
&lt;/h2&gt;

&lt;p&gt;Bing is easy to forget, and it feeds Microsoft Copilot and part of ChatGPT search.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Bing Webmaster Tools and import your site from Google Search Console. It takes one click and needs no extra verification.&lt;/li&gt;
&lt;li&gt;Confirm your sitemap is listed.&lt;/li&gt;
&lt;li&gt;Turn on &lt;strong&gt;IndexNow&lt;/strong&gt; so Bing hears about new and updated pages within minutes instead of waiting for a crawl.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 3: Write pages an AI can quote
&lt;/h2&gt;

&lt;p&gt;Assistants build answers out of short, self-contained passages. Pages that get cited tend to make that easy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Answer first.&lt;/strong&gt; Put the direct answer in the first one or two sentences under each heading, then explain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use question headings&lt;/strong&gt; that match how people ask: "How much does a Next.js website cost in India?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be specific.&lt;/strong&gt; Numbers, ranges, timelines, versions and dates are quotable. Vague reassurance isn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use tables and lists&lt;/strong&gt; for comparisons and steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show who wrote it and when&lt;/strong&gt;, with an author line, a publish date and "updated on" when you revise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep it current.&lt;/strong&gt; Refresh your most important pages at least twice a year.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the difference in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hard to quote:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Pricing depends on many factors, so every project is different. Get in touch for a quote tailored to your needs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Easy to quote:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A 5–8 page business website in Next.js typically takes 3–5 weeks. Price depends mainly on design work, CMS needs and integrations; tell me those three and I can give you a range on the first call.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second version answers a real question in a way an assistant can repeat and attribute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Be one clear identity
&lt;/h2&gt;

&lt;p&gt;An assistant has to be confident that "Your Name, web developer, Your City" is one specific person or business with a specific speciality. Help it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the &lt;strong&gt;same one-line description&lt;/strong&gt; on your site, LinkedIn, GitHub, Google Business Profile and directory listings.&lt;/li&gt;
&lt;li&gt;Add &lt;strong&gt;&lt;code&gt;sameAs&lt;/code&gt; links&lt;/strong&gt; to your &lt;code&gt;Person&lt;/code&gt; or &lt;code&gt;Organization&lt;/code&gt; structured data, pointing at those profiles.&lt;/li&gt;
&lt;li&gt;Have an &lt;strong&gt;About page&lt;/strong&gt; that states the facts plainly: what you do, where, for how long, for whom.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick two or three specialities and repeat them.&lt;/strong&gt; "The Next.js performance person" is easier for an assistant to recommend than "does everything".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5: Get mentioned where AI looks
&lt;/h2&gt;

&lt;p&gt;This is the step most people skip, and it's the one that moves the needle most. Industry studies in 2026 keep finding that &lt;strong&gt;mentions of a brand on other trusted sites&lt;/strong&gt; line up with AI citations more closely than backlinks alone, and that many cited pages aren't in Google's top ten at all.&lt;/p&gt;

&lt;p&gt;Places worth your time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Best of" lists and directories:&lt;/strong&gt; Clutch, GoodFirms and similar sites rank for "best developers in [city]" and get quoted by assistants. Get listed, then get reviews.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reddit and Quora:&lt;/strong&gt; genuinely helpful answers in the communities your clients and peers use. Mention your work only when it actually answers the question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; useful public repositories with your name and site in the README.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn:&lt;/strong&gt; posts and articles are public and indexed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stack Overflow:&lt;/strong&gt; good answers in your speciality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Press:&lt;/strong&gt; journalist-request platforms such as Qwoted and Featured.com. One quote in a real publication outweighs dozens of directory links.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;YouTube:&lt;/strong&gt; short walkthroughs. Transcripts are indexed and quoted.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 6: Measure it
&lt;/h2&gt;

&lt;p&gt;There's no single "AI rank" to check, so measure it three ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A monthly prompt check.&lt;/strong&gt; On the first of each month, ask the same questions in ChatGPT (with search on), Perplexity, Gemini, Copilot and Claude, and log the answers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prompt&lt;/th&gt;
&lt;th&gt;Mentioned?&lt;/th&gt;
&lt;th&gt;Linked?&lt;/th&gt;
&lt;th&gt;Who's named instead?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Who is [your name], the [role] from [city]?&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recommend a freelance [speciality] developer in [city]&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How much does a [service] cost in [country]?&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last column is the useful one. It shows you which sites and lists the assistants trust, so you know where to get yourself listed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Referral traffic.&lt;/strong&gt; In Google Analytics, look for sessions from &lt;code&gt;chatgpt.com&lt;/code&gt;, &lt;code&gt;perplexity.ai&lt;/code&gt;, &lt;code&gt;gemini.google.com&lt;/code&gt;, &lt;code&gt;copilot.microsoft.com&lt;/code&gt; and &lt;code&gt;claude.ai&lt;/code&gt;. ChatGPT adds &lt;code&gt;utm_source=chatgpt.com&lt;/code&gt; to many of its links. Group them into one "AI assistants" channel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ask your leads.&lt;/strong&gt; Add "How did you find me?" to your contact form with an "AI assistant" option. Analytics can't see someone who read your name in a chat and typed your URL later; the form can.&lt;/p&gt;

&lt;p&gt;Google has also started rolling out an AI Overviews and AI Mode report in Search Console. It's in beta, shows impressions only and isn't available for every site yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about llms.txt?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;llms.txt&lt;/code&gt; is a proposed Markdown file at the root of your site that summarises it for language models. No major search engine has confirmed using it, and Google has said it doesn't. It takes ten minutes to write, so add one if you like, but don't expect it to change anything on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What doesn't work
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Blocks of "AI-friendly" keywords stuffed into a page.&lt;/li&gt;
&lt;li&gt;Hundreds of thin, auto-generated pages. Google calls this scaled content abuse.&lt;/li&gt;
&lt;li&gt;Text hidden from visitors but aimed at bots.&lt;/li&gt;
&lt;li&gt;Fake reviews and fake listings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these risk your normal search rankings too, and assistants increasingly lean on the same quality signals.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  If I block GPTBot, do I disappear from ChatGPT search?
&lt;/h3&gt;

&lt;p&gt;No. OpenAI uses separate crawlers: &lt;code&gt;GPTBot&lt;/code&gt; collects training data, and &lt;code&gt;OAI-SearchBot&lt;/code&gt; is what surfaces sites in ChatGPT search. You can block one and allow the other.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does blocking Google-Extended remove me from AI Overviews?
&lt;/h3&gt;

&lt;p&gt;No. AI Overviews and AI Mode are part of Google Search and use Googlebot. &lt;code&gt;Google-Extended&lt;/code&gt; only controls use of your content for Gemini outside Search.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does this take?
&lt;/h3&gt;

&lt;p&gt;Being named for your own name can happen within weeks once you're indexed and your profiles agree with each other. Being recommended for competitive local or niche questions usually takes months, because it depends on mentions building up on other sites.&lt;/p&gt;




&lt;p&gt;I build sites that are fast, findable in search and easy for AI assistants to quote. &lt;a href="https://www.vishalsolanki.online/#contact" rel="noopener noreferrer"&gt;Tell me about yours&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.vishalsolanki.online/blog/get-cited-by-chatgpt-perplexity-ai-search" rel="noopener noreferrer"&gt;vishalsolanki.online&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aisearch</category>
      <category>geo</category>
      <category>seo</category>
      <category>chatgpt</category>
    </item>
    <item>
      <title>PHP2Shell (CVE-2026-109618 and CVE-2026-109619): is your website affected, and how to fix it</title>
      <dc:creator>Vishal Solanki</dc:creator>
      <pubDate>Mon, 26 Oct 2026 19:56:19 +0000</pubDate>
      <link>https://dev.to/vishall/php2shell-cve-2026-109618-and-cve-2026-109619-is-your-website-affected-and-how-to-fix-it-3cdp</link>
      <guid>https://dev.to/vishall/php2shell-cve-2026-109618-and-cve-2026-109619-is-your-website-affected-and-how-to-fix-it-3cdp</guid>
      <description>&lt;p&gt;On 21 October 2026, the PHP project released emergency updates for two critical vulnerabilities, &lt;strong&gt;CVE-2026-109618&lt;/strong&gt; and &lt;strong&gt;CVE-2026-109619&lt;/strong&gt;, now known as &lt;strong&gt;PHP2Shell&lt;/strong&gt;. They let an attacker run commands on a web server with a single crafted request, &lt;strong&gt;without an account and without touching your site's own code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your website, shop or admin panel runs on PHP, and that includes most WordPress, WooCommerce, Laravel, Drupal and Magento sites, this is worth ten minutes of your time today.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Short version:&lt;/strong&gt; update the PHP runtime that serves your site to &lt;strong&gt;8.2.35, 8.3.36, 8.4.27 or 8.5.12&lt;/strong&gt; (or your vendor's patched package), then restart PHP. Updating WordPress or your plugins does &lt;strong&gt;not&lt;/strong&gt; fix this. The widely shared &lt;code&gt;session.upload_progress.enabled=0&lt;/code&gt; setting is &lt;strong&gt;not&lt;/strong&gt; a complete fix.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Last checked: 24 October 2026. This is a fast-moving story; follow PHP's official advisories for changes.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What PHP2Shell is, in plain words
&lt;/h2&gt;

&lt;p&gt;Both flaws are in the PHP engine itself, not in WordPress, Laravel or any plugin. That's what makes them unusual: &lt;strong&gt;a site with perfectly secure code is still exposed&lt;/strong&gt;, because the attack happens while PHP is still reading the incoming request, before your application runs at all. Security researchers have shown that even a one-line "Hello World" page on a vulnerable PHP version can be attacked.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2026-109618:&lt;/strong&gt; when PHP runs out of memory partway through reading a form submission, it leaves half-built data behind, and its cleanup code later treats that data as valid. An attacker who shapes that leftover data can make the cleanup run a command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2026-109619:&lt;/strong&gt; a PHP worker can carry stale data from one request into the next. Researchers confirmed command execution through this route as well, including via the upload-progress feature, which is on by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are rated &lt;strong&gt;9.2 out of 10&lt;/strong&gt; (CVSS v4.0). Researchers also showed that an attacker can read memory from a PHP worker, which can include other visitors' session tokens, passwords and API keys, even without running a command.&lt;/p&gt;

&lt;p&gt;They were found by Wiz Research, Calif and OpenAI's Codex Security team. Details leaked a day before the planned disclosure, so PHP released the fixes early.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it being exploited?
&lt;/h2&gt;

&lt;p&gt;Exploitation attempts started before the patches shipped. AWS reported attempts as early as 17 October, Cloudflare saw attack traffic and attackers changing their payloads to get past its firewall rules, and watchTowr reported active exploitation attempts against both flaws on 21 October. At the time of writing, public reports hadn't confirmed a specific successful break-in, but several organisations took services offline as a precaution.&lt;/p&gt;

&lt;p&gt;Treat this as urgent, not theoretical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which sites are affected?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Any server where PHP answers web requests&lt;/strong&gt;, in any of these setups:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;How PHP runs&lt;/th&gt;
&lt;th&gt;Affected?&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Apache with &lt;code&gt;mod_php&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes, highest risk&lt;/td&gt;
&lt;td&gt;A single request can trigger the attack before any of your code runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHP-FPM (Nginx, Apache, most VPS setups)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Confirmed command execution through the second flaw&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;php-cgi&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FrankenPHP&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LiteSpeed / LSAPI (common on shared hosting)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHP only used on the command line, never serving web requests&lt;/td&gt;
&lt;td&gt;Lower risk&lt;/td&gt;
&lt;td&gt;Still update, but the web attack path doesn't apply&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Affected versions go back to PHP 5.4. These are the first fixed releases:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PHP branch&lt;/th&gt;
&lt;th&gt;Vulnerable&lt;/th&gt;
&lt;th&gt;First fixed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8.5&lt;/td&gt;
&lt;td&gt;below 8.5.12&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.5.12&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8.4&lt;/td&gt;
&lt;td&gt;below 8.4.27&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.4.27&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8.3&lt;/td&gt;
&lt;td&gt;below 8.3.36&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.3.36&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8.2&lt;/td&gt;
&lt;td&gt;below 8.2.35&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.2.35&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8.1, 8.0, 7.4&lt;/td&gt;
&lt;td&gt;all&lt;/td&gt;
&lt;td&gt;No official release. PHP published source patches; check your host or distribution, or upgrade to 8.2+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;PHP 8.2 itself reaches end of life on 31 December 2026, so if you're upgrading anyway, go to 8.3 or newer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not affected:&lt;/strong&gt; sites with no PHP at all, such as a static site, or a Next.js or Node.js app. But if your Next.js front end talks to a PHP backend (for example headless WordPress), that backend server is affected.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check and fix it, by type of hosting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Shared hosting (cPanel, Hostinger, GoDaddy, Bluehost and similar)
&lt;/h3&gt;

&lt;p&gt;You usually can't patch PHP yourself here. Your host does it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find your PHP version. In WordPress, go to &lt;strong&gt;Tools → Site Health → Info → Server&lt;/strong&gt;. In cPanel, look for &lt;strong&gt;Select PHP Version&lt;/strong&gt; or &lt;strong&gt;MultiPHP Manager&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask your host directly&lt;/strong&gt; whether their PHP builds include the fixes for &lt;strong&gt;CVE-2026-109618 and CVE-2026-109619&lt;/strong&gt;. Hosts often patch without changing the version number you see, so the version alone doesn't tell you.&lt;/li&gt;
&lt;li&gt;If you're on PHP 7.4, 8.0 or 8.1, ask the host to move you to 8.3 or newer, and test your site afterwards. Older plugins sometimes need updating first.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A short message you can send:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Hi, my site [domain] runs on your PHP. Can you confirm the PHP version serving it includes the patches for CVE-2026-109618 and CVE-2026-109619 (PHP2Shell)? If not, when will it be patched?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  WordPress, WooCommerce, Laravel and other PHP apps
&lt;/h3&gt;

&lt;p&gt;Keep your app and plugins updated as always, but understand that &lt;strong&gt;this isn't an application bug&lt;/strong&gt;. Updating WordPress, a plugin or Laravel does not update the PHP engine underneath. That comes from your host, your server's package manager or your container image.&lt;/p&gt;

&lt;h3&gt;
  
  
  VPS or dedicated server (Ubuntu, Debian, RHEL)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Check what actually serves your site. &lt;code&gt;php -v&lt;/code&gt; on the command line can be a different build from the one PHP-FPM or Apache uses, so check the web runtime too (your control panel, or a temporary &lt;code&gt;phpinfo()&lt;/code&gt; page that you &lt;strong&gt;delete straight after&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Update through your package manager and restart &lt;strong&gt;every&lt;/strong&gt; PHP process:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Debian / Ubuntu (package names vary, e.g. php8.3-fpm)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart php8.3-fpm   &lt;span class="c"&gt;# PHP-FPM&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart apache2      &lt;span class="c"&gt;# if you use mod_php&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Distribution packages often carry the fix under an older-looking version number. Confirm the fix is in the package's changelog rather than trusting the version string:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt changelog php8.3-fpm | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-m1&lt;/span&gt; CVE-2026-109618
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Docker and Kubernetes
&lt;/h3&gt;

&lt;p&gt;Rebuild your images on a patched PHP base image, redeploy, and make sure &lt;strong&gt;no old containers are still running&lt;/strong&gt;. Also check images you didn't build yourself: some third-party apps bundle their own PHP.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you can't patch today
&lt;/h2&gt;

&lt;p&gt;PHP is clear that &lt;strong&gt;only updating fully fixes this.&lt;/strong&gt; These steps reduce exposure while you arrange the update:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Put a firewall in front of PHP.&lt;/strong&gt; Cloudflare switched on protective rules for its customers before disclosure, but it also saw attackers changing payloads to get around them, so treat this as a stopgap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limit request sizes&lt;/strong&gt; at your web server or proxy, and keep &lt;code&gt;post_max_size&lt;/code&gt; low.&lt;/li&gt;
&lt;li&gt;PHP's temporary guidance for CVE-2026-109618 is to enforce &lt;code&gt;memory_limit=-1&lt;/code&gt; at the administrator level, together with request-size limits in front of PHP and a memory limit on the container.&lt;/li&gt;
&lt;li&gt;For CVE-2026-109619: &lt;code&gt;session.upload_progress.enabled=0&lt;/code&gt;, plus &lt;code&gt;fatal_error_backtraces=0&lt;/code&gt; on PHP 8.5, then restart every worker. This blocks the confirmed paths but &lt;strong&gt;not&lt;/strong&gt; every variant, and does nothing for CVE-2026-109618.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restrict access&lt;/strong&gt; to anything that doesn't need to be public, such as admin panels, staging sites and internal tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Signs you might have been attacked
&lt;/h2&gt;

&lt;p&gt;If your server ran an unpatched PHP version while exposed to the internet, look back through your logs for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unusually large form or multipart POST requests, followed by memory-limit errors or bursts of HTTP 500 responses.&lt;/li&gt;
&lt;li&gt;Oddly large &lt;code&gt;PHPSESSID&lt;/code&gt; cookies in responses.&lt;/li&gt;
&lt;li&gt;PHP workers restarting without explanation.&lt;/li&gt;
&lt;li&gt;Shell commands or processes started by your web server user (for example &lt;code&gt;www-data&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you see any of these, assume secrets in memory may have leaked: &lt;strong&gt;rotate database passwords, API keys and admin passwords&lt;/strong&gt;, invalidate active sessions, and check for new admin users or modified files.&lt;/p&gt;

&lt;h2&gt;
  
  
  After you patch
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The fix adds a new hard memory limit. Hitting it ends the process, so review &lt;code&gt;memory_limit&lt;/code&gt; and &lt;code&gt;post_max_size&lt;/code&gt; against what your site actually needs, and test uploads and large forms.&lt;/li&gt;
&lt;li&gt;PHP has since published an advisory for a related issue, &lt;strong&gt;CVE-2026-115560&lt;/strong&gt;, in the session upload progress feature, which affects even the patched releases. It's due to be fixed in PHP's next regular release. Watch PHP's advisories and update again when that's out.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Running Next.js? Different bug, same month
&lt;/h2&gt;

&lt;p&gt;If your site is built with Next.js, PHP2Shell doesn't apply, but there's a separate security release to install. On 14 October 2026, Next.js fixed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2026-96889&lt;/strong&gt;, a critical remote code execution flaw in the Image Optimization API when SVG files are processed, through the &lt;code&gt;sharp&lt;/code&gt; image library on Linux.&lt;/li&gt;
&lt;li&gt;A second critical remote code execution flaw in &lt;code&gt;use cache&lt;/code&gt; functions that return typed arrays (Next.js 16).&lt;/li&gt;
&lt;li&gt;A high-severity denial-of-service issue affecting App Router apps that use Server Actions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Upgrade to &lt;strong&gt;Next.js 16.4.1 or 15.5.28&lt;/strong&gt; or later, and make sure your lockfile resolves &lt;code&gt;sharp&lt;/code&gt; to &lt;strong&gt;0.35.5 or later&lt;/strong&gt;. Running &lt;code&gt;npm audit&lt;/code&gt; will flag both. More on hardening Server Actions in my &lt;a href="https://www.vishalsolanki.online/blog/secure-nextjs-server-actions" rel="noopener noreferrer"&gt;security checklist&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  I updated WordPress and all my plugins. Am I safe?
&lt;/h3&gt;

&lt;p&gt;Not from this. PHP2Shell is in the PHP engine, which your host or server provides. Check the PHP version and ask your host to confirm the patch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a WordPress security plugin protect me?
&lt;/h3&gt;

&lt;p&gt;A plugin runs inside your application, after PHP has already read the request. This attack happens before that point, so an in-app plugin can't block it. A firewall in front of the server can reduce the risk; only patching removes it.&lt;/p&gt;

&lt;h3&gt;
  
  
  My site is small. Would anyone really target it?
&lt;/h3&gt;

&lt;p&gt;Attacks like this are automated: bots scan the whole internet for vulnerable servers, regardless of size. Security researchers estimate tens of millions of internet-facing PHP servers may be exposed.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I know the patch worked?
&lt;/h3&gt;

&lt;p&gt;Confirm the version serving web requests (not just the command line) is a fixed release, or that your distribution's package changelog mentions both CVEs, and that every PHP process has been restarted since the update.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://securitylabs.datadoghq.com/articles/php2shell-critical-vulnerabilities-in-php/" rel="noopener noreferrer"&gt;Datadog Security Labs: PHP2Shell detection, analysis and remediation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thehackernews.com/2026/10/php-issues-emergency-patches-for-two.html" rel="noopener noreferrer"&gt;The Hacker News: PHP issues emergency patches for PHP2Shell&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://unit42.paloaltonetworks.com/cve-2026-109618-cve-2026-109619/" rel="noopener noreferrer"&gt;Unit 42 threat brief: CVE-2026-109618 and CVE-2026-109619&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.cloudflare.com/php-vulnerabilities-waf-protection/" rel="noopener noreferrer"&gt;Cloudflare: protections against the php2shell vulnerability&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.netlify.com/changelog/2026-10-14-nextjs-react-security-vulnerabilities/" rel="noopener noreferrer"&gt;Netlify: Next.js and React security release, October 2026&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Not sure what your site runs on, or whether your host has patched it? I can check your setup, apply the fix or move you to a supported version. &lt;a href="https://www.vishalsolanki.online/#contact" rel="noopener noreferrer"&gt;Get in touch&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.vishalsolanki.online/blog/php2shell-cve-2026-109618-fix" rel="noopener noreferrer"&gt;vishalsolanki.online&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>php</category>
      <category>php2shell</category>
      <category>wordpress</category>
    </item>
    <item>
      <title>Securing Next.js Server Actions: a 10-point checklist for 2026</title>
      <dc:creator>Vishal Solanki</dc:creator>
      <pubDate>Mon, 26 Oct 2026 19:56:01 +0000</pubDate>
      <link>https://dev.to/vishall/securing-nextjs-server-actions-a-10-point-checklist-for-2026-5f77</link>
      <guid>https://dev.to/vishall/securing-nextjs-server-actions-a-10-point-checklist-for-2026-5f77</guid>
      <description>&lt;p&gt;Server Actions are the nicest way to write mutations in Next.js. You write an async function, mark it &lt;code&gt;"use server"&lt;/code&gt;, pass it to a form, and you're done: no API route, no fetch call, no loading-state plumbing.&lt;/p&gt;

&lt;p&gt;That convenience hides one fact you need to keep in mind every time: &lt;strong&gt;each exported Server Action is a public HTTP endpoint.&lt;/strong&gt; Anyone can call it with a POST request, from anywhere, with any arguments, whether or not your UI shows the button.&lt;/p&gt;

&lt;p&gt;On real products I've moved sensitive operations out of the browser and into server-side code built on Server Actions and secure cookies. This is the checklist I use for every action since.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Next.js already does for you
&lt;/h2&gt;

&lt;p&gt;It's worth knowing exactly where the built-in protection stops.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Built in&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Encrypted, non-deterministic action IDs&lt;/td&gt;
&lt;td&gt;The client calls an action by an ID, not by its name. IDs change between builds.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dead-code elimination&lt;/td&gt;
&lt;td&gt;Actions your app never uses are removed at build time and don't become endpoints.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST only&lt;/td&gt;
&lt;td&gt;Actions can't be triggered by a link, an image tag or a prefetch.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Origin check&lt;/td&gt;
&lt;td&gt;When the browser sends an &lt;code&gt;Origin&lt;/code&gt; header, it must match the host. That blocks most cross-site request forgery.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Body size limit&lt;/td&gt;
&lt;td&gt;Request bodies are capped at 1 MB by default.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What it does &lt;strong&gt;not&lt;/strong&gt; do: check who is calling, check whether they're allowed to touch this record, validate the arguments, or limit how often they can call. Those are your job, inside every action.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Authenticate inside the action, every time
&lt;/h2&gt;

&lt;p&gt;A login check on the page does not protect the actions defined on that page. The page check runs when the page renders. The action runs later, on its own request, and can be called without ever loading the page.&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&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;getSession&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;@/lib/auth&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;updateProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&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;session&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;getSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Not signed in&lt;/span&gt;&lt;span class="dl"&gt;"&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;h2&gt;
  
  
  2. Authorise the specific record
&lt;/h2&gt;

&lt;p&gt;Being signed in only proves who the caller is. It doesn't prove they own the thing they're changing. If an action takes an ID, check ownership, or someone will change the ID and edit another user's data. This bug has a name, insecure direct object reference (IDOR), and it's one of the most common ones I find.&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deleteAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;addressId&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;session&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;getSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Not signed in&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;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;addressId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Not found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;addressId&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;p&gt;Return the same "Not found" whether the record doesn't exist or belongs to someone else, so the action can't be used to probe which IDs exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Validate every argument
&lt;/h2&gt;

&lt;p&gt;TypeScript types disappear at runtime. A caller can send any shape, any length and any type. Validate on the server with a schema:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&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;zod&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;Profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&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="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&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;updateProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&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;session&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;getSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Not signed in&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;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&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;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please check the highlighted fields.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;You don't need a library for small forms. A plain function that trims, checks lengths and checks values against a fixed list works just as well, as long as it runs on the server and has a small test.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Keep the database and secrets behind a server-only layer
&lt;/h2&gt;

&lt;p&gt;Put database access, API keys and permission checks in one data access layer, and mark it so it can never end up in a client bundle:&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;// lib/data/addresses.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;server-only&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;deleteAddressFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&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="nx"&gt;addressId&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="c1"&gt;// ownership check + delete, as in step 2&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then actions stay thin: read the session, validate the input, call the data layer. Auditing one folder is much easier than auditing every &lt;code&gt;"use server"&lt;/code&gt; function in the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Return only what the client needs
&lt;/h2&gt;

&lt;p&gt;Whatever an action returns is sent to the browser. Don't return the whole database row with password hashes, internal flags or other users' data. Pick the fields the UI needs.&lt;/p&gt;

&lt;p&gt;The same goes for errors. Log the real error on the server, and send the user a short, generic message. A stack trace or a raw SQL error tells an attacker far more than it tells your user.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Keep sessions in httpOnly, secure cookies
&lt;/h2&gt;

&lt;p&gt;Tokens in &lt;code&gt;localStorage&lt;/code&gt; can be read by any script that runs on your page, including a compromised third-party one. An &lt;code&gt;httpOnly&lt;/code&gt; cookie can't. Set it from a Server Action or Route Handler, where Next.js can send the &lt;code&gt;Set-Cookie&lt;/code&gt; header:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cookies&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/headers&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;signIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...verify credentials, create a session...&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;cookies&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;httpOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// not readable from JavaScript&lt;/span&gt;
    &lt;span class="na"&gt;secure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// HTTPS only&lt;/span&gt;
    &lt;span class="na"&gt;sameSite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lax&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// not sent on most cross-site requests&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// one week&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;Store a random session ID in the cookie and keep the session data on the server, or sign or encrypt the cookie's contents. Never put anything in a cookie you'd be unhappy for the user to edit.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Rate-limit, and stop the obvious bots
&lt;/h2&gt;

&lt;p&gt;Sign-in, sign-up, password reset, OTP and contact forms get hammered. Limit how often one client can call them:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&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/headers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&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;rateLimit&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;@/lib/rate-limit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// backed by Redis or your host's KV store&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;requestOtp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&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;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-forwarded-for&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&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;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`otp:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ip&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="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;windowSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Too many attempts. Try again in a few minutes.&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;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;The limit has to live in a shared store, not in a variable in memory, because serverless functions don't share memory between instances. Turn on your host's firewall or bot protection as well.&lt;/p&gt;

&lt;p&gt;For public forms, a hidden honeypot field catches most simple bots cheaply: real people never fill it in, so any submission that does can be quietly dropped. The contact form on this site works that way.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Understand the CSRF protection, and don't break it
&lt;/h2&gt;

&lt;p&gt;Next.js compares the request's &lt;code&gt;Origin&lt;/code&gt; header with the host the request arrived at, and rejects actions that don't match. That covers most cross-site request forgery, together with &lt;code&gt;SameSite&lt;/code&gt; cookies.&lt;/p&gt;

&lt;p&gt;Two ways to break it by accident:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Behind a reverse proxy&lt;/strong&gt; that forwards its own host instead of the public one, valid requests start failing. Add the public host to &lt;code&gt;experimental.serverActions.allowedOrigins&lt;/code&gt; in &lt;code&gt;next.config.ts&lt;/code&gt;, rather than disabling anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutations in GET Route Handlers.&lt;/strong&gt; A GET request can be triggered by a link or an image tag from any site. Anything that changes data should be a Server Action or a POST, PUT or DELETE handler.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. Mind the size of what's sent
&lt;/h2&gt;

&lt;p&gt;The 1 MB default body limit is there to stop huge payloads from tying up your server. If you need file uploads, don't just raise the limit: upload straight from the browser to object storage with a short-lived signed URL, then send the resulting file key to a Server Action.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Keep Next.js patched
&lt;/h2&gt;

&lt;p&gt;Framework bugs are part of your attack surface too. On 14 October 2026, Next.js fixed two critical remote code execution flaws (one in image optimisation of SVG files, CVE-2026-96889, and one in &lt;code&gt;use cache&lt;/code&gt; functions returning typed arrays) and a high-severity denial-of-service issue in App Router apps that use Server Actions. The fixes are in 16.4.1 and 15.5.28. Make sure &lt;code&gt;sharp&lt;/code&gt; resolves to 0.35.5 or later as well.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;npm audit&lt;/code&gt; regularly, read the advisories it reports, and update to the patched release. If you also run PHP anywhere, see &lt;a href="https://www.vishalsolanki.online/blog/php2shell-cve-2026-109618-fix" rel="noopener noreferrer"&gt;PHP2Shell: is your website affected&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The audit checklist
&lt;/h2&gt;

&lt;p&gt;Run through this for every file that contains &lt;code&gt;"use server"&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Fix if not&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Does the action check the session itself?&lt;/td&gt;
&lt;td&gt;Call your auth helper first thing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Does it check the caller owns the record?&lt;/td&gt;
&lt;td&gt;Compare the owner ID before reading or writing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Are all arguments validated on the server?&lt;/td&gt;
&lt;td&gt;Schema or validation function&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Is database and secret access in a &lt;code&gt;server-only&lt;/code&gt; layer?&lt;/td&gt;
&lt;td&gt;Move it, keep the action thin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Does it return only what the UI needs?&lt;/td&gt;
&lt;td&gt;Pick fields, generic errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Are sessions in &lt;code&gt;httpOnly&lt;/code&gt;, &lt;code&gt;secure&lt;/code&gt; cookies?&lt;/td&gt;
&lt;td&gt;Move tokens out of &lt;code&gt;localStorage&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Are public or sensitive actions rate-limited?&lt;/td&gt;
&lt;td&gt;Shared-store limiter, honeypot, host firewall&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Does anything mutate data on GET?&lt;/td&gt;
&lt;td&gt;Move it to a Server Action or a POST handler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Are uploads going through the action body?&lt;/td&gt;
&lt;td&gt;Signed direct-to-storage uploads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Does &lt;code&gt;npm audit&lt;/code&gt; report Next.js advisories?&lt;/td&gt;
&lt;td&gt;Update to the patched version&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Are Server Actions secure?
&lt;/h3&gt;

&lt;p&gt;They're as secure as the code inside them. The transport is solid: POST only, an origin check and unguessable IDs. But authentication, authorisation, validation and rate limiting are always your responsibility, just as they would be in an Express route.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I use Server Actions or Route Handlers for mutations?
&lt;/h3&gt;

&lt;p&gt;Use Server Actions for mutations that come from your own UI. Use Route Handlers when something outside your app needs to call you, like a webhook, a mobile app or a partner integration. I compare the options in detail in &lt;a href="https://www.vishalsolanki.online/blog/nextjs-backend-server-actions-route-handlers-or-nodejs-api" rel="noopener noreferrer"&gt;Next.js as a backend: Server Actions, Route Handlers or a separate Node.js API&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need my own CSRF tokens?
&lt;/h3&gt;

&lt;p&gt;Usually not. The built-in origin check plus &lt;code&gt;SameSite&lt;/code&gt; cookies covers the common cases. You'd add tokens if you have to support very old browsers, or you've deliberately relaxed cookie or origin settings.&lt;/p&gt;




&lt;p&gt;Want a security review of your Next.js app's actions and API routes? &lt;a href="https://www.vishalsolanki.online/#contact" rel="noopener noreferrer"&gt;Get in touch&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.vishalsolanki.online/blog/secure-nextjs-server-actions" rel="noopener noreferrer"&gt;vishalsolanki.online&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>security</category>
      <category>serveractions</category>
      <category>fullstack</category>
    </item>
    <item>
      <title>Next.js SEO checklist for 2026: 12 fixes for the App Router</title>
      <dc:creator>Vishal Solanki</dc:creator>
      <pubDate>Mon, 26 Oct 2026 19:55:34 +0000</pubDate>
      <link>https://dev.to/vishall/nextjs-seo-checklist-for-2026-12-fixes-for-the-app-router-akk</link>
      <guid>https://dev.to/vishall/nextjs-seo-checklist-for-2026-12-fixes-for-the-app-router-akk</guid>
      <description>&lt;p&gt;Most Next.js sites that don't rank aren't missing a secret trick. They're missing basics the framework already gives you: a unique title per page, one canonical URL, a sitemap with honest dates, and content that's actually in the HTML.&lt;/p&gt;

&lt;p&gt;This is the checklist I run on every App Router site, including this one. Each item comes with code you can copy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Short version:&lt;/strong&gt; set &lt;code&gt;metadataBase&lt;/code&gt;, give every page its own title and description, pick one canonical host, generate &lt;code&gt;sitemap.ts&lt;/code&gt; and &lt;code&gt;robots.ts&lt;/code&gt;, add JSON-LD, keep one real H1 with real text, make sure your content is server-rendered, and set up Search Console and Bing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Set &lt;code&gt;metadataBase&lt;/code&gt; and a title template
&lt;/h2&gt;

&lt;p&gt;In the root layout, tell Next.js your production URL once. Every relative URL in your metadata (canonicals, Open Graph images) is resolved against it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/layout.tsx&lt;/span&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="kd"&gt;const&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;Metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;metadataBase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.example.com&lt;/span&gt;&lt;span class="dl"&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Example — What you do, where you do it&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;%s — Example&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&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;One or two sentences a searcher would actually click on.&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;With the template, a page that sets &lt;code&gt;title: "Pricing"&lt;/code&gt; shows up in search as "Pricing — Example", so your brand is on every result without repeating it by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Give every page its own title and description
&lt;/h2&gt;

&lt;p&gt;Duplicate titles are the most common problem I find in audits. For dynamic routes, use &lt;code&gt;generateMetadata&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/blog/[slug]/page.tsx&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&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;getPost&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;await&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="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="nx"&gt;post&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="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&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;`/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;post&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep titles around 50–60 characters with the main keyword near the start. Descriptions don't change rankings directly, but they decide whether people click, so write them like a small ad: who it's for, what they get, one proof point.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Pick one canonical host and redirect the rest
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;example.com&lt;/code&gt;, &lt;code&gt;www.example.com&lt;/code&gt;, your &lt;code&gt;*.vercel.app&lt;/code&gt; address and &lt;code&gt;http://&lt;/code&gt; are four copies of the same site as far as Google is concerned. Pick one, use it as &lt;code&gt;metadataBase&lt;/code&gt;, add a canonical to every page (as above) and permanently redirect the others.&lt;/p&gt;

&lt;p&gt;On Vercel, set the primary domain in the dashboard. For an old deployment address, a host-based redirect in &lt;code&gt;next.config.ts&lt;/code&gt; does the job. This one sends an old Vercel URL, and everything it ranked for, to the real domain:&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;// next.config.ts&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;redirects&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="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/:path*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;has&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&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;host&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;old-project.vercel.app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
      &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.example.com/:path*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;permanent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;
  
  
  4. Generate the sitemap, with honest dates
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/sitemap.ts&lt;/span&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;MetadataRoute&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;default&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;sitemap&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;MetadataRoute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Sitemap&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;posts&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;getPosts&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lastModified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-10-24&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;span class="nx"&gt;posts&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;p&lt;/span&gt;&lt;span class="p"&gt;)&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;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://www.example.com/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;p&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="na"&gt;lastModified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&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;p&gt;Two details matter. Google ignores &lt;code&gt;priority&lt;/code&gt; and &lt;code&gt;changeFrequency&lt;/code&gt;, so don't spend time tuning them. It does use &lt;code&gt;lastModified&lt;/code&gt;, but only while it stays accurate. If every URL shows the build date on every deploy, Google learns to ignore it. Change the date when the content changes, and only then.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Add &lt;code&gt;robots.ts&lt;/code&gt;, and don't block what you want
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/robots.ts&lt;/span&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;MetadataRoute&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;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;robots&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;MetadataRoute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Robots&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;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;sitemap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.example.com/sitemap.xml&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to show up in ChatGPT search and Perplexity answers, keep their crawlers (&lt;code&gt;OAI-SearchBot&lt;/code&gt;, &lt;code&gt;PerplexityBot&lt;/code&gt;) allowed. Also check your hosting firewall or CDN: some bot-protection settings block AI crawlers even when &lt;code&gt;robots.txt&lt;/code&gt; allows them. I go deeper on this in &lt;a href="https://www.vishalsolanki.online/blog/get-cited-by-chatgpt-perplexity-ai-search" rel="noopener noreferrer"&gt;how to get cited by ChatGPT, Perplexity and Google AI Mode&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Describe the page with JSON-LD
&lt;/h2&gt;

&lt;p&gt;Structured data tells search engines what a page is: a person, a business, an article, a product. Render it as a script tag from a server component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&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="s2"&gt;@context&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="s2"&gt;https://schema.org&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="s2"&gt;@type&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="s2"&gt;BlogPosting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&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="na"&gt;datePublished&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&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="s2"&gt;Person&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Your Name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.example.com&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;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;
    &lt;span class="na"&gt;dangerouslySetInnerHTML&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;__html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&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;/&amp;lt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;u003c&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;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;/&amp;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 &lt;code&gt;.replace&lt;/code&gt; escapes &lt;code&gt;&amp;lt;&lt;/code&gt; so no string inside your data can close the script tag early. The Next.js docs recommend it. Check the result with Google's Rich Results Test.&lt;/p&gt;

&lt;p&gt;For a personal or freelance site, a &lt;code&gt;Person&lt;/code&gt; and a &lt;code&gt;ProfessionalService&lt;/code&gt; on the homepage plus a &lt;code&gt;BlogPosting&lt;/code&gt; on each post covers most of what matters. Only mark up what's visible on the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. One real H1, with real text
&lt;/h2&gt;

&lt;p&gt;Search engines read the text inside your headings, not what they look like. Until recently, Google read my own homepage H1 as "I BUILD 01 IDEA THINGS 02 INTERACTION FOR THE WEB. 03 PRODUCT", because an animated word swap lived inside the heading. Visitors never saw that jumble. Crawlers saw nothing else.&lt;/p&gt;

&lt;p&gt;The fix was to draw the decorative words with CSS (&lt;code&gt;::before&lt;/code&gt; and &lt;code&gt;::after&lt;/code&gt; with &lt;code&gt;content: attr(data-word)&lt;/code&gt;), so the heading's text is simply "I build things for the web." The animation looks exactly the same.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; per page.&lt;/li&gt;
&lt;li&gt;No decorative fragments, counters or icons with text inside it.&lt;/li&gt;
&lt;li&gt;Don't hide keyword text from visitors. Google treats hidden text as spam.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Make sure the content is in the HTML
&lt;/h2&gt;

&lt;p&gt;Server components render to HTML by default, which is exactly what you want. Problems start when important text only appears after client-side JavaScript runs: inside a &lt;code&gt;useEffect&lt;/code&gt;, behind a tab that only mounts on click, or drawn on a canvas. Google can render JavaScript, but later and less reliably, and most AI crawlers don't run it at all.&lt;/p&gt;

&lt;p&gt;The quick test: fetch the page the way a crawler does and search for a sentence you care about.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s2"&gt;"Mozilla/5.0 (compatible; Googlebot/2.1)"&lt;/span&gt; https://www.example.com/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"a sentence from your page"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it prints &lt;code&gt;0&lt;/code&gt;, that text isn't in the HTML a crawler receives.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Open Graph images per page
&lt;/h2&gt;

&lt;p&gt;Add an &lt;code&gt;opengraph-image.tsx&lt;/code&gt; file to a route segment and Next.js generates the image and the meta tags for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/blog/[slug]/opengraph-image.tsx&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;ImageResponse&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/og&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;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;630&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;contentType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/png&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;default&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;Image&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;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;post&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;getPost&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;await&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ImageResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;size&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;p&gt;It won't change your rankings, but links shared on LinkedIn, WhatsApp and X look like something worth opening instead of a grey box.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Speed: images, fonts and third-party scripts
&lt;/h2&gt;

&lt;p&gt;Core Web Vitals feed into ranking, and they decide whether visitors stay long enough to read anything.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Images:&lt;/strong&gt; use &lt;code&gt;next/image&lt;/code&gt; with &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; (or &lt;code&gt;fill&lt;/code&gt; inside a sized parent) so space is reserved. Add &lt;code&gt;preload&lt;/code&gt; to the one image that is your largest paint. In Next.js 16, &lt;code&gt;preload&lt;/code&gt; replaced the old &lt;code&gt;priority&lt;/code&gt; prop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fonts:&lt;/strong&gt; use &lt;code&gt;next/font&lt;/code&gt;. It self-hosts the font and generates a size-matched fallback, which removes most font-swap layout shift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party scripts:&lt;/strong&gt; load analytics and tag managers late. On this site, Google Tag Manager loads on the first interaction, or after a few seconds if there isn't one, so it never competes with the first paint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full playbook is in &lt;a href="https://www.vishalsolanki.online/blog/core-web-vitals-nextjs-lcp-inp-cls" rel="noopener noreferrer"&gt;Core Web Vitals in 2026: fixing LCP, INP and CLS in Next.js&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Real 404s and clean redirects
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A missing page must return a &lt;strong&gt;404 status&lt;/strong&gt;, not a friendly page with a 200. Call &lt;code&gt;notFound()&lt;/code&gt; for unknown slugs, and set &lt;code&gt;dynamicParams = false&lt;/code&gt; on statically generated routes so unknown URLs 404 automatically.&lt;/li&gt;
&lt;li&gt;Add an &lt;code&gt;app/not-found.tsx&lt;/code&gt; that matches your design and links to your most useful pages.&lt;/li&gt;
&lt;li&gt;Use permanent redirects (308) when a URL has moved for good and temporary ones (307) when it hasn't.&lt;/li&gt;
&lt;li&gt;Avoid redirect chains. Each extra hop costs crawl time and a little speed for visitors.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  12. Tell Google and Bing the site exists
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Verify your domain in &lt;strong&gt;Google Search Console&lt;/strong&gt; with a DNS record (that covers every subdomain and protocol), submit &lt;code&gt;sitemap.xml&lt;/code&gt; and request indexing for your key pages.&lt;/li&gt;
&lt;li&gt;Import the property into &lt;strong&gt;Bing Webmaster Tools&lt;/strong&gt;. Bing also powers Microsoft Copilot and feeds ChatGPT search, so it's worth the two minutes.&lt;/li&gt;
&lt;li&gt;Check the Pages report weekly. "Crawled – currently not indexed" usually means a page is thin, duplicated or not linked from anywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;metadataBase&lt;/code&gt; and a title template&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app/layout.tsx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Unique title and description on every page&lt;/td&gt;
&lt;td&gt;&lt;code&gt;generateMetadata&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;One canonical host, the rest redirected&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;alternates.canonical&lt;/code&gt;, &lt;code&gt;redirects()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Sitemap with accurate &lt;code&gt;lastModified&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app/sitemap.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Search and AI crawlers allowed&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;app/robots.ts&lt;/code&gt;, CDN or firewall rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;JSON-LD, validated&lt;/td&gt;
&lt;td&gt;Server component script tag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;One real H1 per page&lt;/td&gt;
&lt;td&gt;Your components&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Key text in the server HTML&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;curl&lt;/code&gt; test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Open Graph image per page&lt;/td&gt;
&lt;td&gt;&lt;code&gt;opengraph-image.tsx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Good Core Web Vitals&lt;/td&gt;
&lt;td&gt;PageSpeed Insights, Search Console&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Real 404s, no redirect chains&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;not-found.tsx&lt;/code&gt;, &lt;code&gt;notFound()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Search Console and Bing set up&lt;/td&gt;
&lt;td&gt;Their dashboards&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is Next.js good for SEO?
&lt;/h3&gt;

&lt;p&gt;Yes. Server rendering, the Metadata API, file-based sitemaps and robots, and built-in image and font optimisation cover most technical SEO out of the box. What it can't do is choose your keywords, write your content or earn links. That part is still on you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I still need a package like next-seo?
&lt;/h3&gt;

&lt;p&gt;Not with the App Router. The built-in Metadata API covers what &lt;code&gt;next-seo&lt;/code&gt; used to do for the Pages Router.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long until a new Next.js site ranks?
&lt;/h3&gt;

&lt;p&gt;Your own name usually ranks within a few weeks of being indexed. Competitive service keywords take months of useful content and links. Search Console shows you where you stand long before the traffic arrives.&lt;/p&gt;




&lt;p&gt;Want this checklist run on your site, with the fixes done for you? &lt;a href="https://www.vishalsolanki.online/#contact" rel="noopener noreferrer"&gt;That's what I do&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.vishalsolanki.online/blog/nextjs-seo-checklist" rel="noopener noreferrer"&gt;vishalsolanki.online&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>seo</category>
      <category>approuter</category>
    </item>
    <item>
      <title>How map() and Map are Different in JavaScript.</title>
      <dc:creator>Vishal Solanki</dc:creator>
      <pubDate>Mon, 26 Aug 2024 18:50:08 +0000</pubDate>
      <link>https://dev.to/vishall/how-map-and-map-are-different-in-javascript-4abo</link>
      <guid>https://dev.to/vishall/how-map-and-map-are-different-in-javascript-4abo</guid>
      <description>&lt;p&gt;map() and Map are two of the most commonly confused topics in JavaScript because their names are so similar. However, they serve very different purposes. The map() function is used for iterating over arrays, while the Map object is a data structure that helps organize data in a structured way. Let’s dive into these concepts — I’ll do my best to make them easy to understand with clear examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;map()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The map() function is available for Arrays in JavaScript. It Iterate over each element in an array, modifies them, and returns a new array. map() is mainly used to transform the elements of an array based on the function you provide.&lt;br&gt;
&lt;strong&gt;For Example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const numberList = [1,2,3,4,5,6,7,8,9];
const doubledList = numberList?.map((number)=&amp;gt; number*2)
console.log(doubledList);
console.log(numberList);
// output: [2, 4, 6, 8, 10, 12, 14, 16, 18]
// output: [1,2,3,4,5,6,7,8,9]

/* Here map function created a new array where
 every element of the original array is doubled. */
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;code&gt;Map&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Also known as the Map object, it is a built-in JavaScript feature that lets us store key-value pairs, similar to an object. However, here the catch is that in a Map object a key can be of any type — you can even use an entire object as a key. This is different from objects, where keys are usually limited to strings and symbols.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Create a new Map
const myMap = new Map();

// Use different types as keys
const keyString = "a string"; // using string as a key
const keyObject = {name:'i am a key'}; //using a object as a key
const keyFunction = function() {}; // using a function as a key

// Set values in the Map
myMap.set(keyString, "This is a string key");
myMap.set(keyObject, "This is an object key");
myMap.set(keyFunction, "This is a function key");

// Get values from the Map
console.log(myMap.get(keyString)); 
// Output: "This is a string key"

console.log(myMap.get(keyObject));  
// Output: "This is an object key"

console.log(myMap.get(keyFunction)); 
// Output: "This is a function key"

console.log(myMap)   
/* Output: Map(3) {
  'a string' =&amp;gt; 'This is a string key',
  { name: 'i am a key' } =&amp;gt; 'This is an object key',
  [Function: keyFunction] =&amp;gt; 'This is a function key'
}*/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try running this code in your editor to understand it more clearly, but here’s a summary of what’s happening:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First, we are creating a new Map object like this:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;const myMap = new Map();&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Then we create some variables and assign them different data types, like a string and an object.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;After that, we use the .set() method provided by the Map object to create key-value pairs in the myMap object Like this:&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;myMap.set(key,value);&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We then access those values using the .get() method:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;myMap.get(key)&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the final console.log, you can see how the values and keys are stored in a similar way to an object within the myMap object. The key difference is that they can all be of different types.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this I think we’ve clarified the difference between map() and Map, but it might still be challenging to understand where and how to use the Map object in real-life projects. Let’s look at a real-life example to see how the Map object can be useful and how it can be applied in practical situations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example: Shopping Cart&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To better understand how the Map object can be useful in real-life projects, let’s explore a practical example in an e-commerce application. We’ll look at how a Map can be used to manage a shopping cart.&lt;/p&gt;

&lt;p&gt;In a E-commerce Application , you will need to manage items in a shopping cart. Each item will have a unique ID and some other data like product details and its quantity. Now you will have to manage these items like adding or updating them.&lt;/p&gt;

&lt;p&gt;Lets see how we can achieve this with Map Object and why it is more effective then other methods.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Create a new Map for the shopping cart
const cart = new Map();

// Adding Items to the Cart:
/* When a user adds a product to the cart, 
you can check if the product already exists in the cart. 
If it does, you increase the quantity; 
if not, you add it as a new entry. */

function addToCart(product, quantity = 1) {
       if (cart.has(product.id)) {
           // If product is already in the cart, update the quantity
           let cartItem = cart.get(product.id);
           cartItem.quantity += quantity;
       } else {
           // If product is not in the cart, add it
           cart.set(product.id, {
               name: product.name,
               price: product.price,
               quantity: quantity
           });
       }
   }

const product1 = { id: 101, name: 'Laptop', price: 999 };
const product2 = { id: 102, name: 'Smartphone', price: 499 };
addToCart(product1, 1);
addToCart(product2, 2);
addToCart(product1, 1); // Adding the same product again

//Accessing Items from the Cart:
//You can easily retrieve the details of a product in the cart using its ID.

console.log(cart.get(101)); 
console.log(cart.get(102));
// Output: { name: 'Laptop', price: 999, quantity: 2 }
// Output: { name: 'Smartphone', price: 499, quantity: 1 }

/* This output shows that the `Laptop` has been added twice 
and `smartphone` is added only once, so the quantity is updated to 2 and 1. */

//Removing Items from the Cart:
function removeFromCart(productId) {
       cart.delete(productId);
   }

 removeFromCart(102); // Removes the 'Smartphone' from the cart

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pretty easy right. I hope this example helps you see how &lt;code&gt;Map&lt;/code&gt; can be used in a real-life frontend development scenario! Let me know if you have any more questions.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
