<?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: Eduard</title>
    <description>The latest articles on DEV Community by Eduard (@edo911).</description>
    <link>https://dev.to/edo911</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%2F1304913%2Fb46afe2a-cf41-4fbc-8f13-9555d54e7a90.jpg</url>
      <title>DEV Community: Eduard</title>
      <link>https://dev.to/edo911</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/edo911"/>
    <language>en</language>
    <item>
      <title>Image SEO: Complete Guide to Optimizing Images for Search</title>
      <dc:creator>Eduard</dc:creator>
      <pubDate>Thu, 30 Jul 2026 07:37:16 +0000</pubDate>
      <link>https://dev.to/edo911/image-seo-complete-guide-to-optimizing-images-for-search-28lp</link>
      <guid>https://dev.to/edo911/image-seo-complete-guide-to-optimizing-images-for-search-28lp</guid>
      <description>&lt;p&gt;A while back I audited an e-commerce site that looked solid on paper. Fast hosting, clean code, decent content. Mobile PageSpeed score: 34. I kept digging until I checked the page weight. Homepage was pulling &lt;strong&gt;14MB&lt;/strong&gt; of images on first paint. Fourteen megabytes. For a homepage.&lt;/p&gt;

&lt;p&gt;Every product photo had been uploaded as a 3000px-wide PNG straight from the photographer’s camera. No compression, no resizing, no modern formats. Mobile users on 4G were downloading desktop-sized files. We converted everything to WebP, added proper responsive sizes, and got the total image payload down to about 800KB. Score jumped to 87. That single change — just the images — moved their Core Web Vitals from “poor” to “good” across the whole site.&lt;/p&gt;

&lt;p&gt;That’s when it clicked for me. Image SEO isn’t some nice-to-have checklist item. On a lot of sites it’s the biggest performance bottleneck you’ll find.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alt Text (and the mistakes I keep seeing)
&lt;/h2&gt;

&lt;p&gt;Alt text does two jobs that people constantly mix up: it helps screen-reader users understand the image, and it gives search engines context so the image can show up in Google Images. Good alt text does both at once.&lt;/p&gt;

&lt;p&gt;What I see in almost every audit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“image of product”&lt;/li&gt;
&lt;li&gt;or pure keyword stuffing: “buy blue widget online best price”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither helps anyone. Something specific works better:&lt;br&gt;&lt;br&gt;
“Woman hiking on mountain trail with red backpack at sunset”&lt;/p&gt;

&lt;p&gt;That’s useful for a screen reader &lt;em&gt;and&lt;/em&gt; it tells Google exactly what it’s looking at.&lt;/p&gt;

&lt;p&gt;Keep it under roughly 125 characters. Screen readers often cut off after that, so longer text just gets ignored. Decorative images (background patterns, spacers, pure design elements) should get an empty alt: &lt;code&gt;alt=""&lt;/code&gt;. That way screen readers skip them instead of announcing “image”.&lt;/p&gt;

&lt;p&gt;Early on I made a classic mistake myself. I used the same alt text for every color variant of a product — “Blue running shoe” for the blue, red, &lt;em&gt;and&lt;/em&gt; green versions. Google couldn’t tell them apart in image search. Each variant needs its own unique description.&lt;/p&gt;
&lt;h2&gt;
  
  
  File names — the five-minute fix most people skip
&lt;/h2&gt;

&lt;p&gt;I still open sites and see &lt;code&gt;IMG_4729.JPG&lt;/code&gt; and &lt;code&gt;photo-1.png&lt;/code&gt;. File names are a ranking signal for images. Renaming them costs nothing and takes almost no time.&lt;/p&gt;

&lt;p&gt;What I do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hyphens between words&lt;/li&gt;
&lt;li&gt;include the main keyword&lt;/li&gt;
&lt;li&gt;make it descriptive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;red-leather-wallet.jpg&lt;/code&gt; is already better than &lt;code&gt;IMG_4729.JPG&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
&lt;code&gt;seo-audit-checklist-2026.png&lt;/code&gt; tells Google (and future-you) exactly what the file is.&lt;/p&gt;
&lt;h2&gt;
  
  
  Formats — my actual take, not the marketing version
&lt;/h2&gt;

&lt;p&gt;WebP is the right default for most photos. You usually get 25–35% smaller files than JPEG at similar quality, and support is over 96%. For product shots and complex images it’s the clear winner.&lt;/p&gt;

&lt;p&gt;But I don’t buy the “WebP for everything” advice. On screenshot-heavy tutorial pages I’ve seen WebP create ugly compression artifacts around sharp text edges. In those cases PNG looked better even though the file was larger. Sometimes the “best” format isn’t the best for &lt;em&gt;your&lt;/em&gt; images.&lt;/p&gt;

&lt;p&gt;AVIF is technically stronger — up to 50% smaller than WebP — but support still sits around 80%. I use it as a progressive enhancement with a WebP fallback. Most people won’t notice, the ones on modern browsers get a faster load.&lt;/p&gt;

&lt;p&gt;SVG for logos and icons is non-negotiable. If your logo is still a PNG, fix it. Vectors scale cleanly and the files are tiny.&lt;/p&gt;

&lt;p&gt;Quick rule of thumb I use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;photos → WebP (or AVIF + WebP fallback)&lt;/li&gt;
&lt;li&gt;screenshots / text-heavy images → PNG&lt;/li&gt;
&lt;li&gt;logos &amp;amp; icons → SVG&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Responsive images — stop sending desktop files to phones
&lt;/h2&gt;

&lt;p&gt;This is the second biggest performance killer after raw file size. Without &lt;code&gt;srcset&lt;/code&gt;, every mobile visitor downloads the full desktop image.&lt;/p&gt;

&lt;p&gt;Basic pattern:&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;img&lt;/span&gt; 
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"image-800.jpg"&lt;/span&gt;
  &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"image-400.jpg 400w,
          image-800.jpg 800w,
          image-1200.jpg 1200w"&lt;/span&gt;
  &lt;span class="na"&gt;sizes=&lt;/span&gt;&lt;span class="s"&gt;"(max-width: 600px) 400px,
         (max-width: 1000px) 800px,
         1200px"&lt;/span&gt;
  &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Descriptive alt text"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Phone gets the 400px version, tablet 800px, desktop 1200px. On that e-commerce site, just adding responsive images cut average image payload by about 60% for mobile users. That’s the difference between a 3-second load and a 1-second load on typical 4G.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lazy loading — easy to get wrong
&lt;/h2&gt;

&lt;p&gt;Native lazy loading has been around since 2020. Just add &lt;code&gt;loading="lazy"&lt;/code&gt;:&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;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"image.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Description"&lt;/span&gt; &lt;span class="na"&gt;loading=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mistake I still see constantly: people lazy-load the hero image. Above-the-fold content should load immediately. Lazy-loading the main visual tanks LCP because the browser waits to fetch the most important image on the page.&lt;/p&gt;

&lt;p&gt;Also check your CDN or image optimization service. Some inject their own JavaScript lazy-loading that fights with the native attribute. Open Chrome DevTools and watch the network order — make sure the hero loads first and everything else waits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Image sitemaps — Google can’t index what it never finds
&lt;/h2&gt;

&lt;p&gt;Most images get discovered through the HTML. That fails for images loaded with JavaScript, CSS background images, or stuff buried in carousels and galleries.&lt;/p&gt;

&lt;p&gt;Putting images in your XML sitemap gives Google a direct path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;url&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;loc&amp;gt;&lt;/span&gt;https://example.com/page&lt;span class="nt"&gt;&amp;lt;/loc&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;image:image&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;image:loc&amp;gt;&lt;/span&gt;https://example.com/image.jpg&lt;span class="nt"&gt;&amp;lt;/image:loc&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;image:caption&amp;gt;&lt;/span&gt;Description of the image&lt;span class="nt"&gt;&amp;lt;/image:caption&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/image:image&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/url&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I’ve seen image search traffic double on sites after we added proper image entries to the sitemap. The images weren’t new — Google just couldn’t find them before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistakes that show up in almost every audit
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Oversized files&lt;/strong&gt; — 3000px wide when the layout only needs 800px. Resize before you upload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong format&lt;/strong&gt; — PNG product photos that should be WebP. The size difference hurts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing or useless alt text&lt;/strong&gt; — still the most common issue. In my last 20 audits, 14 had serious gaps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generic file names&lt;/strong&gt; — &lt;code&gt;banner-final-v3.jpg&lt;/code&gt; tells Google nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lazy loading&lt;/strong&gt; (or lazy-loading everything, including the hero).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaving EXIF data&lt;/strong&gt; — location and camera info. Some people keep it for context, I usually strip it on client sites for privacy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tools I actually open
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://squoosh.app/" rel="noopener noreferrer"&gt;Squoosh&lt;/a&gt; — still my go-to for quick one-off compression. The quality slider is excellent.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://tinypng.com/" rel="noopener noreferrer"&gt;TinyPNG&lt;/a&gt; — solid for batch PNG/WebP work.&lt;/li&gt;
&lt;li&gt;Lighthouse (Chrome DevTools) — run it after every change so you can see the real impact.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ai-seo-audit-seven.vercel.app/" rel="noopener noreferrer"&gt;AI SEO Copilot&lt;/a&gt; — useful for spotting missing alt text, wrong formats, and images that lack responsive sizing across a whole site.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick check
&lt;/h2&gt;

&lt;p&gt;If you want a fast look at your own images:&lt;br&gt;&lt;br&gt;
&lt;a href="https://ai-seo-audit-seven.vercel.app/free-seo-analyzer" rel="noopener noreferrer"&gt;Run a free image SEO analysis&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It flags alt text coverage, format issues, and whether lazy loading is set up correctly.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Eduard Tymchenko&lt;/em&gt;&lt;br&gt;&lt;br&gt;
Building tools that help people find and fix the SEO problems that actually move the needle.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>seo</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built a free SEO audit tool with 25+ features using Next.js 16, TypeScript, and AI — here's what I learned</title>
      <dc:creator>Eduard</dc:creator>
      <pubDate>Wed, 29 Jul 2026 02:31:30 +0000</pubDate>
      <link>https://dev.to/edo911/i-built-a-free-seo-audit-tool-with-25-features-using-nextjs-16-typescript-and-ai-heres-what-49np</link>
      <guid>https://dev.to/edo911/i-built-a-free-seo-audit-tool-with-25-features-using-nextjs-16-typescript-and-ai-heres-what-49np</guid>
      <description>&lt;p&gt;I spent 2 months building &lt;a href="https://ai-seo-audit-seven.vercel.app" rel="noopener noreferrer"&gt;AI SEO Copilot&lt;/a&gt; — a free SEO analyzer that checks 12 dimensions and generates AI recommendations. No signup, no paywall. Here's the technical breakdown.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2mri6yotop11yxbfxagd.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2mri6yotop11yxbfxagd.png" alt=" " width="800" height="505"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;Paste any URL, get a full audit in 60 seconds. 12-dimension analysis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Meta tags (title, description, OG tags, Twitter cards)&lt;/li&gt;
&lt;li&gt;Heading structure (H1-H6 hierarchy)&lt;/li&gt;
&lt;li&gt;Core Web Vitals (LCP, INP, CLS, FCP, TTFB)&lt;/li&gt;
&lt;li&gt;Schema markup validation + suggestions&lt;/li&gt;
&lt;li&gt;Content quality scoring&lt;/li&gt;
&lt;li&gt;Internal &amp;amp; external link analysis&lt;/li&gt;
&lt;li&gt;Image SEO (alt text, formats, lazy loading)&lt;/li&gt;
&lt;li&gt;Redirect chain detection&lt;/li&gt;
&lt;li&gt;Social media previews&lt;/li&gt;
&lt;li&gt;E-E-A-T signals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus 25 additional tools: schema validator, meta tag analyzer, keyword clusters, rank tracker, content editor, competitor diff, broken link checker, and more.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;Next.js 16 (App Router, Turbopack)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript strict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Styling&lt;/td&gt;
&lt;td&gt;Tailwind CSS v4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;Supabase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;Gemini → Groq → Mistral (fallback chain)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State&lt;/td&gt;
&lt;td&gt;Zustand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Components&lt;/td&gt;
&lt;td&gt;shadcn/ui&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy&lt;/td&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Scan Pipeline
&lt;/h2&gt;

&lt;p&gt;Every URL goes through this flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;URL Input → SSRF Check → Fetch HTML → Parse → Score 12 Dimensions → AI Insights → Response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The core scanner fetches raw HTML and extracts every SEO signal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;scanURL&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PageData&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;response&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="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User-Agent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mozilla/5.0 (compatible; AISEOCopilot/1.0)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;follow&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;html&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extractTitle&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="na"&gt;meta_description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extractMetaDescription&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="na"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extractH1&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="na"&gt;schema_markup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extractSchema&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="na"&gt;internal_links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;countInternalLinks&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="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;external_links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;countExternalLinks&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="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;images_without_alt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;countImagesWithoutAlt&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="na"&gt;word_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;countWords&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="c1"&gt;// ... 12 dimensions total&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;Each dimension gets scored, then weighted into an overall score. Title, H1, meta description, and schema each carry more weight than, say, image count.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Biggest Problems I Solved
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. SSRF Protection
&lt;/h3&gt;

&lt;p&gt;The biggest security risk — users can scan ANY URL. Someone could scan &lt;code&gt;http://169.254.169.254&lt;/code&gt; (AWS metadata) and leak server keys.&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;isSafeURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urlString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&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;urlString&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;hostname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&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="c1"&gt;// Block private IPs, localhost, cloud metadata&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&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;10&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;|172&lt;/span&gt;&lt;span class="se"&gt;\.(&lt;/span&gt;&lt;span class="sr"&gt;1&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;6-9&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;|2&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;|3&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;01&lt;/span&gt;&lt;span class="se"&gt;])\.&lt;/span&gt;&lt;span class="sr"&gt;|192&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;168&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;|127&lt;/span&gt;&lt;span class="se"&gt;\.)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Private IP blocked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;169.254.169.254&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;metadata.google.internal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cloud metadata blocked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every API endpoint that accepts a URL now runs through this before fetching.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. AI Provider Fallback Chain
&lt;/h3&gt;

&lt;p&gt;If Gemini fails, try Groq. If Groq fails, try Mistral. If all fail, generate offline insights from the scan data itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;aiGenerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;providers&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gemini&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;callGemini&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;groq&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;callGroq&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mistral&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;callMistral&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;provider&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&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;result&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;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// All failed — offline analysis&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;generateOfflineInsights&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Users always get results, even when APIs are down.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. AI Output Formatting
&lt;/h3&gt;

&lt;p&gt;LLMs return markdown with &lt;code&gt;**asterisks**&lt;/code&gt; everywhere. Users saw raw formatting in the UI.&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;stripMarkdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;text&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;\*\*([^&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;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$1&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="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;\*([^&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;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$1&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="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;[&lt;/span&gt;&lt;span class="sr"&gt;-*&lt;/span&gt;&lt;span class="se"&gt;]\s&lt;/span&gt;&lt;span class="sr"&gt;+/gm&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple fix, huge UX improvement.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Header Link Explosion
&lt;/h3&gt;

&lt;p&gt;The scanner counts every &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt; in the DOM. My header had both desktop mega menu AND mobile menu always rendered — just CSS-hidden. Scanner saw 90 internal links and flagged it as "Internal Link Overload."&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;// Before: Always in DOM (90 links)&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;mobileMenuOpen&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// After: Only mounts when open (34 links)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;mobileMenuOpen&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Rate Limiting in Serverless
&lt;/h3&gt;

&lt;p&gt;Vercel serverless = each request is a new instance. In-memory rate limiting doesn't work across instances. Currently using in-memory solution. Planning Upstash Redis for distributed state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest Results
&lt;/h2&gt;

&lt;p&gt;28 days live. Google Search Console:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2 clicks, 2,330 impressions&lt;/li&gt;
&lt;li&gt;Position 76.8 (page 8)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tool works well — tested against Ahrefs and Semrush, finds real issues. But without domain authority, Google doesn't care about features. Competing against DA 91 (Ahrefs) with DA ~5.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;WordPress plugin for easier distribution&lt;/li&gt;
&lt;li&gt;Redis for proper rate limiting&lt;/li&gt;
&lt;li&gt;Custom domain ($12/year — budget is tight)&lt;/li&gt;
&lt;li&gt;More blog content (15+ articles and growing)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;a href="https://ai-seo-audit-seven.vercel.app" rel="noopener noreferrer"&gt;ai-seo-audit-seven.vercel.app&lt;/a&gt; — free, no signup, paste any URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Build Together 🤝
&lt;/h2&gt;

&lt;p&gt;I'm open to collaboration, strategic partnerships, or bringing in a co-owner/partner to scale this project, boost marketing, and take it to the next level. &lt;/p&gt;

&lt;p&gt;Reach out to me on &lt;a href="https://www.linkedin.com/in/eduard-tymchenko-smm/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Built with Next.js 16, TypeScript, Supabase, and three AI providers. Open to feedback.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>nextjs</category>
      <category>typescript</category>
      <category>buildinpublic</category>
    </item>
  </channel>
</rss>
