<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Muhammad Zeeshan Sardar</title>
    <description>The latest articles on DEV Community by Muhammad Zeeshan Sardar (@zeeshansardar08).</description>
    <link>https://dev.to/zeeshansardar08</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3815588%2F642e7ea5-a321-4d5b-abf8-d4a1c64a51b7.png</url>
      <title>DEV Community: Muhammad Zeeshan Sardar</title>
      <link>https://dev.to/zeeshansardar08</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zeeshansardar08"/>
    <language>en</language>
    <item>
      <title>Next.js 15 Support Ends Oct 21: The Upgrade Changes Your Build Won't Catch</title>
      <dc:creator>Muhammad Zeeshan Sardar</dc:creator>
      <pubDate>Wed, 14 Oct 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/zeeshansardar08/nextjs-15-support-ends-oct-21-the-upgrade-changes-your-build-wont-catch-38bj</link>
      <guid>https://dev.to/zeeshansardar08/nextjs-15-support-ends-oct-21-the-upgrade-changes-your-build-wont-catch-38bj</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Next.js 15 leaves its support window on 21 October 2026. Run the &lt;code&gt;upgrade&lt;/code&gt; codemod and the separate &lt;code&gt;next-async-request-api&lt;/code&gt; codemod, then test the things a build can't: image quality now defaults to 75 only, image cache TTL went from 60 seconds to 4 hours, &lt;code&gt;next build&lt;/code&gt; no longer lints, sitemap &lt;code&gt;id&lt;/code&gt; is a &lt;code&gt;Promise&amp;lt;string&amp;gt;&lt;/code&gt;, and &lt;code&gt;proxy.ts&lt;/code&gt; runs on Node.js only.&lt;/p&gt;

&lt;p&gt;Next.js 15 leaves its support window on 21 October 2026. From that date, the version most production Next.js apps still run on stops receiving the critical fixes and security updates it has been getting since Next.js 16 shipped a year ago.&lt;/p&gt;

&lt;p&gt;Most upgrade guides stop at "run the codemod and fix the type errors." That gets you a green build. It does not get you a finished upgrade, because several of the changes in Next.js 16 compile cleanly and then quietly change what your users see: image quality, how long images stay cached, whether your CI still lints, and how the page scrolls between routes.&lt;/p&gt;

&lt;p&gt;This guide is the checklist we work through on every Next.js upgrade. It is organised so you can run it top to bottom, and every section opens with the answer so you can skip to the parts that apply to your app. Version numbers are current as of 13 October 2026: Next.js 16.4.0 is the latest stable release, and the newest Next.js 15 backport is 15.5.27.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens to Next.js 15 on October 21, 2026
&lt;/h2&gt;

&lt;p&gt;Next.js 15 reaches the end of its Maintenance LTS period on 21 October 2026, two years after its release on 21 October 2024. After that date it falls outside the &lt;a href="https://nextjs.org/support-policy" rel="noopener noreferrer"&gt;Next.js support policy&lt;/a&gt;, which means no more scheduled critical bug fixes or security patches.&lt;/p&gt;

&lt;p&gt;Your app does not stop working on the 22nd. Nothing is switched off. What changes is the risk profile: the next vulnerability disclosed in the framework will be fixed in Next.js 16, and a Next.js 15 app will either need that upgrade done under pressure or stay exposed. The policy does allow for patching unsupported versions "in rare circumstances" for severe issues, but that is an exception, not a plan.&lt;/p&gt;

&lt;p&gt;The support model works like this. A major version stays in Active LTS, with features, fixes and security patches, until the next major ships. It then moves to Maintenance LTS, where it only receives critical bug fixes and essential security updates, for two years from its original release date. Next.js 16 has been Active LTS since 21 October 2025. Next.js 15 has been in maintenance since the same day, and its clock runs out next week.&lt;/p&gt;

&lt;p&gt;If you are still on Next.js 14, you are already a year past its end of life, which was 26 October 2025. The same upgrade path applies, with the Next.js 15 breaking changes stacked on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a green build is not a finished upgrade
&lt;/h2&gt;

&lt;p&gt;A passing &lt;code&gt;next build&lt;/code&gt; proves your code compiles under the new rules. It says nothing about whether the app behaves the way it did before, and in Next.js 16 several changes are deliberately silent.&lt;/p&gt;

&lt;p&gt;The official upgrade guide says as much in its own migration checklist: a green build alone does not close runtime findings. Here are the changes that fit that description, each covered in detail below:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Image requests for a quality other than 75 are now quietly coerced to the nearest allowed value, so images can look different without any error in the build.&lt;/li&gt;
&lt;li&gt;Optimised images without cache headers now stay cached for four hours instead of sixty seconds, so an image replaced in your CMS can keep serving the old version all afternoon.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;next build&lt;/code&gt; no longer runs ESLint, so a pipeline that relied on the build to catch lint errors now ships them.&lt;/li&gt;
&lt;li&gt;Next.js no longer overrides a global &lt;code&gt;scroll-behavior: smooth&lt;/code&gt; during route changes, so navigations that used to jump to the top now animate.&lt;/li&gt;
&lt;li&gt;A sitemap &lt;code&gt;id&lt;/code&gt; is now a promise of a string, and plain JavaScript will not warn you when arithmetic on it produces the wrong number.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these is a reasonable default on its own. Together they are why the testing section near the end of this guide matters more than the codemod at the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check Node.js, TypeScript and browser requirements first
&lt;/h2&gt;

&lt;p&gt;Next.js 16 requires Node.js 20.9.0 or newer, TypeScript 5.1.0 or newer, and drops support for older browsers. Check these before touching any code, because a mismatch here produces confusing errors later.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Next.js 16 minimum&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;20.9.0 (Node.js 18 is no longer supported)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;5.1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browsers&lt;/td&gt;
&lt;td&gt;Chrome 111+, Edge 111+, Firefox 111+, Safari 16.4+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three places tend to get missed. The first is the Node.js version in your hosting configuration, which is separate from the one on your laptop. The second is the Node.js image in your CI workflow and any Dockerfile. The third is your browser support promise: if a client contract or analytics data says you support older Safari versions, that conversation needs to happen before the upgrade, not after.&lt;/p&gt;

&lt;p&gt;Update &lt;code&gt;@types/react&lt;/code&gt; and &lt;code&gt;@types/react-dom&lt;/code&gt; alongside &lt;code&gt;react&lt;/code&gt; and &lt;code&gt;react-dom&lt;/code&gt;. The App Router in Next.js 16 runs on the React canary channel that includes React 19.2 features such as View Transitions, &lt;code&gt;useEffectEvent&lt;/code&gt; and Activity, and stale type packages produce misleading errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the upgrade codemod, then run the one it skips
&lt;/h2&gt;

&lt;p&gt;Start on a dedicated branch and run the official upgrade codemod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @next/codemod@canary upgrade latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;According to the &lt;a href="https://nextjs.org/docs/app/guides/upgrading/codemods" rel="noopener noreferrer"&gt;codemod documentation&lt;/a&gt;, this updates &lt;code&gt;next.config.js&lt;/code&gt; for the new top-level &lt;code&gt;turbopack&lt;/code&gt; option, migrates &lt;code&gt;next lint&lt;/code&gt; to the ESLint CLI, renames the &lt;code&gt;middleware&lt;/code&gt; convention to &lt;code&gt;proxy&lt;/code&gt;, removes the &lt;code&gt;unstable_&lt;/code&gt; prefix from stabilised APIs, and removes the &lt;code&gt;experimental_ppr&lt;/code&gt; route segment config.&lt;/p&gt;

&lt;p&gt;What it does not do is migrate synchronous request APIs. If your app still reads &lt;code&gt;params&lt;/code&gt;, &lt;code&gt;searchParams&lt;/code&gt;, &lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;headers()&lt;/code&gt; or &lt;code&gt;draftMode()&lt;/code&gt; synchronously, which Next.js 15 allowed with a warning during its compatibility period, run the second codemod as well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @next/codemod@canary next-async-request-api &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the single most common reason an upgrade that "ran the codemod" still fails, so it is worth running even if you believe your code is already async.&lt;/p&gt;

&lt;p&gt;If you work with a coding agent, the guide now recommends &lt;code&gt;npx next@canary upgrade --agent=latest&lt;/code&gt;, which prepares the migration guides and codemod commands for the agent to apply and verify. The checklist below still applies, because an agent can also produce a green build that behaves differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Async request APIs: synchronous access is gone
&lt;/h2&gt;

&lt;p&gt;In Next.js 16, &lt;code&gt;cookies&lt;/code&gt;, &lt;code&gt;headers&lt;/code&gt;, &lt;code&gt;draftMode&lt;/code&gt;, &lt;code&gt;params&lt;/code&gt; and &lt;code&gt;searchParams&lt;/code&gt; can only be read asynchronously. Next.js 15 introduced the async versions with a temporary synchronous fallback, and that fallback has been removed.&lt;/p&gt;

&lt;p&gt;The change covers &lt;code&gt;params&lt;/code&gt; in &lt;code&gt;layout&lt;/code&gt;, &lt;code&gt;page&lt;/code&gt;, &lt;code&gt;route&lt;/code&gt; and &lt;code&gt;default&lt;/code&gt; files, in &lt;code&gt;opengraph-image&lt;/code&gt;, &lt;code&gt;twitter-image&lt;/code&gt;, &lt;code&gt;icon&lt;/code&gt; and &lt;code&gt;apple-icon&lt;/code&gt;, and &lt;code&gt;searchParams&lt;/code&gt; in &lt;code&gt;page&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;The cleanest way to get the types right is the generated helpers. Run &lt;code&gt;npx next typegen&lt;/code&gt; and you get globally available &lt;code&gt;PageProps&lt;/code&gt;, &lt;code&gt;LayoutProps&lt;/code&gt; and &lt;code&gt;RouteContext&lt;/code&gt; types that know your route parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/blog/[slug]/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PageProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/blog/[slug]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Blog Post: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the codemod runs, search the codebase for &lt;code&gt;UnsafeUnwrapped&lt;/code&gt; and for comments the codemod left where it could not migrate safely. Those markers are easy to commit by accident and they are exactly the places the upgrade guide tells you to resolve. In Client Components that need a value synchronously, React's &lt;code&gt;use&lt;/code&gt; hook is the documented route.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metadata images and sitemaps now receive promises
&lt;/h2&gt;

&lt;p&gt;The image generation functions behind &lt;code&gt;opengraph-image&lt;/code&gt;, &lt;code&gt;twitter-image&lt;/code&gt;, &lt;code&gt;icon&lt;/code&gt; and &lt;code&gt;apple-icon&lt;/code&gt; now receive &lt;code&gt;params&lt;/code&gt; and &lt;code&gt;id&lt;/code&gt; as promises, and so does the &lt;code&gt;sitemap&lt;/code&gt; function when you use &lt;code&gt;generateSitemaps&lt;/code&gt;. The &lt;code&gt;generateImageMetadata&lt;/code&gt; and &lt;code&gt;generateSitemaps&lt;/code&gt; functions themselves still receive synchronous values.&lt;/p&gt;

&lt;p&gt;For Open Graph images, the fix is mechanical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/shop/[slug]/opengraph-image.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sitemaps deserve more care. In Next.js 15 the &lt;code&gt;id&lt;/code&gt; passed to &lt;code&gt;sitemap&lt;/code&gt; was the number you returned from &lt;code&gt;generateSitemaps&lt;/code&gt;. In Next.js 16 it is a promise that resolves to a string. TypeScript will flag the missing &lt;code&gt;await&lt;/code&gt;, but in a plain JavaScript project nothing stops &lt;code&gt;id * 50000&lt;/code&gt; from quietly evaluating to &lt;code&gt;NaN&lt;/code&gt;, and nothing stops &lt;code&gt;resolvedId + 1&lt;/code&gt; from producing &lt;code&gt;"11"&lt;/code&gt; instead of &lt;code&gt;2&lt;/code&gt;. Either one gives you a sitemap that builds, deploys and lists the wrong URLs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/product/sitemap.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sitemap&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resolvedId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolvedId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;50000&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Convert to a number explicitly, then open the generated sitemap files in a browser and check the first and last URL in each one. Sitemaps are not something anyone looks at after launch, which is why a broken one can go unnoticed until Search Console starts reporting coverage drops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turbopack is the default bundler, and custom webpack config fails the build
&lt;/h2&gt;

&lt;p&gt;Turbopack is now stable and used by default for both &lt;code&gt;next dev&lt;/code&gt; and &lt;code&gt;next build&lt;/code&gt;. If your project has a custom &lt;code&gt;webpack&lt;/code&gt; configuration, &lt;code&gt;next build&lt;/code&gt; will fail on purpose rather than silently ignore it.&lt;/p&gt;

&lt;p&gt;You can remove the &lt;code&gt;--turbopack&lt;/code&gt; and &lt;code&gt;--turbo&lt;/code&gt; flags from your scripts. If the build fails because a webpack config was found, you have three options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Migrate the configuration to Turbopack equivalents, which is the long-term answer.&lt;/li&gt;
&lt;li&gt;Keep webpack for production builds with &lt;code&gt;next build --webpack&lt;/code&gt; while you migrate.&lt;/li&gt;
&lt;li&gt;Build with Turbopack and ignore the webpack config, but only if you are sure nothing in it matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you never wrote a webpack config yourself and still see this error, a plugin added one. Bundle analysers, MDX setups, SVG loaders and some monitoring SDKs are common sources.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;experimental.turbopack&lt;/code&gt; block has moved to a top-level &lt;code&gt;turbopack&lt;/code&gt; key in &lt;code&gt;next.config.ts&lt;/code&gt;. Two smaller changes catch people out. Turbopack does not support the legacy tilde prefix in Sass imports, so &lt;code&gt;@import '~bootstrap/...'&lt;/code&gt; becomes &lt;code&gt;@import 'bootstrap/...'&lt;/code&gt;. And code that relied on webpack's &lt;code&gt;resolve.fallback&lt;/code&gt; to silence errors about Node.js modules like &lt;code&gt;fs&lt;/code&gt; in client bundles needs &lt;code&gt;turbopack.resolveAlias&lt;/code&gt; instead, although the better fix is to stop importing server modules from client code.&lt;/p&gt;

&lt;h2&gt;
  
  
  middleware.ts becomes proxy.ts, unless you run on the edge
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;middleware&lt;/code&gt; file convention is deprecated and renamed to &lt;code&gt;proxy&lt;/code&gt;, along with the exported function name and any config flags containing the word, such as &lt;code&gt;skipMiddlewareUrlNormalize&lt;/code&gt;, which becomes &lt;code&gt;skipProxyUrlNormalize&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mv &lt;/span&gt;middleware.ts proxy.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// proxy.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important detail is the runtime. &lt;code&gt;proxy&lt;/code&gt; runs on Node.js, and that cannot be configured. The edge runtime is not supported in &lt;code&gt;proxy&lt;/code&gt;. If your middleware currently runs on the edge, for geolocation, low-latency redirects or A/B routing at the network edge, the guidance is to keep using &lt;code&gt;middleware&lt;/code&gt; for now rather than renaming it.&lt;/p&gt;

&lt;p&gt;The upgrade codemod handles the rename. After it runs, check whether any of your middleware declared the edge runtime, and if it did, decide deliberately whether to move it to Node.js or keep the old convention. The &lt;a href="https://nextjs.org/docs/app/api-reference/file-conventions/proxy" rel="noopener noreferrer"&gt;proxy documentation&lt;/a&gt; is the reference to read before choosing.&lt;/p&gt;

&lt;h2&gt;
  
  
  next/image defaults changed in six ways
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;next/image&lt;/code&gt; has six breaking changes in Next.js 16, and most of them change behaviour without producing an error. This is the section where we spend the most testing time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quality values.&lt;/strong&gt; &lt;code&gt;images.qualities&lt;/code&gt; now defaults to &lt;code&gt;[75]&lt;/code&gt; only. A &lt;code&gt;quality={90}&lt;/code&gt; prop is quietly coerced to the closest allowed value, so it renders at 75. Direct requests to &lt;code&gt;/_next/image&lt;/code&gt; with an unsupported &lt;code&gt;q&lt;/code&gt; value return a 400. If your design depends on higher-quality hero or product images, list the values you use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;qualities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cache lifetime.&lt;/strong&gt; &lt;code&gt;images.minimumCacheTTL&lt;/code&gt; now defaults to 4 hours (14,400 seconds) instead of 60 seconds. This reduces CPU cost when upstream images lack cache headers, which is a sensible default. The side effect is that an image replaced at the same URL in a headless CMS can keep serving the old version for hours. If editors expect instant changes, either set a lower value or change the image URL on update.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Image sizes.&lt;/strong&gt; &lt;code&gt;16&lt;/code&gt; has been removed from the default &lt;code&gt;images.imageSizes&lt;/code&gt; array, which shortens the &lt;code&gt;srcset&lt;/code&gt;. Add it back only if you genuinely serve 16-pixel images.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local images with query strings.&lt;/strong&gt; A local source like &lt;code&gt;/assets/photo?v=1&lt;/code&gt; now requires a matching &lt;code&gt;images.localPatterns&lt;/code&gt; entry with a &lt;code&gt;search&lt;/code&gt; value. This prevents enumeration attacks, and it also breaks cache-busting query strings that used to work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local IP addresses.&lt;/strong&gt; Optimising images from local IP addresses is now blocked by default. Hosting inside a VPC with split-horizon DNS is the usual way to hit this, and it shows up as a 400. &lt;code&gt;images.dangerouslyAllowLocalIP&lt;/code&gt; exists for private networks, and the name tells you how carefully to use it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redirects.&lt;/strong&gt; &lt;code&gt;images.maximumRedirects&lt;/code&gt; now defaults to 3 rather than unlimited. Image CDNs that chain redirects can start failing.&lt;/p&gt;

&lt;p&gt;Two deprecations belong on the same list. &lt;code&gt;next/legacy/image&lt;/code&gt; is deprecated in favour of &lt;code&gt;next/image&lt;/code&gt;, and &lt;code&gt;images.domains&lt;/code&gt; is deprecated in favour of &lt;code&gt;images.remotePatterns&lt;/code&gt;, which is stricter about protocol and hostname. The full option list is in the &lt;a href="https://nextjs.org/docs/app/api-reference/components/image" rel="noopener noreferrer"&gt;image configuration reference&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching APIs: revalidateTag needs a profile, and updateTag is new
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;revalidateTag&lt;/code&gt; now takes a second argument naming a &lt;code&gt;cacheLife&lt;/code&gt; profile. The single-argument form is deprecated and produces a TypeScript error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;max&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The more interesting change is what the second argument forces you to decide. &lt;code&gt;revalidateTag&lt;/code&gt; gives stale-while-revalidate behaviour: readers see the old content while fresh data loads in the background. That is right for blog posts, product catalogues and documentation.&lt;/p&gt;

&lt;p&gt;For changes a user makes and expects to see immediately, Next.js 16 adds &lt;a href="https://nextjs.org/docs/app/api-reference/functions/updateTag" rel="noopener noreferrer"&gt;&lt;code&gt;updateTag&lt;/code&gt;&lt;/a&gt;, which is only available in Server Actions and gives read-your-writes semantics by expiring and refreshing the data within the same request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;updateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateUserProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`user-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also a new &lt;code&gt;refresh()&lt;/code&gt; for refreshing the client router from inside a Server Action, which suits cases like updating a notification count after marking something as read. &lt;code&gt;cacheLife&lt;/code&gt; and &lt;code&gt;cacheTag&lt;/code&gt; are stable, so imports aliased from &lt;code&gt;unstable_cacheLife&lt;/code&gt; and &lt;code&gt;unstable_cacheTag&lt;/code&gt; can be simplified.&lt;/p&gt;

&lt;p&gt;When you review each &lt;code&gt;revalidateTag&lt;/code&gt; call during the upgrade, ask one question: would a user who just made this change be confused to see the old value? If yes, it should probably be &lt;code&gt;updateTag&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parallel route slots now need default.js
&lt;/h2&gt;

&lt;p&gt;Every parallel route slot now requires an explicit &lt;code&gt;default.js&lt;/code&gt; file, and builds fail without one. This one is loud, which makes it easy.&lt;/p&gt;

&lt;p&gt;To keep the previous behaviour, add a &lt;code&gt;default.js&lt;/code&gt; that either calls &lt;code&gt;notFound()&lt;/code&gt; or returns &lt;code&gt;null&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/@modal/default.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The decision between the two matters. Returning &lt;code&gt;null&lt;/code&gt; renders nothing in that slot on a hard navigation or reload. Calling &lt;code&gt;notFound()&lt;/code&gt; turns the whole route into a 404. Test both a direct visit and a browser refresh on any route that uses a modal or a slot, because that is where the difference shows. The &lt;a href="https://nextjs.org/docs/app/api-reference/file-conventions/default" rel="noopener noreferrer"&gt;default.js reference&lt;/a&gt; explains when each one fires.&lt;/p&gt;

&lt;h2&gt;
  
  
  next lint is gone, and next build no longer lints
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;next lint&lt;/code&gt; command has been removed, &lt;code&gt;next build&lt;/code&gt; no longer runs linting, and the &lt;code&gt;eslint&lt;/code&gt; option in &lt;code&gt;next.config&lt;/code&gt; is no longer supported. If your CI relied on the build step to catch lint errors, it has stopped doing that.&lt;/p&gt;

&lt;p&gt;The codemod migrates your scripts to the ESLint CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @next/codemod@canary next-lint-to-eslint-cli &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add an explicit lint step to your pipeline before the build, so a lint failure blocks a merge the way it used to. Biome is an equally valid choice if you were already considering it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@next/eslint-plugin-next&lt;/code&gt; now defaults to the ESLint flat config format, in line with ESLint 10 dropping legacy configuration. If you still use &lt;code&gt;.eslintrc&lt;/code&gt;, the &lt;a href="https://nextjs.org/docs/app/api-reference/config/eslint" rel="noopener noreferrer"&gt;ESLint configuration reference&lt;/a&gt; covers the flat config setup. One practical note from the official troubleshooting section: migrating off &lt;code&gt;next lint&lt;/code&gt; does not require upgrading ESLint itself. &lt;code&gt;eslint-config-next@16&lt;/code&gt; needs ESLint 9, but if an ESLint upgrade causes peer dependency conflicts, you can restore your previous lint stack and run the upgrade with &lt;code&gt;--skip-eslint-upgrade&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Removed features to search your codebase for
&lt;/h2&gt;

&lt;p&gt;Several features deprecated in earlier versions are now removed outright. A quick search for each one tells you whether any of these sections apply to you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AMP.&lt;/strong&gt; All AMP APIs are gone, including the &lt;code&gt;amp&lt;/code&gt; config, &lt;code&gt;useAmp&lt;/code&gt; from &lt;code&gt;next/amp&lt;/code&gt;, and &lt;code&gt;export const config = { amp: true }&lt;/code&gt;. If you still serve AMP pages, this is a product decision before it is a code change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime configuration.&lt;/strong&gt; &lt;code&gt;serverRuntimeConfig&lt;/code&gt; and &lt;code&gt;publicRuntimeConfig&lt;/code&gt; have been removed. Server-only values come from &lt;code&gt;process.env&lt;/code&gt; directly in Server Components. Values the browser needs use the &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; prefix. Note that &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; values are inlined at build time, so if you relied on runtime config to change a value without rebuilding, call &lt;code&gt;await connection()&lt;/code&gt; from &lt;code&gt;next/server&lt;/code&gt; before reading &lt;code&gt;process.env&lt;/code&gt;, which forces the read to happen at request time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experimental flags.&lt;/strong&gt; &lt;code&gt;experimental.dynamicIO&lt;/code&gt;, &lt;code&gt;experimental.useCache&lt;/code&gt; and the experimental Partial Prerendering flag are gone. If you were actively using them, the replacement is the top-level &lt;code&gt;cacheComponents&lt;/code&gt; option. If you were not, delete them rather than renaming them, because enabling &lt;code&gt;cacheComponents&lt;/code&gt; is a model change, not a rename.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smaller removals.&lt;/strong&gt; &lt;code&gt;unstable_rootParams&lt;/code&gt; is replaced by &lt;code&gt;next/root-params&lt;/code&gt;, and the &lt;code&gt;appIsrStatus&lt;/code&gt;, &lt;code&gt;buildActivity&lt;/code&gt; and &lt;code&gt;buildActivityPosition&lt;/code&gt; options under &lt;code&gt;devIndicators&lt;/code&gt; are removed, although the indicator itself remains.&lt;/p&gt;

&lt;p&gt;Search terms worth running: &lt;code&gt;next/amp&lt;/code&gt;, &lt;code&gt;RuntimeConfig&lt;/code&gt;, &lt;code&gt;getConfig&lt;/code&gt;, &lt;code&gt;dynamicIO&lt;/code&gt;, &lt;code&gt;useCache&lt;/code&gt;, &lt;code&gt;experimental_ppr&lt;/code&gt;, &lt;code&gt;rootParams&lt;/code&gt; and &lt;code&gt;devIndicators&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smaller behaviour changes that surprise people
&lt;/h2&gt;

&lt;p&gt;A handful of changes do not break anything but will generate questions from your team or your clients. Knowing about them in advance saves time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smooth scrolling between routes.&lt;/strong&gt; If your global CSS sets &lt;code&gt;scroll-behavior: smooth&lt;/code&gt; on &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;, earlier versions temporarily overrode it during navigation so route changes snapped to the top. Next.js 16 no longer does this, so navigations now animate. To restore the old behaviour, add &lt;code&gt;data-scroll-behavior="smooth"&lt;/code&gt; to the &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; element in your root layout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build output metrics.&lt;/strong&gt; The &lt;code&gt;size&lt;/code&gt; and &lt;code&gt;First Load JS&lt;/code&gt; columns are gone from &lt;code&gt;next build&lt;/code&gt; output because they were inaccurate with React Server Components. If someone on your team tracked bundle size from that output, switch to Lighthouse or real-user Core Web Vitals data. Our guide to &lt;a href="https://zignites.com/blog/mobile-first-design-core-web-vitals" rel="noopener noreferrer"&gt;passing Core Web Vitals on real phones&lt;/a&gt; covers how to measure what users actually experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate dev output.&lt;/strong&gt; &lt;code&gt;next dev&lt;/code&gt; now writes to &lt;code&gt;.next/dev&lt;/code&gt;, so dev and build can run at the same time, and a lockfile stops two instances of the same command from running on one project. Scripts that cleaned or inspected &lt;code&gt;.next&lt;/code&gt; directly may need a path update.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Config loaded once in development.&lt;/strong&gt; The config file is no longer loaded twice during &lt;code&gt;next dev&lt;/code&gt;, which means checking whether &lt;code&gt;process.argv&lt;/code&gt; includes &lt;code&gt;'dev'&lt;/code&gt; inside &lt;code&gt;next.config&lt;/code&gt; now returns &lt;code&gt;false&lt;/code&gt;. Plugins that used that check to start side processes should check &lt;code&gt;process.env.NODE_ENV === 'development'&lt;/code&gt; or use the config &lt;code&gt;phase&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefetching.&lt;/strong&gt; Navigation prefetching has been rebuilt with layout deduplication and incremental prefetching. You may see more individual prefetch requests in the network tab, each much smaller in total transfer size. This is expected and needs no code changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  A testing plan that catches what the build misses
&lt;/h2&gt;

&lt;p&gt;The build tells you the code compiles. This testing plan tells you the app still behaves the same way. We run it on a preview deployment of the upgrade branch, side by side with production.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Images.&lt;/strong&gt; Open your heaviest image pages, typically the homepage, a product or portfolio page and a blog post, on both deployments. Compare visually at full size. Check the network tab for any &lt;code&gt;/_next/image&lt;/code&gt; request returning 400.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content freshness.&lt;/strong&gt; Replace an image and edit a piece of content in your CMS. Confirm both update within the time your editors expect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sitemaps and metadata.&lt;/strong&gt; Open every generated sitemap and check the first and last URLs. Share a page link in a social preview tool to confirm Open Graph images render.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routes with slots or modals.&lt;/strong&gt; Visit each one directly, then refresh the browser on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proxy or middleware.&lt;/strong&gt; Exercise every redirect, rewrite, auth gate and locale rule it handles. If you changed runtimes, measure latency from more than one region.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forms and Server Actions.&lt;/strong&gt; Submit every form that changes data and confirm the user sees their change immediately where they should.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Navigation feel.&lt;/strong&gt; Click through the main routes and confirm scroll behaviour matches what you intend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI.&lt;/strong&gt; Push a commit with a deliberate lint error and confirm the pipeline fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-user metrics.&lt;/strong&gt; After release, watch Core Web Vitals and server CPU for a few days. The image cache change usually lowers CPU, and a sudden rise is worth investigating.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you'd rather work from paper, the &lt;a href="https://zignites.com/contact?resource=nextjs16-upgrade&amp;amp;from=nextjs-16-upgrade-checklist" rel="noopener noreferrer"&gt;printable upgrade worksheet&lt;/a&gt; has this test plan as tick boxes with a pass condition for each one.&lt;/p&gt;

&lt;p&gt;The upgrade guide closes with its own migration checklist, and it is worth reading line by line before you merge. One line in particular is worth repeating to anyone in a hurry: do not enable React Compiler or Cache Components just to complete the upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you turn on React Compiler or Cache Components during the upgrade?
&lt;/h2&gt;

&lt;p&gt;No. Both are worth evaluating, and neither belongs in the same pull request as a major version upgrade.&lt;/p&gt;

&lt;p&gt;React Compiler support is stable in Next.js 16 through the top-level &lt;code&gt;reactCompiler&lt;/code&gt; option, following React Compiler 1.0. It is not enabled by default, it requires &lt;code&gt;babel-plugin-react-compiler&lt;/code&gt;, and the documentation notes that it increases compile times because it relies on Babel. It is a reasonable experiment once the upgrade has settled, measured against real render performance.&lt;/p&gt;

&lt;p&gt;Cache Components, enabled through &lt;code&gt;cacheComponents: true&lt;/code&gt;, is how Partial Prerendering works in Next.js 16. It can surface build errors for uncached data accessed outside a &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary and asks you to adopt a different caching model. The &lt;a href="https://nextjs.org/docs/app/guides/migrating-to-cache-components" rel="noopener noreferrer"&gt;Cache Components migration guide&lt;/a&gt; is the place to start when you are ready.&lt;/p&gt;

&lt;p&gt;Keeping these separate is basic change management. If something regresses after release, you want one change to suspect, not three. For teams building larger products, our post on &lt;a href="https://zignites.com/blog/multi-tenant-saas-architecture" rel="noopener noreferrer"&gt;multi-tenant SaaS architecture&lt;/a&gt; covers the same principle at the system level: isolate changes so failures stay small and traceable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Next.js 16 Upgrade Worksheet&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All 24 checks from this guide on two printable pages, each with a pass condition, plus a readiness score that tells you when it's safe to merge. No account, no sales sequence.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://zignites.com/contact?resource=nextjs16-upgrade&amp;amp;from=nextjs-16-upgrade-checklist" rel="noopener noreferrer"&gt;Get the PDF&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Will my Next.js 15 site stop working after October 21, 2026?
&lt;/h3&gt;

&lt;p&gt;No. Your site keeps running exactly as it does today. What ends is support: Next.js 15 stops receiving the critical bug fixes and security updates it gets during Maintenance LTS. The risk is that the next security issue in the framework will be patched in Next.js 16, leaving you to upgrade under time pressure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I upgrade from Next.js 14 directly to Next.js 16?
&lt;/h3&gt;

&lt;p&gt;Yes, but you take on two sets of breaking changes at once. Read the Next.js 15 upgrade guide as well, because the async request API change started there, and run both the &lt;code&gt;upgrade&lt;/code&gt; codemod and the &lt;code&gt;next-async-request-api&lt;/code&gt; codemod. Next.js 14 has been unsupported since 26 October 2025, so this upgrade is overdue rather than optional.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I have to switch to Turbopack?
&lt;/h3&gt;

&lt;p&gt;No. Turbopack is the default, but you can keep webpack for builds with &lt;code&gt;next build --webpack&lt;/code&gt;. If your project has a custom webpack configuration, you must choose explicitly, because the build fails rather than ignoring it. Migrating to Turbopack is the recommended long-term path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the upgrade codemod handle everything?
&lt;/h3&gt;

&lt;p&gt;No. It handles the Turbopack config move, the &lt;code&gt;next lint&lt;/code&gt; migration, the &lt;code&gt;middleware&lt;/code&gt; to &lt;code&gt;proxy&lt;/code&gt; rename, &lt;code&gt;unstable_&lt;/code&gt; prefixes and the &lt;code&gt;experimental_ppr&lt;/code&gt; segment config. It does not migrate synchronous request APIs, which need the separate &lt;code&gt;next-async-request-api&lt;/code&gt; codemod, and it cannot test the behaviour changes in images, caching, scrolling and CI linting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the minimum Node.js version for Next.js 16 really 20.9?
&lt;/h3&gt;

&lt;p&gt;Yes. Next.js 16 requires Node.js 20.9.0 or newer and no longer supports Node.js 18. Check the version in your hosting platform settings, CI configuration and Docker images, not just on your development machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Next.js 15 support ends on 21 October 2026. Run both codemods, fix what the build reports, then spend real time on the changes it cannot report: image quality and caching, sitemap IDs, CI linting, scroll behaviour and any edge middleware. Keep React Compiler and Cache Components for a later, separate change.&lt;/p&gt;

&lt;p&gt;If you are planning the rest of your stack for the coming year, our roundup of &lt;a href="https://zignites.com/blog/web-development-trends-2026" rel="noopener noreferrer"&gt;what actually changed in web development in 2026&lt;/a&gt; puts this release in context. The full detail for everything above lives in the official &lt;a href="https://nextjs.org/docs/app/guides/upgrading/version-16" rel="noopener noreferrer"&gt;Next.js 16 upgrade guide&lt;/a&gt;, which is worth keeping open in a tab while you work.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zignites.com/blog/nextjs-16-upgrade-checklist" rel="noopener noreferrer"&gt;zignites.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to Tell If a WordPress Plugin Is Abandoned (One Field Won't Tell You)</title>
      <dc:creator>Muhammad Zeeshan Sardar</dc:creator>
      <pubDate>Fri, 02 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/zeeshansardar08/how-to-tell-if-a-wordpress-plugin-is-abandoned-one-field-wont-tell-you-186h</link>
      <guid>https://dev.to/zeeshansardar08/how-to-tell-if-a-wordpress-plugin-is-abandoned-one-field-wont-tell-you-186h</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; The last release date is the strongest signal. "Tested up to" goes stale on its own. A closed listing means opposite things depending on the &lt;code&gt;reason&lt;/code&gt; field. Support-thread ratios are noisy. Premium plugins sit outside the directory entirely. There is a script at the end that checks a list of plugins in one run.&lt;/p&gt;

&lt;p&gt;Ask the WordPress.org plugin API about Display Widgets and you get this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"closed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This plugin has been closed as of January 30, 2021 and is not available for download. This closure is permanent. Reason: Security Issue."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask it about Paid Memberships Pro and you get this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"closed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This plugin has been closed as of October 17, 2024 and is not available for download. This closure is permanent. Reason: Author Request."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same error, same wording, same word "permanent". But these are opposite situations. Display Widgets was pulled over a security issue. Paid Memberships Pro left the directory by its own authors' choice and kept shipping releases from its own site. It was still releasing updates in September 2026.&lt;/p&gt;

&lt;p&gt;Any check that stops at "is it listed?" treats those two plugins the same way. So does any check that stops at "when was it last updated?", for different reasons.&lt;/p&gt;

&lt;p&gt;There is no official definition of an abandoned plugin, and no single field that tells you. What you do have is a handful of facts the directory publishes, each of which means something narrower than it looks. Read them together and you get a reliable picture. This post goes through each one, then gives you a short script that checks a list of plugins in one run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get the facts yourself first
&lt;/h2&gt;

&lt;p&gt;Everything below comes from one public endpoint. No key, no signup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&amp;amp;request[slug]=akismet&amp;amp;request[fields][sections]=0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fields that matter for maintenance:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;What it holds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The current release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;last_updated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;When a release last shipped, e.g. &lt;code&gt;2026-08-18 11:42pm GMT&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tested&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The newest WordPress version the author says they tested against&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;requires_php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The minimum PHP version the plugin declares&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;active_installs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A rounded floor, not an exact count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;support_threads&lt;/code&gt; / &lt;code&gt;support_threads_resolved&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Forum threads in the recent window, and how many are marked resolved&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a closed plugin you get &lt;code&gt;closed: true&lt;/code&gt;, a &lt;code&gt;closed_date&lt;/code&gt;, and a &lt;code&gt;reason&lt;/code&gt; such as &lt;code&gt;security-issue&lt;/code&gt; or &lt;code&gt;author-request&lt;/code&gt;, instead of the fields above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signal 1: the last release date (the strongest one)
&lt;/h2&gt;

&lt;p&gt;A release is the only thing in the list that proves someone recently did work on the plugin. A reasonable way to read it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Under 6 months:&lt;/strong&gt; actively maintained by any sensible standard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6 to 11 months:&lt;/strong&gt; worth a look, especially for anything large.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;12 to 23 months:&lt;/strong&gt; ageing. Find out what you would replace it with.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;24 months or more:&lt;/strong&gt; treat it as unmaintained until you learn otherwise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The nuance is plugin size. A small plugin that does one narrow thing may genuinely need no changes for two years, and WordPress is careful about backwards compatibility. A page builder, a checkout extension or a membership system that has not shipped in two years is a different story, because those plugins touch everything that changes around them.&lt;/p&gt;

&lt;p&gt;The date is not the risk on its own. The risk is what happens when PHP or WordPress next ships a breaking change and nobody is there to fix the plugin. You want to know about that before your host upgrades PHP, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signal 2: "tested up to" (stale by design)
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;tested&lt;/code&gt; field is the author telling you the newest WordPress version they checked the plugin against. It goes stale on its own. WordPress ships two or three major releases a year, so a plugin that nobody touches falls behind whether or not anything in it is broken.&lt;/p&gt;

&lt;p&gt;WordPress.org shows a warning on a plugin's page once it has not been tested with the latest three major releases, which is a sensible line.&lt;/p&gt;

&lt;p&gt;Read it alongside the release date:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Recent release, old "tested up to":&lt;/strong&gt; usually the author forgot to bump the header. Low concern, but test it on staging before a major WordPress upgrade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Old release, old "tested up to":&lt;/strong&gt; both fields are telling you the same thing, and it is the release date that matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not add the two together as if they were separate problems. When the last release is over a year old, the "tested up to" gap is a symptom of the same fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signal 3: closed does not mean one thing
&lt;/h2&gt;

&lt;p&gt;This is the case from the top of the post. The &lt;code&gt;reason&lt;/code&gt; field is what separates them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;security-issue&lt;/code&gt;&lt;/strong&gt; (and the other reasons set by the plugin review team): the directory pulled the plugin. Take this seriously, find out why, and plan to replace it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;author-request&lt;/code&gt;:&lt;/strong&gt; the author asked for the listing to be removed. That can mean they gave up. It can also mean they moved distribution to their own site, which is exactly what happened with Paid Memberships Pro.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For author-request closures, the next step is to check the author's own site or repository for recent releases. If they are still shipping, the plugin is maintained. It simply is not in the directory any more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signal 4: support threads (noisy, use with care)
&lt;/h2&gt;

&lt;p&gt;It is tempting to read &lt;code&gt;support_threads_resolved / support_threads&lt;/code&gt; as a responsiveness score. Be careful.&lt;/p&gt;

&lt;p&gt;A thread only counts as resolved when someone marks it resolved. Plenty of active authors answer every thread and never tick the box, and plenty of users never come back to close their own threads. The ratio also needs a reasonable sample. Five threads with two resolved tells you almost nothing.&lt;/p&gt;

&lt;p&gt;Use it as a prompt to open the forum and read the last page of threads yourself. If recent questions have replies from the author, the plugin is supported, whatever the ratio says.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signal 5: premium plugins sit outside all of this
&lt;/h2&gt;

&lt;p&gt;Commercial plugins sold from the developer's own site are not in the directory, so none of the fields above exist for them. That is completely normal, and it carries its own risk.&lt;/p&gt;

&lt;p&gt;Premium plugins usually only get updates while the licence is active. When a renewal lapses, the plugin keeps running and quietly stops receiving fixes, and nothing on the site tells you. No outside check can see that.&lt;/p&gt;

&lt;p&gt;For premium plugins, the questions are not technical. Which licences are still active, and who is responsible for renewing them?&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;How to read it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recent release, current "tested up to"&lt;/td&gt;
&lt;td&gt;Maintained&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recent release, old "tested up to"&lt;/td&gt;
&lt;td&gt;Maintained, test before major upgrades&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No release for 12+ months&lt;/td&gt;
&lt;td&gt;Ageing, know your replacement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No release for 24+ months, large plugin&lt;/td&gt;
&lt;td&gt;Treat as unmaintained&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Closed, &lt;code&gt;security-issue&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Replace it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Closed, &lt;code&gt;author-request&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Check the author's own site before deciding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not in the directory&lt;/td&gt;
&lt;td&gt;Premium or custom, check the licence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  A script to check a list of plugins
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;check-plugins.mjs&lt;/code&gt; and run it with Node 18 or later: &lt;code&gt;node check-plugins.mjs akismet contact-form-7 display-widgets&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.wordpress.org/plugins/info/1.2/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;monthsSince&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastUpdated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastUpdated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(\d)(&lt;/span&gt;&lt;span class="sr"&gt;am|pm&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$1 $2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;30.44&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?action=plugin_information&amp;amp;request[slug]=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;request[fields][sections]=0`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;closed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: closed on &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;closed_date&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: not in the directory (premium, custom, or a typo)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;months&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;monthsSince&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;last_updated&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;months&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;months&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; month&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;months&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; ago`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: v&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, last release &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, tested up to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tested&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details in there are worth knowing. &lt;code&gt;last_updated&lt;/code&gt; comes back in a format like &lt;code&gt;2026-08-18 11:42pm GMT&lt;/code&gt;, which JavaScript's date parser does not reliably accept until the space before &lt;code&gt;pm&lt;/code&gt; is added. And a slug that has never existed also returns an &lt;code&gt;error&lt;/code&gt; without &lt;code&gt;closed&lt;/code&gt;, so "not in the directory" covers typos as well as premium plugins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing this for a whole site
&lt;/h2&gt;

&lt;p&gt;The script works when you already know the slugs. On a client's site you often do not, especially before you have a login.&lt;/p&gt;

&lt;p&gt;That is the gap I built &lt;a href="https://zignites.com/tools/wordpress-plugin-risk-report" rel="noopener noreferrer"&gt;a free WordPress plugin checker&lt;/a&gt; to fill. You paste a URL, and it detects the plugins the site loads publicly and reads the same directory fields covered above for each one. It separates plugin-team closures from author requests, marks which installed versions are inferred, and says plainly what it could not see. It does not make vulnerability claims, because that data sits behind commercial APIs and guessing at it does more harm than good.&lt;/p&gt;

&lt;p&gt;If you want the signals for one plugin at a time, there is also a &lt;a href="https://zignites.com/plugin-check" rel="noopener noreferrer"&gt;plugin maintenance check&lt;/a&gt; covering widely installed plugins, with the date each one was read from the directory.&lt;/p&gt;

&lt;p&gt;And if you are on the other side of this and maintain a plugin yourself, &lt;a href="https://zignites.com/blog/wordpress-plugin-development-guide" rel="noopener noreferrer"&gt;this guide to WordPress plugin development&lt;/a&gt; covers what keeping one healthy in the directory actually involves.&lt;/p&gt;

&lt;p&gt;The standard I would suggest for any site you are responsible for is simple. For every plugin that has gone a year without a release, know what you would replace it with before you need to. The day you need that answer is usually the day a host upgrades PHP.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The Obvious Way to Detect WordPress Plugins Counts WooCommerce Six Times</title>
      <dc:creator>Muhammad Zeeshan Sardar</dc:creator>
      <pubDate>Wed, 30 Sep 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/zeeshansardar08/the-obvious-way-to-detect-wordpress-plugins-counts-woocommerce-six-times-45eo</link>
      <guid>https://dev.to/zeeshansardar08/the-obvious-way-to-detect-wordpress-plugins-counts-woocommerce-six-times-45eo</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Asset paths, script handles and REST namespaces all reveal WordPress plugins from outside, and each one produces false positives if you trust it on its own. Deny core handles, map handle families to their parent plugin, require corroboration, and never trust a &lt;code&gt;?ver=&lt;/code&gt; that matches the core version.&lt;/p&gt;

&lt;p&gt;Point a naive plugin detector at a WooCommerce store and it will tell you the site runs &lt;code&gt;wc&lt;/code&gt;, &lt;code&gt;wc-admin&lt;/code&gt;, &lt;code&gt;wc-analytics&lt;/code&gt;, &lt;code&gt;wc-telemetry&lt;/code&gt;, &lt;code&gt;wccom-site&lt;/code&gt; and &lt;code&gt;wc-admin-email&lt;/code&gt;. Six plugins. It is one plugin, WooCommerce, and the real &lt;code&gt;woocommerce&lt;/code&gt; slug may not appear in the list at all.&lt;/p&gt;

&lt;p&gt;The same detector will report &lt;code&gt;wp-block-editor&lt;/code&gt; and &lt;code&gt;wp-site-health&lt;/code&gt; as plugins. Both are WordPress core.&lt;/p&gt;

&lt;p&gt;I build WordPress plugins, and I recently built a free scanner that reads a public site and lists the plugins it runs, with the maintenance facts WordPress.org publishes about each one. The use case is quoting: before a client hands over a login, you want to know whether you are walking into twelve current plugins or four that have not shipped a release since 2022.&lt;/p&gt;

&lt;p&gt;Getting that list right turned out to be the whole job. Detection from outside is mostly an exercise in not believing your own evidence too quickly. Here are the three signals that work, the trap in each one, and the rules that keep the list honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signal 1: asset paths (the one to trust most)
&lt;/h2&gt;

&lt;p&gt;Plugins live in &lt;code&gt;/wp-content/plugins/&amp;lt;slug&amp;gt;/&lt;/code&gt;, and when a plugin loads a stylesheet, script or image on a page, that path shows up in the HTML.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PLUGIN_PATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;wp-content&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;plugins&lt;/span&gt;&lt;span class="se"&gt;\/([&lt;/span&gt;&lt;span class="sr"&gt;a-z0-9-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)\/&lt;/span&gt;&lt;span class="sr"&gt;/gi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;slugsFromPaths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slugs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PLUGIN_PATH&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;slugs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;slugs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a slug appears in an asset path, the plugin is installed and active on that page. That is about as strong as outside evidence gets, and the directory name matches the WordPress.org slug for anything installed from the directory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trap:&lt;/strong&gt; absence proves nothing. Three things hide plugins from this signal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Optimisation and caching plugins&lt;/strong&gt; that combine every stylesheet and script into one bundle served from their own cache folder. The individual plugin paths disappear from the HTML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugins with no front-end assets.&lt;/strong&gt; Anything that works entirely in wp-admin or on the server leaves no trace in the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security plugins&lt;/strong&gt; that rewrite or hide plugin paths on purpose.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a short list from this signal is evidence of what is visible, not evidence of a small install. That distinction has to reach the person reading the report, or they will draw the wrong conclusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signal 2: script and style handles (useful, and full of traps)
&lt;/h2&gt;

&lt;p&gt;When WordPress prints an enqueued script or stylesheet, it adds an ID built from the handle the code registered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"contact-form-7-css"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"wc-add-to-cart-js"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Strip the &lt;code&gt;-js&lt;/code&gt; or &lt;code&gt;-css&lt;/code&gt; suffix and you have the handle. This signal matters because it survives some of the situations that hide asset paths, including assets served from a CDN under a different URL structure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HANDLE_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;script|link&lt;/span&gt;&lt;span class="se"&gt;)\b[^&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;id=&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;]([&lt;/span&gt;&lt;span class="sr"&gt;a-z0-9_-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;js|css&lt;/span&gt;&lt;span class="se"&gt;)[&lt;/span&gt;&lt;span class="sr"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/gi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handlesFromIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HANDLE_ID&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;handles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;handles&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The regex requires the closing quote straight after &lt;code&gt;-js&lt;/code&gt;, which quietly skips inline companions such as &lt;code&gt;wc-add-to-cart-js-extra&lt;/code&gt; and &lt;code&gt;-js-after&lt;/code&gt;. Those belong to a handle you have already counted.&lt;/p&gt;

&lt;p&gt;This is where the six WooCommerce rows come from. A handle is not a slug. It is whatever string the developer chose, and three kinds of handle break the naive approach. (Core packages also start with &lt;code&gt;wp-&lt;/code&gt;, so a blanket "anything beginning with wp-" rule would throw away real plugins such as &lt;code&gt;wp-mail-smtp&lt;/code&gt;. The deny list has to be explicit.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core handles.&lt;/strong&gt; WordPress registers dozens of its own scripts: &lt;code&gt;wp-block-editor&lt;/code&gt;, &lt;code&gt;wp-polyfill&lt;/code&gt;, &lt;code&gt;jquery-core&lt;/code&gt;, &lt;code&gt;wp-emoji-release&lt;/code&gt;, &lt;code&gt;wp-block-library&lt;/code&gt;, &lt;code&gt;global-styles&lt;/code&gt;, &lt;code&gt;classic-theme-styles&lt;/code&gt; and more. None of them is a plugin. You need an explicit deny list, and it needs a comment saying where it came from, because it will need extending every few releases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Handle families.&lt;/strong&gt; One plugin often registers many handles under a shared prefix. WooCommerce uses &lt;code&gt;wc-*&lt;/code&gt; and &lt;code&gt;wccom-*&lt;/code&gt;. Yoast uses &lt;code&gt;yoast-*&lt;/code&gt; and &lt;code&gt;wpseo-*&lt;/code&gt;, and its directory slug is &lt;code&gt;wordpress-seo&lt;/code&gt;, which no handle contains. You need a maintained map from prefix to parent slug, and you emit one row for the parent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Theme handles.&lt;/strong&gt; Themes enqueue assets too. A theme stylesheet with the ID &lt;code&gt;acme-siteheader-css&lt;/code&gt; produces the handle &lt;code&gt;acme-siteheader&lt;/code&gt;, which looks exactly like a plugin that does not exist.&lt;/p&gt;

&lt;p&gt;The rule that fixes all three is corroboration. A slug derived only from a handle is accepted when it resolves in the WordPress.org directory, or when the same slug also appears as a real asset path on the site. Anything else is dropped silently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Core script and style handles. Extend this as WordPress adds packages.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CORE_HANDLES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jquery&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jquery-core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jquery-migrate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wp-polyfill&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wp-emoji-release&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wp-block-library&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wp-block-editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wp-site-health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;global-styles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;classic-theme-styles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin-bar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;regenerator-runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lodash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;moment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// Prefix to parent slug. Order matters: more specific prefixes first.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HANDLE_FAMILIES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wccom-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;woocommerce&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wc-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;woocommerce&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wpseo-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wordpress-seo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yoast-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wordpress-seo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;elementor-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;elementor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;familyParent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;woocommerce&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;HANDLE_FAMILIES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;acceptHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;pathSlugs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;directorySlugs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CORE_HANDLES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;familyParent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pathSlugs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;directorySlugs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dropping silently is the important part. The tempting alternative is to show unmatched handles as "not in directory". That turns a theme stylesheet into a finding, and a WordPress developer will spot it in the first ten seconds and stop trusting everything else on the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signal 3: REST API namespaces
&lt;/h2&gt;

&lt;p&gt;Most WordPress sites expose an index at &lt;code&gt;/wp-json/&lt;/code&gt;, and its &lt;code&gt;namespaces&lt;/code&gt; array lists the route prefixes every active plugin has registered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"namespaces"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"oembed/1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wc/v3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yoast/v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"contact-form-7/v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wp/v2"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches plugins that load nothing on the front end but still register routes, which fills part of the gap Signal 1 leaves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trap:&lt;/strong&gt; namespaces are not slugs either. &lt;code&gt;wp/v2&lt;/code&gt; and &lt;code&gt;oembed/1.0&lt;/code&gt; are core. &lt;code&gt;yoast/v1&lt;/code&gt; belongs to &lt;code&gt;wordpress-seo&lt;/code&gt;. &lt;code&gt;wc/v3&lt;/code&gt; belongs to &lt;code&gt;woocommerce&lt;/code&gt;. The same family map from Signal 2 does most of the work here, and the same corroboration rule applies.&lt;/p&gt;

&lt;p&gt;It is also the weakest of the three signals. A namespace proves the plugin registered routes. It does not prove the plugin does anything visible, and some sites disable or restrict the REST index. When you report a plugin, record which signal found it (path, handle, namespace, or several), so you can tell later how much weight each row deserves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting the installed version
&lt;/h2&gt;

&lt;p&gt;Knowing a plugin is installed is half of it. Knowing the site runs 4.2 while the current release is 6.1 is the part that matters for a quote.&lt;/p&gt;

&lt;p&gt;The best source is the plugin's own &lt;code&gt;readme.txt&lt;/code&gt;, fetched from &lt;code&gt;/wp-content/plugins/&amp;lt;slug&amp;gt;/readme.txt&lt;/code&gt;, which carries a &lt;code&gt;Stable tag&lt;/code&gt; line. Many sites block direct requests to plugin folders, and larger ones block them almost always.&lt;/p&gt;

&lt;p&gt;The fallback is the &lt;code&gt;ver&lt;/code&gt; query string on asset URLs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;versionFromAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;pageUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;coreVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageUrl&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ver&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ver&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;coreVersion&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;ver&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;coreVersion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ver&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The core version check exists because of how &lt;code&gt;wp_register_script&lt;/code&gt; behaves. When a plugin registers an asset without passing its own version, WordPress appends the core version instead. Read that at face value and you will report that a plugin is on version 6.8, when 6.8 is simply the WordPress version.&lt;/p&gt;

&lt;p&gt;Two more rules make this usable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Label it.&lt;/strong&gt; A version from a readme is confirmed. A version from &lt;code&gt;?ver=&lt;/code&gt; is inferred, because it is a cache-busting parameter and a site can put anything in it. The report should say which is which.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find the core version from core assets&lt;/strong&gt;, such as the &lt;code&gt;ver&lt;/code&gt; on files under &lt;code&gt;/wp-includes/&lt;/code&gt;. The generator meta tag is often removed, so it cannot be the only source.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the directory can tell you, and what it cannot
&lt;/h2&gt;

&lt;p&gt;Once you have a clean list of slugs, the WordPress.org plugin API gives you the maintenance facts for each one: when it last shipped a release, which WordPress version its author last tested against, and whether the directory still lists it.&lt;/p&gt;

&lt;p&gt;What it does not give you is vulnerability data. I made a deliberate decision not to guess at it. Good vulnerability data for WordPress plugins sits behind commercial APIs, and a free tool that fakes it has two options. It can guess from version numbers, which flags plugins that are fine. Or it can flag a few famous cases and stay quiet on the rest, which implies everything unflagged has been checked. Both are worse than saying clearly that no security check was run.&lt;/p&gt;

&lt;p&gt;The same principle applies to the score. If a site loads 11 plugins and 8 of them are premium plugins sold outside the directory, there is no public release history to judge those 8 on. A score built on the remaining 3 should say so, rather than printing "Healthy" next to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The standard I hold a scanner to
&lt;/h2&gt;

&lt;p&gt;Fewer, truer findings beat a longer list. Every rule above removes rows rather than adding them: the deny list, the family map, the corroboration check and the core version check. Each one exists because a plausible-looking false positive costs more trust than a missing row.&lt;/p&gt;

&lt;p&gt;If you want to see the result on a real site, the scanner is free and needs no signup: &lt;a href="https://zignites.com/tools/wordpress-plugin-risk-report" rel="noopener noreferrer"&gt;WordPress Plugin Checker&lt;/a&gt;. It marks which installed versions are inferred rather than confirmed, and states plainly what it could not see. If you only care about one plugin, the &lt;a href="https://zignites.com/plugin-check" rel="noopener noreferrer"&gt;plugin maintenance check&lt;/a&gt; covers widely installed plugins one at a time.&lt;/p&gt;

&lt;p&gt;If you build plugins yourself, I wrote about &lt;a href="https://zignites.com/blog/wordpress-plugin-development-guide" rel="noopener noreferrer"&gt;what maintaining one actually involves&lt;/a&gt;, including why "tested up to" goes stale on its own.&lt;/p&gt;

&lt;p&gt;I would like to hear from anyone who has built detection like this. Which handle families or namespaces have caught you out? The family map only gets better with more eyes on it.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Export Installed WordPress Plugins &amp; Themes to CSV (Developer-Friendly Method)</title>
      <dc:creator>Muhammad Zeeshan Sardar</dc:creator>
      <pubDate>Mon, 09 Mar 2026 23:08:21 +0000</pubDate>
      <link>https://dev.to/zeeshansardar08/export-installed-wordpress-plugins-themes-to-csv-developer-friendly-method-2cpa</link>
      <guid>https://dev.to/zeeshansardar08/export-installed-wordpress-plugins-themes-to-csv-developer-friendly-method-2cpa</guid>
      <description>&lt;p&gt;If you manage WordPress websites, you've probably encountered this problem.&lt;/p&gt;

&lt;p&gt;You open a project and need to quickly answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What plugins are installed on this site?&lt;/li&gt;
&lt;li&gt;Which theme is active?&lt;/li&gt;
&lt;li&gt;How can I export this list for documentation or auditing?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Surprisingly, WordPress does not provide a built-in way to export installed plugins and themes.&lt;/p&gt;

&lt;p&gt;For developers and agencies managing multiple projects, this can become frustrating.&lt;/p&gt;

&lt;p&gt;So I built a small tool to solve this problem.&lt;/p&gt;

&lt;p&gt;In this article, I’ll show you how to export installed WordPress plugins and themes in seconds.&lt;/p&gt;

&lt;p&gt;If you just want the quick solution:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install Site Extensions Snapshot&lt;/li&gt;
&lt;li&gt;Go to Tools → Site Extensions Snapshot&lt;/li&gt;
&lt;li&gt;Click Export to CSV
You will instantly download a list of all installed plugins and themes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Plugin link:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://wordpress.org/plugins/site-extensions-snapshot/" rel="noopener noreferrer"&gt;https://wordpress.org/plugins/site-extensions-snapshot/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Developers Need a Plugin Snapshot
&lt;/h2&gt;

&lt;p&gt;In real-world development, having a snapshot of installed extensions is extremely useful.&lt;/p&gt;

&lt;p&gt;Here are a few common scenarios.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client Documentation&lt;/strong&gt;&lt;br&gt;
When handing over a project, you may need to document all plugins and themes used in the website.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Website Audits&lt;/strong&gt;&lt;br&gt;
Security or performance audits often require reviewing installed plugins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Troubleshooting&lt;/strong&gt;&lt;br&gt;
Plugin conflicts are common in WordPress. Having a full list helps identify potential issues faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migration Preparation&lt;/strong&gt;&lt;br&gt;
Before migrating a website, it's helpful to know exactly which extensions are installed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with WordPress
&lt;/h2&gt;

&lt;p&gt;WordPress lets you view plugins inside the dashboard, but it doesn't allow you to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Export the plugin list&lt;/li&gt;
&lt;li&gt;Download the plugin inventory&lt;/li&gt;
&lt;li&gt;Share it with a team&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most developers end up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manually copying plugin names&lt;/li&gt;
&lt;li&gt;Taking screenshots&lt;/li&gt;
&lt;li&gt;Writing custom scripts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This wastes time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Site Extensions Snapshot
&lt;/h2&gt;

&lt;p&gt;To make this easier, I built a small WordPress plugin called:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Site Extensions Snapshot&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It allows developers to export installed plugins and themes into a CSV file with one click.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plugin page:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://wordpress.org/plugins/site-extensions-snapshot/" rel="noopener noreferrer"&gt;https://wordpress.org/plugins/site-extensions-snapshot/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Plugin Does
&lt;/h2&gt;

&lt;p&gt;Once installed, the plugin allows you to:&lt;/p&gt;

&lt;p&gt;✔ Export installed plugins to CSV&lt;br&gt;
✔ Export installed themes to CSV&lt;br&gt;
✔ Create a quick snapshot of the site environment&lt;br&gt;
✔ Use the exported list for documentation or audits&lt;/p&gt;

&lt;p&gt;It's a lightweight utility tool designed to save developers time.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Use the Plugin
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — Install the Plugin&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Go to your WordPress dashboard:&lt;br&gt;
Plugins → Add New&lt;br&gt;
Search for:&lt;br&gt;
Site Extensions Snapshot&lt;br&gt;
Install and activate it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — Export Installed Plugins &amp;amp; Themes&lt;/strong&gt;&lt;br&gt;
After activating the plugin:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to Tools&lt;/li&gt;
&lt;li&gt;Click Site Extensions Snapshot&lt;/li&gt;
&lt;li&gt;Click Export to CSV
That's it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The plugin will download a CSV file containing all installed plugins and themes.&lt;br&gt;
You can now use this file for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Documentation&lt;/li&gt;
&lt;li&gt;Website audits&lt;/li&gt;
&lt;li&gt;Migration planning&lt;/li&gt;
&lt;li&gt;Team collaboration&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who This Plugin Is For
&lt;/h2&gt;

&lt;p&gt;This tool is useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WordPress developers&lt;/li&gt;
&lt;li&gt;Freelancers&lt;/li&gt;
&lt;li&gt;Agencies&lt;/li&gt;
&lt;li&gt;Website auditors&lt;/li&gt;
&lt;li&gt;DevOps teams managing WordPress environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you work with multiple WordPress projects, this tool can simplify documentation and save time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I Built This Plugin&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;While working on WordPress projects, I often needed a quick list of installed plugins and themes for documentation or troubleshooting.&lt;/p&gt;

&lt;p&gt;Since there wasn't an easy way to export that information, I decided to build a small tool that solves this problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;

&lt;p&gt;If you'd like to try the plugin, you can install it from the WordPress plugin directory.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://wordpress.org/plugins/site-extensions-snapshot/" rel="noopener noreferrer"&gt;https://wordpress.org/plugins/site-extensions-snapshot/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you try it, I’d love to hear feedback or suggestions from the developer community.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8g2m1wjx75bf055lilg3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8g2m1wjx75bf055lilg3.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
Sometimes the simplest tools solve real developer problems.&lt;/p&gt;

&lt;p&gt;If you regularly manage WordPress websites, having a quick way to export installed plugins and themes can make documentation and audits much easier.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>wordpress</category>
      <category>opensource</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
