<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Vishal Solanki]]></title><description><![CDATA[Vishal Solanki]]></description><link>https://vishalsolanki.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 02 Nov 2026 03:06:59 GMT</lastBuildDate><atom:link href="https://vishalsolanki.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[I built an Android file viewer that has no internet permission]]></title><description><![CDATA[Most file viewer apps I tried on Android wanted access to all my storage, showed ads between pages, or asked me to sign in just to open a PDF someone sent…]]></description><link>https://vishalsolanki.hashnode.dev/i-built-an-android-file-viewer-that-has-no-internet-permission</link><guid isPermaLink="true">https://vishalsolanki.hashnode.dev/i-built-an-android-file-viewer-that-has-no-internet-permission</guid><dc:creator><![CDATA[Vishal Solanki]]></dc:creator><pubDate>Sun, 01 Nov 2026 08:09:56 GMT</pubDate><content:encoded><![CDATA[<p>Most file viewer apps I tried on Android wanted access to all my storage, showed ads between pages, or asked me to sign in just to open a PDF someone sent me on WhatsApp. So I built my own. It's called Folio, it's free, and the one rule I set on day one was simple: the app gets no <code>INTERNET</code> permission. Not "we don't collect data". Android itself won't let it open a socket.</p>
<p>That one rule shaped almost every decision after it. Here's what it was like to build.</p>
<h2>What it opens</h2>
<ul>
<li>PDF, with search and password-protected files</li>
<li>Word (.docx), Excel (.xlsx) and PowerPoint (.pptx)</li>
<li>CSV, TXT, Markdown, logs, subtitles, contacts (.vcf) and calendars (.ics)</li>
<li>JSON, XML, YAML, HTML and 40+ programming languages with syntax colours</li>
<li>Images including HEIC, AVIF and SVG</li>
<li>Audio and video</li>
<li>ZIP files: browse them and open what's inside</li>
</ul>
<p>Text, Markdown, CSV and code can be edited. Word, Excel and PowerPoint stay view-only on purpose. I'd rather show you a slightly simplified slide than ship an editor that quietly breaks your layout when you save.</p>
<h2>No storage permission either</h2>
<p>Folio also doesn't ask for storage access. It only sees files you open with it (from WhatsApp, Gmail or the Files app), files you pick inside it, or folders you add yourself. All of that goes through Android's Storage Access Framework, so access is per file or per folder, and you can take it back.</p>
<p>The catch: a file can reach the app under several different URIs. The Downloads provider alone has more than one form. So Recents has to dedupe by name, size and type, or the same PDF shows up three times.</p>
<h2>The stack</h2>
<p>Kotlin and Jetpack Compose, one app module, no backend because there can't be one.</p>
<ul>
<li><strong>PDF:</strong> PDFium, the engine Chrome uses. All rendering runs on a single background thread so renders, search and close never overlap. Pages re-render sharper after you zoom, with a cap of about 8 MP per page so memory stays sane on a 200-page file.</li>
<li><strong>Word:</strong> mammoth converts .docx to HTML, shown in a locked-down WebView. Mammoth didn't work on Android out of the box (Android's XML parser rejects one of the features it sets), so I ship a patched copy of one small class.</li>
<li><strong>Excel:</strong> my own streaming XLSX reader. The grid draws each row in one pass instead of one composable per cell, which is the difference between smooth and unusable on a 50,000-row CSV.</li>
<li><strong>PowerPoint:</strong> my own PPTX reader. Getting text styles right meant following inheritance from shape to layout to master, which was most of the work.</li>
<li><strong>Code and text:</strong> Sora Editor with TextMate grammars from VS Code.</li>
<li><strong>Images and media:</strong> Coil (without its network module) and Media3 ExoPlayer.</li>
</ul>
<p>The WebView used for Word and HTML has JavaScript off, network loads blocked, and file access off. HTML files also get their scripts stripped before they're shown. If a page needs scripts, there's an "Open in browser" option that says so before you tap it.</p>
<h2>Features you can't build the normal way offline</h2>
<p>Some things every app has turned out to be interesting without internet.</p>
<p><strong>Feedback.</strong> There's no API to call, so "Send feedback" opens an email draft in your mail app. You read it and decide whether to send it.</p>
<p><strong>Crash reports.</strong> When the app crashes, it writes the stack trace to a local file. Next launch, it asks if you want to email it. Nothing leaves the phone unless you press send.</p>
<p><strong>Updates.</strong> No push, no background checks. "Check for updates" opens my website in your browser with your installed version in the URL, and the page tells you if there's something newer. The page does the comparison in your browser, so the site stays static.</p>
<h2>Shipping without the Play Store</h2>
<p>I can't pay for a Play developer account right now, so Folio ships as an APK on GitHub Releases. Things I learned doing that:</p>
<ul>
<li><strong>The signing key is the app.</strong> Without Play App Signing, every update must be signed with the same key forever. Lose it and every user has to uninstall to update. Mine is backed up in two places.</li>
<li><strong>Chrome can sit at 100% on APK downloads.</strong> It runs a safety check after the download finishes and can hang there. Closing the download panel and opening Chrome's Downloads usually unsticks it. Other browsers like Brave don't do it.</li>
<li><strong>Play Protect will warn people.</strong> It says "App scan recommended" for any app it hasn't seen. My download page shows every warning screen in advance so nobody is surprised.</li>
<li><strong>Obtainium</strong> can track GitHub releases and update the app automatically, which is the closest thing to auto-update outside the Play Store.</li>
</ul>
<h2>Try it</h2>
<p>Folio needs Android 8.0 or newer and is about 25 MB. Screenshots, the download and step-by-step install instructions are here:</p>
<p><a href="https://www.vishalsolanki.online/apps/folio?utm_source=devto&amp;utm_medium=article&amp;utm_campaign=folio_launch">Download Folio</a></p>
<p>Built this because I was tired of file viewers full of ads and permissions. If you’ve felt the same, I hope it helps.</p>
<p>It’s my first public app, so if it fails on one of your files or there’s something you’d want it to do, let me know. There’s a feedback form on the page, and I read all of it.</p>
]]></content:encoded></item><item><title><![CDATA[Securing Next.js Server Actions: a 10-point checklist for 2026]]></title><description><![CDATA[Every exported Server Action is a public POST endpoint. Here's what Next.js protects for you, what it doesn't, and the ten checks I put on every action…]]></description><link>https://vishalsolanki.hashnode.dev/secure-nextjs-server-actions</link><guid isPermaLink="true">https://vishalsolanki.hashnode.dev/secure-nextjs-server-actions</guid><category><![CDATA[Next.js]]></category><category><![CDATA[Security]]></category><category><![CDATA[React]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Vishal Solanki]]></dc:creator><pubDate>Tue, 27 Oct 2026 19:04:01 GMT</pubDate><content:encoded><![CDATA[<p>Server Actions are the nicest way to write mutations in Next.js. You write an async function, mark it <code>"use server"</code>, pass it to a form, and you're done: no API route, no fetch call, no loading-state plumbing.</p>
<p>That convenience hides one fact you need to keep in mind every time: <strong>each exported Server Action is a public HTTP endpoint.</strong> Anyone can call it with a POST request, from anywhere, with any arguments, whether or not your UI shows the button.</p>
<p>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.</p>
<h2>What Next.js already does for you</h2>
<p>It's worth knowing exactly where the built-in protection stops.</p>
<table>
<thead>
<tr>
<th>Built in</th>
<th>What it means</th>
</tr>
</thead>
<tbody><tr>
<td>Encrypted, non-deterministic action IDs</td>
<td>The client calls an action by an ID, not by its name. IDs change between builds.</td>
</tr>
<tr>
<td>Dead-code elimination</td>
<td>Actions your app never uses are removed at build time and don't become endpoints.</td>
</tr>
<tr>
<td>POST only</td>
<td>Actions can't be triggered by a link, an image tag or a prefetch.</td>
</tr>
<tr>
<td>Origin check</td>
<td>When the browser sends an <code>Origin</code> header, it must match the host. That blocks most cross-site request forgery.</td>
</tr>
<tr>
<td>Body size limit</td>
<td>Request bodies are capped at 1 MB by default.</td>
</tr>
</tbody></table>
<p>What it does <strong>not</strong> 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.</p>
<h2>1. Authenticate inside the action, every time</h2>
<p>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.</p>
<pre><code class="language-ts">"use server";

import { getSession } from "@/lib/auth";

export async function updateProfile(formData: FormData) {
  const session = await getSession();
  if (!session) throw new Error("Not signed in");
  // ...
}
</code></pre>
<h2>2. Authorise the specific record</h2>
<p>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.</p>
<pre><code class="language-ts">export async function deleteAddress(addressId: string) {
  const session = await getSession();
  if (!session) throw new Error("Not signed in");

  const address = await db.address.findUnique({ where: { id: addressId } });
  if (!address || address.userId !== session.userId) throw new Error("Not found");

  await db.address.delete({ where: { id: addressId } });
}
</code></pre>
<p>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.</p>
<h2>3. Validate every argument</h2>
<p>TypeScript types disappear at runtime. A caller can send any shape, any length and any type. Validate on the server with a schema:</p>
<pre><code class="language-ts">import { z } from "zod";

const Profile = z.object({
  name: z.string().trim().min(1).max(100),
  email: z.string().trim().email().max(200),
  city: z.string().trim().max(80).optional(),
});

export async function updateProfile(formData: FormData) {
  const session = await getSession();
  if (!session) throw new Error("Not signed in");

  const parsed = Profile.safeParse(Object.fromEntries(formData));
  if (!parsed.success) return { error: "Please check the highlighted fields." };

  await db.user.update({ where: { id: session.userId }, data: parsed.data });
  return { ok: true };
}
</code></pre>
<p>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.</p>
<h2>4. Keep the database and secrets behind a server-only layer</h2>
<p>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:</p>
<pre><code class="language-ts">// lib/data/addresses.ts
import "server-only";

export async function deleteAddressFor(userId: string, addressId: string) {
  // ownership check + delete, as in step 2
}
</code></pre>
<p>Then actions stay thin: read the session, validate the input, call the data layer. Auditing one folder is much easier than auditing every <code>"use server"</code> function in the app.</p>
<h2>5. Return only what the client needs</h2>
<p>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.</p>
<p>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.</p>
<h2>6. Keep sessions in httpOnly, secure cookies</h2>
<p>Tokens in <code>localStorage</code> can be read by any script that runs on your page, including a compromised third-party one. An <code>httpOnly</code> cookie can't. Set it from a Server Action or Route Handler, where Next.js can send the <code>Set-Cookie</code> header:</p>
<pre><code class="language-ts">import { cookies } from "next/headers";

export async function signIn(formData: FormData) {
  // ...verify credentials, create a session...
  (await cookies()).set("session", sessionId, {
    httpOnly: true, // not readable from JavaScript
    secure: true, // HTTPS only
    sameSite: "lax", // not sent on most cross-site requests
    path: "/",
    maxAge: 60 * 60 * 24 * 7, // one week
  });
}
</code></pre>
<p>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.</p>
<h2>7. Rate-limit, and stop the obvious bots</h2>
<p>Sign-in, sign-up, password reset, OTP and contact forms get hammered. Limit how often a client can call them, and enforce those limits on the server.</p>
<pre><code class="language-ts">import { headers } from "next/headers";
import { rateLimit } from "@/lib/rate-limit";

export async function requestOtp(formData: FormData) {
  const requestHeaders = await headers();

  // Use the trusted client-IP header provided by your hosting platform.
  // Replace this with the header documented by your provider.
  const ip = requestHeaders.get("YOUR_TRUSTED_CLIENT_IP_HEADER");

  if (!ip) {
    throw new Error("Unable to determine client IP");
  }

  // Apply limits by trusted IP and by normalized account identifier.
  const email = String(formData.get("email") ?? "")
    .trim()
    .toLowerCase();

  if (!email || email.length &gt; 254) {
    return { error: "Please enter a valid email address." };
  }

  const ipAllowed = await rateLimit(`otp:ip:${ip}`, {
    limit: 10,
    windowSeconds: 600,
  });

  const accountAllowed = await rateLimit(`otp:email:${email}`, {
    limit: 5,
    windowSeconds: 600,
  });

  if (!ipAllowed || !accountAllowed) {
    return {
      error: "Too many attempts. Try again in a few minutes.",
    };
  }

  // Validate the request and continue with OTP generation and delivery.
}
</code></pre>
<p><strong>Important:</strong> <code>YOUR_TRUSTED_CLIENT_IP_HEADER</code> is a placeholder, not a literal header name. Replace it with the client-IP header documented by your hosting provider, and verify that clients cannot spoof it. If you're self-hosting behind a reverse proxy, configure trusted proxies correctly before relying on forwarded IP headers. Never blindly trust the first <code>X-Forwarded-For</code> value.</p>
<p>The example uses separate IP-based and email-based limits. The limits shown are illustrative and should be adjusted to your traffic, abuse risk and OTP provider's limits. Apply account-based limits carefully to avoid allowing attackers to lock legitimate users out. Your rate limiter should use a shared store, such as Redis or your hosting platform's KV service, with atomic updates so concurrent requests cannot bypass the limit.</p>
<p>Turn on your hosting provider's firewall or bot protection as an additional layer, not as a replacement for application-level rate limiting.</p>
<p>For public forms, a hidden honeypot field can catch simple bots cheaply: legitimate users should leave it empty, so submissions that fill it can be rejected. The contact form on this site uses that approach. Honeypots are only a supplementary defense; sophisticated bots can bypass them.</p>
<h2>8. Understand the CSRF protection, and don't break it</h2>
<p>Next.js compares the request's <code>Origin</code> header with the host the request arrived at, and rejects actions that don't match. That covers most cross-site request forgery, together with <code>SameSite</code> cookies.</p>
<p>Two ways to break it by accident:</p>
<ul>
<li><strong>Behind a reverse proxy</strong> that forwards its own host instead of the public one, valid requests start failing. Add the public host to <code>experimental.serverActions.allowedOrigins</code> in <code>next.config.ts</code>, rather than disabling anything.</li>
<li><strong>Mutations in GET Route Handlers.</strong> 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.</li>
</ul>
<h2>9. Mind the size of what's sent</h2>
<p>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.</p>
<h2>10. Keep Next.js patched</h2>
<p>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 <code>use cache</code> 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 <code>sharp</code> resolves to 0.35.5 or later as well.</p>
<p>Run <code>npm audit</code> regularly, read the advisories it reports, and update to the patched release. If you also run PHP anywhere, see <a href="https://www.vishalsolanki.online/blog/php2shell-cve-2026-109618-fix">PHP2Shell: is your website affected</a>.</p>
<h2>The audit checklist</h2>
<p>Run through this for every file that contains <code>"use server"</code>:</p>
<table>
<thead>
<tr>
<th>#</th>
<th>Question</th>
<th>Fix if not</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>Does the action check the session itself?</td>
<td>Call your auth helper first thing</td>
</tr>
<tr>
<td>2</td>
<td>Does it check the caller owns the record?</td>
<td>Compare the owner ID before reading or writing</td>
</tr>
<tr>
<td>3</td>
<td>Are all arguments validated on the server?</td>
<td>Schema or validation function</td>
</tr>
<tr>
<td>4</td>
<td>Is database and secret access in a <code>server-only</code> layer?</td>
<td>Move it, keep the action thin</td>
</tr>
<tr>
<td>5</td>
<td>Does it return only what the UI needs?</td>
<td>Pick fields, generic errors</td>
</tr>
<tr>
<td>6</td>
<td>Are sessions in <code>httpOnly</code>, <code>secure</code> cookies?</td>
<td>Move tokens out of <code>localStorage</code></td>
</tr>
<tr>
<td>7</td>
<td>Are public or sensitive actions rate-limited?</td>
<td>Shared-store limiter, honeypot, host firewall</td>
</tr>
<tr>
<td>8</td>
<td>Does anything mutate data on GET?</td>
<td>Move it to a Server Action or a POST handler</td>
</tr>
<tr>
<td>9</td>
<td>Are uploads going through the action body?</td>
<td>Signed direct-to-storage uploads</td>
</tr>
<tr>
<td>10</td>
<td>Does <code>npm audit</code> report Next.js advisories?</td>
<td>Update to the patched version</td>
</tr>
</tbody></table>
<h2>FAQ</h2>
<h3>Are Server Actions secure?</h3>
<p>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.</p>
<h3>Should I use Server Actions or Route Handlers for mutations?</h3>
<p>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 <a href="https://www.vishalsolanki.online/blog/nextjs-backend-server-actions-route-handlers-or-nodejs-api">Next.js as a backend: Server Actions, Route Handlers or a separate Node.js API</a>.</p>
<h3>Do I need my own CSRF tokens?</h3>
<p>Usually not. The built-in origin check plus <code>SameSite</code> 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.</p>
<hr />
<p>Want a security review of your Next.js app's actions and API routes? <a href="https://www.vishalsolanki.online/#contact">Get in touch</a>.</p>
<p><em>Originally published on</em> <a href="https://www.vishalsolanki.online/blog/secure-nextjs-server-actions"><em>vishalsolanki.online</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[PHP2Shell (CVE-2026-109618 and CVE-2026-109619): is your website affected, and how to fix it]]></title><description><![CDATA[Two critical PHP flaws let attackers run code on a web server without logging in. Which sites are affected, the fixed PHP versions, how to check yours on…]]></description><link>https://vishalsolanki.hashnode.dev/php2shell-cve-2026-109618-fix</link><guid isPermaLink="true">https://vishalsolanki.hashnode.dev/php2shell-cve-2026-109618-fix</guid><category><![CDATA[PHP]]></category><category><![CDATA[Security]]></category><category><![CDATA[WordPress]]></category><category><![CDATA[CVE]]></category><dc:creator><![CDATA[Vishal Solanki]]></dc:creator><pubDate>Tue, 27 Oct 2026 19:03:49 GMT</pubDate><content:encoded><![CDATA[<p>On 21 October 2026, the PHP project released emergency updates for two critical vulnerabilities, <strong>CVE-2026-109618</strong> and <strong>CVE-2026-109619</strong>, now known as <strong>PHP2Shell</strong>. They let an attacker run commands on a web server with a single crafted request, <strong>without an account and without touching your site's own code.</strong></p>
<p>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.</p>
<blockquote>
<p><strong>Short version:</strong> update the PHP runtime that serves your site to <strong>8.2.35, 8.3.36, 8.4.27 or 8.5.12</strong> (or your vendor's patched package), then restart PHP. Updating WordPress or your plugins does <strong>not</strong> fix this. The widely shared <code>session.upload_progress.enabled=0</code> setting is <strong>not</strong> a complete fix.</p>
</blockquote>
<p><em>Last checked: 24 October 2026. This is a fast-moving story; follow PHP's official advisories for changes.</em></p>
<h2>What PHP2Shell is, in plain words</h2>
<p>Both flaws are in the PHP engine itself, not in WordPress, Laravel or any plugin. That's what makes them unusual: <strong>a site with perfectly secure code is still exposed</strong>, 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.</p>
<ul>
<li><strong>CVE-2026-109618:</strong> 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.</li>
<li><strong>CVE-2026-109619:</strong> 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.</li>
</ul>
<p>Both are rated <strong>9.2 out of 10</strong> (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.</p>
<p>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.</p>
<h2>Is it being exploited?</h2>
<p>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.</p>
<p>Treat this as urgent, not theoretical.</p>
<h2>Which sites are affected?</h2>
<p><strong>Any server where PHP answers web requests</strong>, in any of these setups:</p>
<table>
<thead>
<tr>
<th>How PHP runs</th>
<th>Affected?</th>
<th>Notes</th>
</tr>
</thead>
<tbody><tr>
<td>Apache with <code>mod_php</code></td>
<td>Yes, highest risk</td>
<td>A single request can trigger the attack before any of your code runs</td>
</tr>
<tr>
<td>PHP-FPM (Nginx, Apache, most VPS setups)</td>
<td>Yes</td>
<td>Confirmed command execution through the second flaw</td>
</tr>
<tr>
<td>php-cgi</td>
<td>Yes</td>
<td></td>
</tr>
<tr>
<td>FrankenPHP</td>
<td>Yes</td>
<td></td>
</tr>
<tr>
<td>LiteSpeed / LSAPI (common on shared hosting)</td>
<td>Yes</td>
<td></td>
</tr>
<tr>
<td>PHP only used on the command line, never serving web requests</td>
<td>Lower risk</td>
<td>Still update, but the web attack path doesn't apply</td>
</tr>
</tbody></table>
<p>Affected versions go back to PHP 5.4. These are the first fixed releases:</p>
<table>
<thead>
<tr>
<th>PHP branch</th>
<th>Vulnerable</th>
<th>First fixed</th>
</tr>
</thead>
<tbody><tr>
<td>8.5</td>
<td>below 8.5.12</td>
<td><strong>8.5.12</strong></td>
</tr>
<tr>
<td>8.4</td>
<td>below 8.4.27</td>
<td><strong>8.4.27</strong></td>
</tr>
<tr>
<td>8.3</td>
<td>below 8.3.36</td>
<td><strong>8.3.36</strong></td>
</tr>
<tr>
<td>8.2</td>
<td>below 8.2.35</td>
<td><strong>8.2.35</strong></td>
</tr>
<tr>
<td>8.1, 8.0, 7.4</td>
<td>all</td>
<td>No official release. PHP published source patches; check your host or distribution, or upgrade to 8.2+</td>
</tr>
</tbody></table>
<p>PHP 8.2 itself reaches end of life on 31 December 2026, so if you're upgrading anyway, go to 8.3 or newer.</p>
<p><strong>Not affected:</strong> 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.</p>
<h2>How to check and fix it, by type of hosting</h2>
<h3>Shared hosting (cPanel, Hostinger, GoDaddy, Bluehost and similar)</h3>
<p>You usually can't patch PHP yourself here. Your host does it.</p>
<ol>
<li>Find your PHP version. In WordPress, go to <strong>Tools → Site Health → Info → Server</strong>. In cPanel, look for <strong>Select PHP Version</strong> or <strong>MultiPHP Manager</strong>.</li>
<li><strong>Ask your host directly</strong> whether their PHP builds include the fixes for <strong>CVE-2026-109618 and CVE-2026-109619</strong>. Hosts often patch without changing the version number you see, so the version alone doesn't tell you.</li>
<li>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.</li>
</ol>
<p>A short message you can send:</p>
<blockquote>
<p>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?</p>
</blockquote>
<h3>WordPress, WooCommerce, Laravel and other PHP apps</h3>
<p>Keep your app and plugins updated as always, but understand that <strong>this isn't an application bug</strong>. 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.</p>
<h3>VPS or dedicated server (Ubuntu, Debian, RHEL)</h3>
<ol>
<li>Check what actually serves your site. <code>php -v</code> 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 <code>phpinfo()</code> page that you <strong>delete straight after</strong>).</li>
<li>Update through your package manager and restart <strong>every</strong> PHP process:</li>
</ol>
<pre><code class="language-bash"># Debian / Ubuntu (package names vary, e.g. php8.3-fpm)
sudo apt update &amp;&amp; sudo apt upgrade
sudo systemctl restart php8.3-fpm   # PHP-FPM
sudo systemctl restart apache2      # if you use mod_php
</code></pre>
<ol start="3">
<li>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:</li>
</ol>
<pre><code class="language-bash">apt changelog php8.3-fpm | grep -m1 CVE-2026-109618
</code></pre>
<h3>Docker and Kubernetes</h3>
<p>Rebuild your images on a patched PHP base image, redeploy, and make sure <strong>no old containers are still running</strong>. Also check images you didn't build yourself: some third-party apps bundle their own PHP.</p>
<h2>If you can't patch today</h2>
<p>PHP is clear that <strong>only updating fully fixes this.</strong> These steps reduce exposure while you arrange the update:</p>
<ul>
<li><strong>Put a firewall in front of PHP.</strong> 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.</li>
<li><strong>Limit request sizes</strong> at your web server or proxy, and keep <code>post_max_size</code> low.</li>
<li>PHP's temporary guidance for CVE-2026-109618 is to enforce <code>memory_limit=-1</code> at the administrator level, together with request-size limits in front of PHP and a memory limit on the container.</li>
<li>For CVE-2026-109619: <code>session.upload_progress.enabled=0</code>, plus <code>fatal_error_backtraces=0</code> on PHP 8.5, then restart every worker. This blocks the confirmed paths but <strong>not</strong> every variant, and does nothing for CVE-2026-109618.</li>
<li><strong>Restrict access</strong> to anything that doesn't need to be public, such as admin panels, staging sites and internal tools.</li>
</ul>
<h2>Signs you might have been attacked</h2>
<p>If your server ran an unpatched PHP version while exposed to the internet, look back through your logs for:</p>
<ul>
<li>Unusually large form or multipart POST requests, followed by memory-limit errors or bursts of HTTP 500 responses.</li>
<li>Oddly large <code>PHPSESSID</code> cookies in responses.</li>
<li>PHP workers restarting without explanation.</li>
<li>Shell commands or processes started by your web server user (for example <code>www-data</code>).</li>
</ul>
<p>If you see any of these, assume secrets in memory may have leaked: <strong>rotate database passwords, API keys and admin passwords</strong>, invalidate active sessions, and check for new admin users or modified files.</p>
<h2>After you patch</h2>
<ul>
<li>The fix adds a new hard memory limit. Hitting it ends the process, so review <code>memory_limit</code> and <code>post_max_size</code> against what your site actually needs, and test uploads and large forms.</li>
<li>PHP has since published an advisory for a related issue, <strong>CVE-2026-115560</strong>, 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.</li>
</ul>
<h2>Running Next.js? Different bug, same month</h2>
<p>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:</p>
<ul>
<li><strong>CVE-2026-96889</strong>, a critical remote code execution flaw in the Image Optimization API when SVG files are processed, through the <code>sharp</code> image library on Linux.</li>
<li>A second critical remote code execution flaw in <code>use cache</code> functions that return typed arrays (Next.js 16).</li>
<li>A high-severity denial-of-service issue affecting App Router apps that use Server Actions.</li>
</ul>
<p>Upgrade to <strong>Next.js 16.4.1 or 15.5.28</strong> or later, and make sure your lockfile resolves <code>sharp</code> to <strong>0.35.5 or later</strong>. Running <code>npm audit</code> will flag both. More on hardening Server Actions in my <a href="https://www.vishalsolanki.online/blog/secure-nextjs-server-actions">security checklist</a>.</p>
<h2>FAQ</h2>
<h3>I updated WordPress and all my plugins. Am I safe?</h3>
<p>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.</p>
<h3>Does a WordPress security plugin protect me?</h3>
<p>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.</p>
<h3>My site is small. Would anyone really target it?</h3>
<p>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.</p>
<h3>How do I know the patch worked?</h3>
<p>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.</p>
<h2>Sources</h2>
<ul>
<li><a href="https://securitylabs.datadoghq.com/articles/php2shell-critical-vulnerabilities-in-php/">Datadog Security Labs: PHP2Shell detection, analysis and remediation</a></li>
<li><a href="https://thehackernews.com/2026/10/php-issues-emergency-patches-for-two.html">The Hacker News: PHP issues emergency patches for PHP2Shell</a></li>
<li><a href="https://unit42.paloaltonetworks.com/cve-2026-109618-cve-2026-109619/">Unit 42 threat brief: CVE-2026-109618 and CVE-2026-109619</a></li>
<li><a href="https://blog.cloudflare.com/php-vulnerabilities-waf-protection/">Cloudflare: protections against the php2shell vulnerability</a></li>
<li><a href="https://www.netlify.com/changelog/2026-10-14-nextjs-react-security-vulnerabilities/">Netlify: Next.js and React security release, October 2026</a></li>
</ul>
<hr />
<p>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. <a href="https://www.vishalsolanki.online/#contact">Get in touch</a>.</p>
<p><em>Originally published on <a href="https://www.vishalsolanki.online/blog/php2shell-cve-2026-109618-fix">vishalsolanki.online</a>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Next.js SEO checklist for 2026: 12 fixes for the App Router]]></title><description><![CDATA[The checklist I run on every Next.js App Router site: metadata, canonicals, sitemap, robots, JSON-LD, headings, server-rendered content, speed and Search…]]></description><link>https://vishalsolanki.hashnode.dev/nextjs-seo-checklist</link><guid isPermaLink="true">https://vishalsolanki.hashnode.dev/nextjs-seo-checklist</guid><category><![CDATA[Next.js]]></category><category><![CDATA[SEO]]></category><category><![CDATA[React]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Vishal Solanki]]></dc:creator><pubDate>Tue, 27 Oct 2026 19:03:36 GMT</pubDate><content:encoded><![CDATA[<p>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.</p>
<p>This is the checklist I run on every App Router site, including this one. Each item comes with code you can copy.</p>
<blockquote>
<p><strong>Short version:</strong> set <code>metadataBase</code>, give every page its own title and description, pick one canonical host, generate <code>sitemap.ts</code> and <code>robots.ts</code>, add JSON-LD, keep one real H1 with real text, make sure your content is server-rendered, and set up Search Console and Bing.</p>
</blockquote>
<h2>1. Set <code>metadataBase</code> and a title template</h2>
<p>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.</p>
<pre><code class="language-tsx">// app/layout.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  metadataBase: new URL("https://www.example.com"),
  title: { default: "Example — What you do, where you do it", template: "%s — Example" },
  description: "One or two sentences a searcher would actually click on.",
};
</code></pre>
<p>With the template, a page that sets <code>title: "Pricing"</code> shows up in search as "Pricing — Example", so your brand is on every result without repeating it by hand.</p>
<h2>2. Give every page its own title and description</h2>
<p>Duplicate titles are the most common problem I find in audits. For dynamic routes, use <code>generateMetadata</code>:</p>
<pre><code class="language-tsx">// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
  const post = await getPost((await params).slug);
  return {
    title: post.title,
    description: post.description,
    alternates: { canonical: `/blog/${post.slug}` },
  };
}
</code></pre>
<p>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.</p>
<h2>3. Pick one canonical host and redirect the rest</h2>
<p><code>example.com</code>, <code>www.example.com</code>, your <code>*.vercel.app</code> address and <code>http://</code> are four copies of the same site as far as Google is concerned. Pick one, use it as <code>metadataBase</code>, add a canonical to every page (as above) and permanently redirect the others.</p>
<p>On Vercel, set the primary domain in the dashboard. For an old deployment address, a host-based redirect in <code>next.config.ts</code> does the job. This one sends an old Vercel URL, and everything it ranked for, to the real domain:</p>
<pre><code class="language-ts">// next.config.ts
async redirects() {
  return [
    {
      source: "/:path*",
      has: [{ type: "host", value: "old-project.vercel.app" }],
      destination: "https://www.example.com/:path*",
      permanent: true,
    },
  ];
}
</code></pre>
<h2>4. Generate the sitemap, with honest dates</h2>
<pre><code class="language-ts">// app/sitemap.ts
import type { MetadataRoute } from "next";

export default async function sitemap(): Promise&lt;MetadataRoute.Sitemap&gt; {
  const posts = await getPosts();
  return [
    { url: "https://www.example.com", lastModified: "2026-10-24" },
    ...posts.map((p) =&gt; ({
      url: `https://www.example.com/blog/${p.slug}`,
      lastModified: p.updated ?? p.date,
    })),
  ];
}
</code></pre>
<p>Two details matter. Google ignores <code>priority</code> and <code>changeFrequency</code>, so don't spend time tuning them. It does use <code>lastModified</code>, 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.</p>
<h2>5. Add <code>robots.ts</code>, and don't block what you want</h2>
<pre><code class="language-ts">// app/robots.ts
import type { MetadataRoute } from "next";

export default function robots(): MetadataRoute.Robots {
  return {
    rules: { userAgent: "*", allow: "/" },
    sitemap: "https://www.example.com/sitemap.xml",
  };
}
</code></pre>
<p>If you want to show up in ChatGPT search and Perplexity answers, keep their crawlers (<code>OAI-SearchBot</code>, <code>PerplexityBot</code>) allowed. Also check your hosting firewall or CDN: some bot-protection settings block AI crawlers even when <code>robots.txt</code> allows them. I go deeper on this in <a href="https://www.vishalsolanki.online/blog/get-cited-by-chatgpt-perplexity-ai-search">how to get cited by ChatGPT, Perplexity and Google AI Mode</a>.</p>
<h2>6. Describe the page with JSON-LD</h2>
<p>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:</p>
<pre><code class="language-tsx">const schema = {
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  headline: post.title,
  datePublished: post.date,
  author: { "@type": "Person", name: "Your Name", url: "https://www.example.com" },
};

return (
  &lt;script
    type="application/ld+json"
    dangerouslySetInnerHTML={{ __html: JSON.stringify(schema).replace(/&lt;/g, "\\u003c") }}
  /&gt;
);
</code></pre>
<p>The <code>.replace</code> escapes <code>&lt;</code> 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.</p>
<p>For a personal or freelance site, a <code>Person</code> and a <code>ProfessionalService</code> on the homepage plus a <code>BlogPosting</code> on each post covers most of what matters. Only mark up what's visible on the page.</p>
<h2>7. One real H1, with real text</h2>
<p>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.</p>
<p>The fix was to draw the decorative words with CSS (<code>::before</code> and <code>::after</code> with <code>content: attr(data-word)</code>), so the heading's text is simply "I build things for the web." The animation looks exactly the same.</p>
<ul>
<li>One <code>&lt;h1&gt;</code> per page.</li>
<li>No decorative fragments, counters or icons with text inside it.</li>
<li>Don't hide keyword text from visitors. Google treats hidden text as spam.</li>
</ul>
<h2>8. Make sure the content is in the HTML</h2>
<p>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 <code>useEffect</code>, 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.</p>
<p>The quick test: fetch the page the way a crawler does and search for a sentence you care about.</p>
<pre><code class="language-bash">curl -s -A "Mozilla/5.0 (compatible; Googlebot/2.1)" https://www.example.com/ | grep -c "a sentence from your page"
</code></pre>
<p>If it prints <code>0</code>, that text isn't in the HTML a crawler receives.</p>
<h2>9. Open Graph images per page</h2>
<p>Add an <code>opengraph-image.tsx</code> file to a route segment and Next.js generates the image and the meta tags for you:</p>
<pre><code class="language-tsx">// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from "next/og";

export const size = { width: 1200, height: 630 };
export const contentType = "image/png";

export default async function Image({ params }: { params: Promise&lt;{ slug: string }&gt; }) {
  const post = await getPost((await params).slug);
  return new ImageResponse(
    &lt;div style={{ display: "flex", fontSize: 64, padding: 80 }}&gt;{post.title}&lt;/div&gt;,
    size,
  );
}
</code></pre>
<p>It won't change your rankings, but links shared on LinkedIn, WhatsApp and X look like something worth opening instead of a grey box.</p>
<h2>10. Speed: images, fonts and third-party scripts</h2>
<p>Core Web Vitals feed into ranking, and they decide whether visitors stay long enough to read anything.</p>
<ul>
<li><strong>Images:</strong> use <code>next/image</code> with <code>width</code> and <code>height</code> (or <code>fill</code> inside a sized parent) so space is reserved. Add <code>preload</code> to the one image that is your largest paint. In Next.js 16, <code>preload</code> replaced the old <code>priority</code> prop.</li>
<li><strong>Fonts:</strong> use <code>next/font</code>. It self-hosts the font and generates a size-matched fallback, which removes most font-swap layout shift.</li>
<li><strong>Third-party scripts:</strong> 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.</li>
</ul>
<p>The full playbook is in <a href="https://www.vishalsolanki.online/blog/core-web-vitals-nextjs-lcp-inp-cls">Core Web Vitals in 2026: fixing LCP, INP and CLS in Next.js</a>.</p>
<h2>11. Real 404s and clean redirects</h2>
<ul>
<li>A missing page must return a <strong>404 status</strong>, not a friendly page with a 200. Call <code>notFound()</code> for unknown slugs, and set <code>dynamicParams = false</code> on statically generated routes so unknown URLs 404 automatically.</li>
<li>Add an <code>app/not-found.tsx</code> that matches your design and links to your most useful pages.</li>
<li>Use permanent redirects (308) when a URL has moved for good and temporary ones (307) when it hasn't.</li>
<li>Avoid redirect chains. Each extra hop costs crawl time and a little speed for visitors.</li>
</ul>
<h2>12. Tell Google and Bing the site exists</h2>
<ul>
<li>Verify your domain in <strong>Google Search Console</strong> with a DNS record (that covers every subdomain and protocol), submit <code>sitemap.xml</code> and request indexing for your key pages.</li>
<li>Import the property into <strong>Bing Webmaster Tools</strong>. Bing also powers Microsoft Copilot and feeds ChatGPT search, so it's worth the two minutes.</li>
<li>Check the Pages report weekly. "Crawled – currently not indexed" usually means a page is thin, duplicated or not linked from anywhere.</li>
</ul>
<h2>The checklist</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>Check</th>
<th>Where</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td><code>metadataBase</code> and a title template</td>
<td><code>app/layout.tsx</code></td>
</tr>
<tr>
<td>2</td>
<td>Unique title and description on every page</td>
<td><code>generateMetadata</code></td>
</tr>
<tr>
<td>3</td>
<td>One canonical host, the rest redirected</td>
<td><code>alternates.canonical</code>, <code>redirects()</code></td>
</tr>
<tr>
<td>4</td>
<td>Sitemap with accurate <code>lastModified</code></td>
<td><code>app/sitemap.ts</code></td>
</tr>
<tr>
<td>5</td>
<td>Search and AI crawlers allowed</td>
<td><code>app/robots.ts</code>, CDN or firewall rules</td>
</tr>
<tr>
<td>6</td>
<td>JSON-LD, validated</td>
<td>Server component script tag</td>
</tr>
<tr>
<td>7</td>
<td>One real H1 per page</td>
<td>Your components</td>
</tr>
<tr>
<td>8</td>
<td>Key text in the server HTML</td>
<td>The <code>curl</code> test</td>
</tr>
<tr>
<td>9</td>
<td>Open Graph image per page</td>
<td><code>opengraph-image.tsx</code></td>
</tr>
<tr>
<td>10</td>
<td>Good Core Web Vitals</td>
<td>PageSpeed Insights, Search Console</td>
</tr>
<tr>
<td>11</td>
<td>Real 404s, no redirect chains</td>
<td><code>not-found.tsx</code>, <code>notFound()</code></td>
</tr>
<tr>
<td>12</td>
<td>Search Console and Bing set up</td>
<td>Their dashboards</td>
</tr>
</tbody></table>
<h2>FAQ</h2>
<h3>Is Next.js good for SEO?</h3>
<p>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.</p>
<h3>Do I still need a package like next-seo?</h3>
<p>Not with the App Router. The built-in Metadata API covers what <code>next-seo</code> used to do for the Pages Router.</p>
<h3>How long until a new Next.js site ranks?</h3>
<p>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.</p>
<hr />
<p>Want this checklist run on your site, with the fixes done for you? <a href="https://www.vishalsolanki.online/#contact">That's what I do</a>.</p>
<p><em>Originally published on <a href="https://www.vishalsolanki.online/blog/nextjs-seo-checklist">vishalsolanki.online</a>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Next.js as a backend: Server Actions, Route Handlers or a separate Node.js API?]]></title><description><![CDATA[Do you need Express or NestJS next to your Next.js app? What each Next.js server feature is for, when a separate Node.js API is worth it, and a decision…]]></description><link>https://vishalsolanki.hashnode.dev/nextjs-backend-server-actions-route-handlers-or-nodejs-api</link><guid isPermaLink="true">https://vishalsolanki.hashnode.dev/nextjs-backend-server-actions-route-handlers-or-nodejs-api</guid><category><![CDATA[Next.js]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[backend]]></category><category><![CDATA[architecture]]></category><dc:creator><![CDATA[Vishal Solanki]]></dc:creator><pubDate>Tue, 27 Oct 2026 19:03:16 GMT</pubDate><content:encoded><![CDATA[<p>"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: <strong>not yet.</strong> 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.</p>
<p>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.</p>
<h2>The four server tools in Next.js</h2>
<table>
<thead>
<tr>
<th>Tool</th>
<th>What it's for</th>
<th>Who calls it</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Server Components</strong></td>
<td>Reading data and rendering it to HTML</td>
<td>Your own pages, during render</td>
</tr>
<tr>
<td><strong>Server Actions</strong></td>
<td>Mutations from your own UI: forms, buttons</td>
<td>Your own client code, over POST</td>
</tr>
<tr>
<td><strong>Route Handlers</strong> (<code>route.ts</code>)</td>
<td>Public HTTP endpoints that return anything: JSON, files, XML</td>
<td>Anyone: webhooks, mobile apps, partners, other services</td>
</tr>
<tr>
<td><strong>Proxy</strong> (<code>proxy.ts</code>)</td>
<td>Code that runs before a request: redirects, rewrites, headers</td>
<td>Every matching request</td>
</tr>
</tbody></table>
<p>In Next.js 16, Middleware was renamed to <strong>Proxy</strong>. It does the same job; the new name says more clearly what it's for.</p>
<h2>Reading data: go straight to the source</h2>
<p>In a Server Component, read from the database or the upstream service directly, ideally through one data access layer:</p>
<pre><code class="language-tsx">// app/orders/page.tsx
import { getOrdersFor } from "@/lib/data/orders";
import { getSession } from "@/lib/auth";

export default async function Orders() {
  const session = await getSession();
  const orders = await getOrdersFor(session.userId);
  return &lt;OrderList orders={orders} /&gt;;
}
</code></pre>
<p>A common mistake is calling your own Route Handler from a Server Component, like <code>fetch("/api/orders")</code>. 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.</p>
<h2>Mutations from your own UI: Server Actions</h2>
<p>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.</p>
<p>Two things to know:</p>
<ul>
<li><strong>They're public POST endpoints.</strong> Check the session, ownership and input inside every action. My <a href="https://www.vishalsolanki.online/blog/secure-nextjs-server-actions">Server Actions security checklist</a> covers this step by step.</li>
<li><strong>They're queued.</strong> Actions from the same client run one after another. That's right for mutations, but it makes them a poor choice for fetching data.</li>
</ul>
<h2>Public endpoints: Route Handlers</h2>
<p>Reach for a Route Handler when something <strong>other than your own UI</strong> needs to talk to your app:</p>
<ul>
<li><strong>Webhooks:</strong> payment providers, CRMs and messaging tools post events to a URL you give them.</li>
<li><strong>Other clients:</strong> a mobile app, a partner integration or an internal tool that needs JSON.</li>
<li><strong>OAuth and payment callbacks</strong>, where a third party redirects the user back to you.</li>
<li><strong>Non-HTML responses:</strong> RSS, CSV exports, generated files.</li>
</ul>
<pre><code class="language-ts">// app/api/webhooks/payments/route.ts
export async function POST(request: Request) {
  const body = await request.text();
  const signature = request.headers.get("x-signature");
  if (!verifySignature(body, signature)) return new Response("Invalid signature", { status: 401 });

  const event = JSON.parse(body);
  await handlePaymentEvent(event); // idempotent: the provider may retry
  return new Response(null, { status: 204 });
}
</code></pre>
<p>Treat every Route Handler like any public API: verify signatures or tokens, validate the payload, rate-limit, and never put a mutation behind GET.</p>
<h2>Before the request: Proxy</h2>
<p>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 <strong>not</strong> 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.</p>
<h2>When a separate Node.js API is worth it</h2>
<p>Next.js on its own starts to strain when one or more of these is true:</p>
<ol>
<li><strong>Several clients share the same business logic.</strong> 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.</li>
<li><strong>You have long-running or background work.</strong> 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.</li>
<li><strong>You need persistent connections.</strong> 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.</li>
<li><strong>The work is CPU-heavy.</strong> Image or video processing and heavy calculations shouldn't slow down page rendering.</li>
<li><strong>Teams or scaling are separate.</strong> A dedicated backend team, separate deploys, or very different traffic patterns between the website and the API.</li>
<li><strong>Compliance requires it.</strong> Some industries need sensitive processing in a specific network, region or audited service.</li>
</ol>
<p>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.</p>
<h2>The middle path: Next.js as the backend for your frontend</h2>
<p>You don't have to choose all or nothing. A common, solid setup:</p>
<ul>
<li><strong>Core services</strong> (Node.js with Express or NestJS, or anything else) own the business logic, the database and the background jobs.</li>
<li><strong>Next.js</strong> 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.</li>
<li><strong>The browser never talks to the core services directly.</strong> Tokens and service credentials stay on the Next.js server, and the user's session lives in an <code>httpOnly</code> cookie.</li>
</ul>
<p>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.</p>
<h2>Keep one data access layer either way</h2>
<p>Whichever setup you choose, put data access behind one <code>server-only</code> module and call it from Server Components, Server Actions and Route Handlers alike:</p>
<pre><code class="language-txt">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
</code></pre>
<p>If you later move to a separate API, only <code>lib/data</code> changes: it calls the API instead of the database. The pages, actions and handlers stay as they are.</p>
<h2>A decision checklist</h2>
<table>
<thead>
<tr>
<th>Question</th>
<th>If yes</th>
</tr>
</thead>
<tbody><tr>
<td>Is the only client your Next.js web app?</td>
<td>Start with Next.js alone</td>
</tr>
<tr>
<td>Will a mobile app or partners need the same logic soon?</td>
<td>Plan a standalone API, or at least a clean data layer you can move</td>
</tr>
<tr>
<td>Any jobs that run longer than a normal request?</td>
<td>Add a worker or queue service</td>
</tr>
<tr>
<td>Real-time features (chat, live updates)?</td>
<td>Add a WebSocket server or a managed real-time service</td>
</tr>
<tr>
<td>Heavy CPU work?</td>
<td>Separate service</td>
</tr>
<tr>
<td>Separate backend team or very different scaling needs?</td>
<td>Separate API</td>
</tr>
<tr>
<td>None of the above?</td>
<td>Next.js alone; revisit when one becomes true</td>
</tr>
</tbody></table>
<h2>FAQ</h2>
<h3>Can Next.js replace Express?</h3>
<p>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.</p>
<h3>Should I build REST endpoints for my own frontend?</h3>
<p>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.</p>
<h3>Is a Next.js backend production-ready?</h3>
<p>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.</p>
<hr />
<p>Planning a new product, or untangling an existing one? I design and build both sides, from interface to API. <a href="https://www.vishalsolanki.online/#contact">Tell me what you're building</a>.</p>
<p><em>Originally published on <a href="https://www.vishalsolanki.online/blog/nextjs-backend-server-actions-route-handlers-or-nodejs-api">vishalsolanki.online</a>.</em></p>
]]></content:encoded></item><item><title><![CDATA[How to get your website cited by ChatGPT, Perplexity and Google AI Mode]]></title><description><![CDATA[What decides whether AI assistants mention and link your site in 2026, and six steps that work for small businesses and freelancers: crawler access, Bing…]]></description><link>https://vishalsolanki.hashnode.dev/get-cited-by-chatgpt-perplexity-ai-search</link><guid isPermaLink="true">https://vishalsolanki.hashnode.dev/get-cited-by-chatgpt-perplexity-ai-search</guid><category><![CDATA[SEO]]></category><category><![CDATA[AI]]></category><category><![CDATA[chatgpt]]></category><category><![CDATA[marketing]]></category><dc:creator><![CDATA[Vishal Solanki]]></dc:creator><pubDate>Tue, 27 Oct 2026 19:03:05 GMT</pubDate><content:encoded><![CDATA[<p>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.</p>
<p>This is often called <strong>GEO</strong> (generative engine optimisation) or <strong>AEO</strong> (answer engine optimisation). Most of it is ordinary good SEO, plus a few things that now matter more than they used to.</p>
<blockquote>
<p><strong>An honest caveat:</strong> 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.</p>
</blockquote>
<h2>Where each assistant gets its answers</h2>
<table>
<thead>
<tr>
<th>Assistant</th>
<th>Where live answers come from</th>
<th>What that means for you</th>
</tr>
</thead>
<tbody><tr>
<td>Google AI Overviews and AI Mode</td>
<td>Google's own index (Googlebot)</td>
<td>Normal Google SEO. The page must be indexed and allowed in snippets.</td>
</tr>
<tr>
<td>Gemini app</td>
<td>Google Search</td>
<td>Same as above</td>
</tr>
<tr>
<td>ChatGPT search</td>
<td>OpenAI's crawler (<code>OAI-SearchBot</code>) plus Bing results</td>
<td>Allow <code>OAI-SearchBot</code> and get indexed in Bing</td>
</tr>
<tr>
<td>Microsoft Copilot</td>
<td>Bing</td>
<td>Bing Webmaster Tools and Bing Places</td>
</tr>
<tr>
<td>Perplexity</td>
<td>Its own crawler (<code>PerplexityBot</code>) and other indexes</td>
<td>Fresh, well-structured pages it can reach</td>
</tr>
<tr>
<td>Claude</td>
<td>Web search plus Anthropic's crawlers (<code>Claude-SearchBot</code>)</td>
<td>Being findable and mentioned on trusted sites</td>
</tr>
</tbody></table>
<p>When an assistant answers <strong>without</strong> 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.</p>
<h2>Step 1: Make sure the crawlers can reach you</h2>
<p>Start with <code>robots.txt</code>. "Allow everyone" is the simplest setting that keeps you visible:</p>
<pre><code class="language-txt">User-Agent: *
Allow: /

Sitemap: https://www.example.com/sitemap.xml
</code></pre>
<p>If you ever add rules, know which bot does what:</p>
<table>
<thead>
<tr>
<th>Bot</th>
<th>Owner</th>
<th>Used for</th>
</tr>
</thead>
<tbody><tr>
<td><code>Googlebot</code></td>
<td>Google</td>
<td>Search, including AI Overviews and AI Mode</td>
</tr>
<tr>
<td><code>Google-Extended</code></td>
<td>Google</td>
<td>Gemini training and grounding outside Search. Blocking it does not remove you from AI Overviews.</td>
</tr>
<tr>
<td><code>bingbot</code></td>
<td>Microsoft</td>
<td>Bing and Copilot</td>
</tr>
<tr>
<td><code>OAI-SearchBot</code></td>
<td>OpenAI</td>
<td>Showing sites in ChatGPT search</td>
</tr>
<tr>
<td><code>GPTBot</code></td>
<td>OpenAI</td>
<td>Model training</td>
</tr>
<tr>
<td><code>PerplexityBot</code></td>
<td>Perplexity</td>
<td>Perplexity's search index</td>
</tr>
<tr>
<td><code>ClaudeBot</code>, <code>Claude-SearchBot</code></td>
<td>Anthropic</td>
<td>Training, and search</td>
</tr>
</tbody></table>
<p>Then check the layer <code>robots.txt</code> can't see: your CDN or hosting firewall. Some bot-protection presets block AI crawlers by default. Test it directly:</p>
<pre><code class="language-bash">curl -s -o /dev/null -w "%{http_code}\n" \
  -A "Mozilla/5.0 (compatible; OAI-SearchBot/1.0; +https://openai.com/searchbot)" \
  https://www.example.com/
</code></pre>
<p>You want <code>200</code>. A <code>403</code> means something in front of your site is turning the crawler away.</p>
<p>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 <a href="https://www.vishalsolanki.online/blog/nextjs-seo-checklist">Next.js SEO checklist</a> has a one-line test for this.</p>
<h2>Step 2: Get indexed by Bing, not just Google</h2>
<p>Bing is easy to forget, and it feeds Microsoft Copilot and part of ChatGPT search.</p>
<ol>
<li>Open Bing Webmaster Tools and import your site from Google Search Console. It takes one click and needs no extra verification.</li>
<li>Confirm your sitemap is listed.</li>
<li>Turn on <strong>IndexNow</strong> so Bing hears about new and updated pages within minutes instead of waiting for a crawl.</li>
</ol>
<h2>Step 3: Write pages an AI can quote</h2>
<p>Assistants build answers out of short, self-contained passages. Pages that get cited tend to make that easy:</p>
<ul>
<li><strong>Answer first.</strong> Put the direct answer in the first one or two sentences under each heading, then explain.</li>
<li><strong>Use question headings</strong> that match how people ask: "How much does a Next.js website cost in India?"</li>
<li><strong>Be specific.</strong> Numbers, ranges, timelines, versions and dates are quotable. Vague reassurance isn't.</li>
<li><strong>Use tables and lists</strong> for comparisons and steps.</li>
<li><strong>Show who wrote it and when</strong>, with an author line, a publish date and "updated on" when you revise.</li>
<li><strong>Keep it current.</strong> Refresh your most important pages at least twice a year.</li>
</ul>
<p>Here's the difference in practice.</p>
<p><strong>Hard to quote:</strong></p>
<blockquote>
<p>Pricing depends on many factors, so every project is different. Get in touch for a quote tailored to your needs.</p>
</blockquote>
<p><strong>Easy to quote:</strong></p>
<blockquote>
<p>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.</p>
</blockquote>
<p>The second version answers a real question in a way an assistant can repeat and attribute.</p>
<h2>Step 4: Be one clear identity</h2>
<p>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:</p>
<ul>
<li>Use the <strong>same one-line description</strong> on your site, LinkedIn, GitHub, Google Business Profile and directory listings.</li>
<li>Add <strong><code>sameAs</code> links</strong> to your <code>Person</code> or <code>Organization</code> structured data, pointing at those profiles.</li>
<li>Have an <strong>About page</strong> that states the facts plainly: what you do, where, for how long, for whom.</li>
<li><strong>Pick two or three specialities and repeat them.</strong> "The Next.js performance person" is easier for an assistant to recommend than "does everything".</li>
</ul>
<h2>Step 5: Get mentioned where AI looks</h2>
<p>This is the step most people skip, and it's the one that moves the needle most. Industry studies in 2026 keep finding that <strong>mentions of a brand on other trusted sites</strong> line up with AI citations more closely than backlinks alone, and that many cited pages aren't in Google's top ten at all.</p>
<p>Places worth your time:</p>
<ul>
<li><strong>"Best of" lists and directories:</strong> Clutch, GoodFirms and similar sites rank for "best developers in [city]" and get quoted by assistants. Get listed, then get reviews.</li>
<li><strong>Reddit and Quora:</strong> genuinely helpful answers in the communities your clients and peers use. Mention your work only when it actually answers the question.</li>
<li><strong>GitHub:</strong> useful public repositories with your name and site in the README.</li>
<li><strong>LinkedIn:</strong> posts and articles are public and indexed.</li>
<li><strong>Stack Overflow:</strong> good answers in your speciality.</li>
<li><strong>Press:</strong> journalist-request platforms such as Qwoted and Featured.com. One quote in a real publication outweighs dozens of directory links.</li>
<li><strong>YouTube:</strong> short walkthroughs. Transcripts are indexed and quoted.</li>
</ul>
<h2>Step 6: Measure it</h2>
<p>There's no single "AI rank" to check, so measure it three ways.</p>
<p><strong>A monthly prompt check.</strong> On the first of each month, ask the same questions in ChatGPT (with search on), Perplexity, Gemini, Copilot and Claude, and log the answers:</p>
<table>
<thead>
<tr>
<th>Prompt</th>
<th>Mentioned?</th>
<th>Linked?</th>
<th>Who's named instead?</th>
</tr>
</thead>
<tbody><tr>
<td>Who is [your name], the [role] from [city]?</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Recommend a freelance [speciality] developer in [city]</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>How much does a [service] cost in [country]?</td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody></table>
<p>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.</p>
<p><strong>Referral traffic.</strong> In Google Analytics, look for sessions from <code>chatgpt.com</code>, <code>perplexity.ai</code>, <code>gemini.google.com</code>, <code>copilot.microsoft.com</code> and <code>claude.ai</code>. ChatGPT adds <code>utm_source=chatgpt.com</code> to many of its links. Group them into one "AI assistants" channel.</p>
<p><strong>Ask your leads.</strong> 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.</p>
<p>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.</p>
<h2>What about llms.txt?</h2>
<p><code>llms.txt</code> 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.</p>
<h2>What doesn't work</h2>
<ul>
<li>Blocks of "AI-friendly" keywords stuffed into a page.</li>
<li>Hundreds of thin, auto-generated pages. Google calls this scaled content abuse.</li>
<li>Text hidden from visitors but aimed at bots.</li>
<li>Fake reviews and fake listings.</li>
</ul>
<p>All of these risk your normal search rankings too, and assistants increasingly lean on the same quality signals.</p>
<h2>FAQ</h2>
<h3>If I block GPTBot, do I disappear from ChatGPT search?</h3>
<p>No. OpenAI uses separate crawlers: <code>GPTBot</code> collects training data, and <code>OAI-SearchBot</code> is what surfaces sites in ChatGPT search. You can block one and allow the other.</p>
<h3>Does blocking Google-Extended remove me from AI Overviews?</h3>
<p>No. AI Overviews and AI Mode are part of Google Search and use Googlebot. <code>Google-Extended</code> only controls use of your content for Gemini outside Search.</p>
<h3>How long does this take?</h3>
<p>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.</p>
<hr />
<p>I build sites that are fast, findable in search and easy for AI assistants to quote. <a href="https://www.vishalsolanki.online/#contact">Tell me about yours</a>.</p>
<p><em>Originally published on <a href="https://www.vishalsolanki.online/blog/get-cited-by-chatgpt-perplexity-ai-search">vishalsolanki.online</a>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Prompt injection in AI coding agents: how one CLAUDE.md or AGENTS.md can hijack them]]></title><description><![CDATA[New research shows a planted AGENTS.md or CLAUDE.md can make Codex and Claude Code install malware, rewrite their own config and leak tokens. How the…]]></description><link>https://vishalsolanki.hashnode.dev/ai-coding-agent-prompt-injection-claude-md-agents-md</link><guid isPermaLink="true">https://vishalsolanki.hashnode.dev/ai-coding-agent-prompt-injection-claude-md-agents-md</guid><category><![CDATA[AI]]></category><category><![CDATA[Security]]></category><category><![CDATA[claude-code]]></category><category><![CDATA[prompt injection ]]></category><dc:creator><![CDATA[Vishal Solanki]]></dc:creator><pubDate>Tue, 27 Oct 2026 19:02:53 GMT</pubDate><content:encoded><![CDATA[<p>AI coding agents read instruction files before they do anything else. Codex reads <code>AGENTS.md</code>. Claude Code reads <code>CLAUDE.md</code>. That's how you tell them your test command, your code style and what not to touch.</p>
<p>It's also an attack surface. On October 26, 2026, CSO Online reported research from Forever Security showing that <strong>a single Markdown file in the wrong folder can turn your agent against your own machine.</strong> No exploit and no executable code: just text the agent treats as instructions and runs with the permissions you already gave it.</p>
<p>Here's what happened, why it works, and what I do about it.</p>
<h2>What the researchers found</h2>
<p>Forever Security found a malicious <code>AGENTS.md</code> in a customer's Downloads folder. It told Codex to install an npm package called <code>function-color</code>, which was malware. The install was blocked before it ran.</p>
<p>They then tested the same idea on purpose:</p>
<table>
<thead>
<tr>
<th>Agent</th>
<th>What the planted file did</th>
<th>How often it worked</th>
</tr>
</thead>
<tbody><tr>
<td>Codex (<code>AGENTS.md</code>)</td>
<td>Edited the machine-wide MCP server config and launched a new session with approvals and the sandbox turned off</td>
<td>More than 90% of tests</td>
</tr>
<tr>
<td>Claude Code (<code>CLAUDE.md</code>)</td>
<td>Redirected the agent's API traffic to a server the researchers controlled, exposing auth tokens and letting that server send back commands</td>
<td>About half of tests</td>
</tr>
</tbody></table>
<p>Two details make this worse than it sounds:</p>
<ul>
<li><strong>It persists.</strong> The malicious MCP server went into Codex's machine-wide config, so it loaded again in later sessions, even in other projects.</li>
<li><strong>Stolen tokens work.</strong> The researchers replayed a captured OAuth token against the real API. That's an account takeover, not just one bad session.</li>
</ul>
<p>They also showed that a malicious website could trigger a download like this without an extra approval prompt.</p>
<p>To be fair to the vendors: an IDC analyst quoted in the article called this a credible early warning rather than evidence of widespread exploitation, and the success rates haven't been independently checked. But the pattern is real, and it isn't limited to Downloads.</p>
<h2>Why this works</h2>
<p>An agent can't tell the difference between "instructions from my user" and "text I found in a file". If a file says "before you start, run <code>npm i some-package</code>", a helpful agent will try.</p>
<p>One analyst put it neatly: these tools treat <strong>following an instruction</strong> and <strong>granting a privilege</strong> as the same act. The file doesn't need permissions of its own. It borrows yours.</p>
<p>Anywhere an agent reads untrusted text is a way in:</p>
<ul>
<li>A cloned repo's <code>CLAUDE.md</code>, <code>AGENTS.md</code>, <code>.cursor/rules</code> or README</li>
<li>A downloaded zip or "starter template"</li>
<li>A package's own files inside <code>node_modules</code></li>
<li>Web pages, issues and PR comments the agent fetches</li>
<li>Tool and MCP server responses</li>
</ul>
<h2>8 defences that actually help</h2>
<h3>1. Never run an agent from Downloads or your home folder</h3>
<p>Claude Code loads <code>CLAUDE.md</code> from the folder you start it in and the folders above it. Codex does the same with <code>AGENTS.md</code> in trusted folders. Start agents only inside a project folder you created or reviewed, never in <code>~/Downloads</code>, <code>~</code> or a freshly unzipped archive.</p>
<h3>2. Read instruction files before you trust a repo</h3>
<p>Before you run an agent in a repo you just cloned, open every instruction file:</p>
<pre><code class="language-bash">find . -maxdepth 3 \( -name "CLAUDE.md" -o -name "AGENTS.md" -o -name "GEMINI.md" -o -path "*/.cursor/rules/*" \) -not -path "./node_modules/*"
</code></pre>
<p>Look for anything that installs packages, changes config, touches <code>~/.claude</code>, <code>~/.codex</code> or MCP settings, sets environment variables or points at an unfamiliar URL. A real project's instructions describe the project. They don't need to reconfigure your machine.</p>
<h3>3. Treat instruction files like code</h3>
<p>In your own repos, put <code>CLAUDE.md</code>, <code>AGENTS.md</code> and agent config folders under code review. A <code>CODEOWNERS</code> entry is enough:</p>
<pre><code class="language-text">CLAUDE.md          @your-team/leads
AGENTS.md          @your-team/leads
.claude/           @your-team/leads
.codex/            @your-team/leads
</code></pre>
<h3>4. Keep approvals and the sandbox on</h3>
<p>The whole point of the Codex attack was to start a session with <code>--dangerously-bypass-approvals-and-sandbox</code>. Claude Code's equivalent is <code>--dangerously-skip-permissions</code>. Don't make either a habit, and never alias them. If something keeps asking for approval, allow that one command in your settings instead of turning approvals off.</p>
<h3>5. Deny what an agent should never touch</h3>
<p>In Claude Code, project or user <code>settings.json</code> can deny reads and commands outright. I deny secrets and the agent's own config:</p>
<pre><code class="language-json">{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(~/.ssh/**)",
      "Edit(~/.claude/**)",
      "Bash(curl:*)"
    ]
  }
}
</code></pre>
<p>An instruction file can change what the agent <em>tries</em>. Deny rules decide what it's <em>allowed</em> to do.</p>
<h3>6. Slow down new packages</h3>
<p>The first move in the real attack was <code>npm install</code>. Agents install dependencies without a human looking, which is exactly what install-time malware relies on. Two practical guards:</p>
<ul>
<li><strong>A cooldown.</strong> pnpm's <code>minimumReleaseAge</code> setting refuses versions newer than the age you set, so a package published an hour ago can't be pulled in. Many malicious versions are caught and pulled within hours or days.</li>
<li><strong>A package firewall.</strong> GitHub launched one in public preview on October 20, 2026 for Enterprise Cloud. It sits between agents and the npm registry and blocks packages with malware advisories or that are inside a cooldown window.</li>
</ul>
<h3>7. Keep real credentials off the agent's machine</h3>
<p>If an agent can read a production key, so can anything that hijacks it. Use separate, short-lived, low-scope keys for development. Keep production secrets in your host's environment settings, not in a local <code>.env</code>. And turn on secret scanning: since October 13, 2026, GitHub push protection blocks Anthropic API keys by default on repos with secret scanning enabled, including free public ones.</p>
<h3>8. Watch for config changes</h3>
<p>Check what MCP servers your agents load every now and then (<code>claude mcp list</code> for Claude Code, <code>~/.codex/config.toml</code> for Codex). If one appears that you didn't add, remove it, rotate your tokens and look for the file that put it there.</p>
<h2>Quick checklist</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>Check</th>
<th>If not</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>Do you start agents only inside known project folders?</td>
<td>Stop running them from Downloads or <code>~</code></td>
</tr>
<tr>
<td>2</td>
<td>Did you read the instruction files in that cloned repo?</td>
<td>Run the <code>find</code> command above first</td>
</tr>
<tr>
<td>3</td>
<td>Are <code>CLAUDE.md</code> / <code>AGENTS.md</code> changes reviewed?</td>
<td>Add them to <code>CODEOWNERS</code></td>
</tr>
<tr>
<td>4</td>
<td>Are approvals and the sandbox on?</td>
<td>Drop the <code>--dangerously-*</code> flags</td>
</tr>
<tr>
<td>5</td>
<td>Are secrets and agent config denied?</td>
<td>Add deny rules to <code>settings.json</code></td>
</tr>
<tr>
<td>6</td>
<td>Is there a cooldown on new package versions?</td>
<td>Set <code>minimumReleaseAge</code> or use a firewall</td>
</tr>
<tr>
<td>7</td>
<td>Can the agent see production keys?</td>
<td>Move them to your host's env settings</td>
</tr>
<tr>
<td>8</td>
<td>Do you know which MCP servers load?</td>
<td>List them and remove strangers</td>
</tr>
</tbody></table>
<h2>FAQ</h2>
<h3>Is Claude Code or Codex unsafe to use?</h3>
<p>No. Both ask for approval before risky actions, and Codex runs sandboxed by default (Claude Code has an opt-in sandbox too). The attacks above worked by talking the agent into loosening those settings, or by catching people who had already turned them off. Keep the guard rails on and you remove most of the risk.</p>
<h3>Does this affect Cursor, Copilot and Gemini CLI too?</h3>
<p>The research tested Codex and Claude Code, but every agent that reads project instruction files or fetched content has the same basic weakness. Cursor rules, Copilot instructions and <code>GEMINI.md</code> all deserve the same care.</p>
<h3>What should I do if I think I ran an agent on a malicious file?</h3>
<p>Check your agents' MCP config and settings for anything you didn't add, remove it, then rotate every token and API key that machine could read. Review recent shell history and installed global packages for anything unfamiliar.</p>
<hr />
<p>Using AI agents on a codebase with real users and real keys? I review setups like this and lock them down. <a href="https://www.vishalsolanki.online/#contact">Get in touch</a>. For the web app side, see my <a href="https://www.vishalsolanki.online/blog/secure-nextjs-server-actions">Server Actions security checklist</a>.</p>
<p>Source: <a href="https://www.csoonline.com/article/4240222/malicious-downloads-can-trick-ai-coding-agents-into-compromising-themselves.html">CSO Online, "Malicious downloads can trick AI coding agents into compromising themselves"</a>, October 26, 2026. GitHub changelog: <a href="https://github.blog/changelog/2026-10-13-secret-scanning-blocks-anthropic-keys-by-default-adds-salesforce/">secret scanning blocks Anthropic keys by default</a> and <a href="https://github.blog/changelog/2026-10-20-github-package-firewall-in-public-preview/">package firewall in public preview</a>.</p>
<p><em>Originally published on <a href="https://www.vishalsolanki.online/blog/ai-coding-agent-prompt-injection-claude-md-agents-md">vishalsolanki.online</a>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Core Web Vitals in 2026: fixing LCP, INP and CLS in Next.js]]></title><description><![CDATA[The three metrics Google measures, what counts as good, and the fixes that move them on a real Next.js site: images, fonts, JavaScript, long tasks and…]]></description><link>https://vishalsolanki.hashnode.dev/core-web-vitals-nextjs-lcp-inp-cls</link><guid isPermaLink="true">https://vishalsolanki.hashnode.dev/core-web-vitals-nextjs-lcp-inp-cls</guid><category><![CDATA[Next.js]]></category><category><![CDATA[web performance]]></category><category><![CDATA[SEO]]></category><category><![CDATA[performance]]></category><dc:creator><![CDATA[Vishal Solanki]]></dc:creator><pubDate>Tue, 27 Oct 2026 19:01:58 GMT</pubDate><content:encoded><![CDATA[<p>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.</p>
<p>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.</p>
<h2>The three metrics, and what "good" means</h2>
<table>
<thead>
<tr>
<th>Metric</th>
<th>What it measures</th>
<th>Good</th>
<th>Poor</th>
</tr>
</thead>
<tbody><tr>
<td><strong>LCP</strong>, Largest Contentful Paint</td>
<td>How long until the biggest thing on screen (usually a hero image or headline) appears</td>
<td>2.5 s or less</td>
<td>over 4 s</td>
</tr>
<tr>
<td><strong>INP</strong>, Interaction to Next Paint</td>
<td>How long the page takes to visibly respond after a tap, click or key press</td>
<td>200 ms or less</td>
<td>over 500 ms</td>
</tr>
<tr>
<td><strong>CLS</strong>, Cumulative Layout Shift</td>
<td>How much the layout jumps around while loading</td>
<td>0.1 or less</td>
<td>over 0.25</td>
</tr>
</tbody></table>
<p>Google judges each one at the <strong>75th percentile of real visits</strong>, 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.</p>
<h3>Field data versus lab data</h3>
<p>PageSpeed Insights shows two different things on one page:</p>
<ul>
<li><strong>The top section is field data</strong>: real Chrome users over the last 28 days (the Chrome UX Report). This is what Google uses.</li>
<li><strong>The Lighthouse score underneath is lab data</strong>: one simulated load on a throttled device. It's great for debugging, varies from run to run, and is not what ranks you.</li>
</ul>
<p>A Lighthouse 100 with failing field data is still failing. A Lighthouse 70 with green field data is fine.</p>
<h2>Measure first</h2>
<p>Guessing wastes days. Find out which metric fails, on which pages, before you change anything.</p>
<ol>
<li><strong>Search Console → Core Web Vitals</strong> groups your URLs by problem.</li>
<li><strong>PageSpeed Insights</strong> names the LCP element and the worst layout shifts on a given page.</li>
<li><strong>Chrome DevTools → Performance</strong> shows the LCP element, each interaction's INP and every layout shift, live, as you use the page.</li>
<li><strong>Your own real-user data</strong>, sent to analytics from a small client component:</li>
</ol>
<pre><code class="language-tsx">"use client";
import { useReportWebVitals } from "next/web-vitals";

export function WebVitals() {
  useReportWebVitals((metric) =&gt; {
    // Send LCP, INP and CLS to your analytics, e.g. as a GA4 event or to your own endpoint.
    window.dataLayer?.push({ event: "web_vitals", name: metric.name, value: metric.value, rating: metric.rating });
  });
  return null;
}
</code></pre>
<h2>Fixing LCP</h2>
<p><strong>1. Find out what the LCP element is.</strong> PageSpeed Insights tells you. The fix is completely different for an image than for a heading.</p>
<p><strong>2. If it's an image,</strong> serve it with <code>next/image</code>, tell the browser to fetch it early, and give it accurate <code>sizes</code>:</p>
<pre><code class="language-tsx">import Image from "next/image";

&lt;Image
  src="/hero.jpg"
  alt="The living room at dusk"
  width={1600}
  height={900}
  sizes="100vw"
  preload
/&gt;
</code></pre>
<p>In Next.js 16, <code>preload</code> replaced the old <code>priority</code> prop. Use it on one image per page, the LCP one. Enable modern formats once in <code>next.config.ts</code> with <code>images: { formats: ["image/avif", "image/webp"] }</code>.</p>
<p><strong>3. If it's text,</strong> the font is usually the bottleneck. <code>next/font</code> self-hosts and preloads it. Keep only the weights and subsets you actually use.</p>
<p><strong>4. Don't hide the LCP element behind an intro animation.</strong> Text that starts at <code>opacity: 0</code> 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.</p>
<p><strong>5. Check the server response.</strong> Statically generated pages served from a CDN are fastest. If time to first byte is slow, everything after it is slow too.</p>
<h2>Fixing INP</h2>
<p>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.</p>
<p><strong>1. Ship less JavaScript.</strong> Keep components on the server unless they need state, effects or browser APIs. Push <code>"use client"</code> 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.</p>
<p><strong>2. Show feedback first, then do the heavy work.</strong> Break long tasks up so the browser can paint in between:</p>
<pre><code class="language-ts">// Let the browser paint before continuing a long task.
const yieldToMain = () =&gt;
  globalThis.scheduler?.yield?.() ?? new Promise((resolve) =&gt; setTimeout(resolve, 0));

async function onApplyFilters() {
  setLoading(true); // visible feedback, painted right away
  await yieldToMain();
  const results = expensiveFilter(allItems); // the slow part
  setItems(results);
}
</code></pre>
<p><strong>3. Mark expensive re-renders as non-urgent.</strong> For things like filtering a long list as someone types, wrap the update in <code>startTransition</code> (from <code>useTransition</code>) or render from <code>useDeferredValue</code>, so typing stays responsive while the list catches up.</p>
<p><strong>4. Keep animation off the main thread where you can.</strong> Animate <code>transform</code> and <code>opacity</code> rather than <code>top</code>, <code>width</code> or <code>height</code>. Don't read layout (<code>getBoundingClientRect</code>, <code>offsetHeight</code>) inside scroll handlers. Load heavy animation code only for the sections that use it, and honour <code>prefers-reduced-motion</code>.</p>
<p><strong>5. Load third-party scripts late.</strong> 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.</p>
<h2>Fixing CLS</h2>
<p><strong>1. Reserve space for everything that loads later.</strong> Give images <code>width</code> and <code>height</code>. Give embeds, videos and ads a fixed <code>aspect-ratio</code> box.</p>
<p><strong>2. Stop fonts from shifting text.</strong> <code>next/font</code> 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 <code>display: "optional"</code>: on a slow first visit the fallback simply stays, rather than swapping mid-read. This site uses that for its body font.</p>
<p><strong>3. Never insert content above what's already on screen.</strong> Cookie bars, promo banners and "related" blocks should overlay the page or have space reserved for them.</p>
<p><strong>4. Animate with <code>transform</code>.</strong> Changing <code>height</code>, <code>margin</code> or <code>top</code> moves everything below. A <code>transform</code> or an <code>opacity</code> fade doesn't.</p>
<h2>A 30-minute audit routine</h2>
<ol>
<li>Open Search Console → Core Web Vitals. Note which metric fails, on mobile or desktop, and which URL groups.</li>
<li>Run PageSpeed Insights on one URL from each failing group. Write down the LCP element and the largest layout shifts.</li>
<li>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.</li>
<li>In the trace, look for long tasks (over 50 ms) right after load and right after each tap. Note what code they belong to.</li>
<li>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.</li>
</ol>
<h2>FAQ</h2>
<h3>Do Core Web Vitals affect Google rankings?</h3>
<p>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.</p>
<h3>Why does PageSpeed Insights give a different score every time?</h3>
<p>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.</p>
<h3>Do I need a Lighthouse score of 100?</h3>
<p>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.</p>
<hr />
<p>Is your site slow on phones, or failing Core Web Vitals in Search Console? <a href="https://www.vishalsolanki.online/#contact">I find the cause and fix it</a>.</p>
<p><em>Originally published on <a href="https://www.vishalsolanki.online/blog/core-web-vitals-nextjs-lcp-inp-cls">vishalsolanki.online</a>.</em></p>
]]></content:encoded></item></channel></rss>