<?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: Vikram Yadav</title>
    <description>The latest articles on DEV Community by Vikram Yadav (@vikram_yadav_2323b2df8bda).</description>
    <link>https://dev.to/vikram_yadav_2323b2df8bda</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%2F4198282%2F1122bd4d-fe9e-4423-aac3-10163b4e6e25.jpg</url>
      <title>DEV Community: Vikram Yadav</title>
      <link>https://dev.to/vikram_yadav_2323b2df8bda</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vikram_yadav_2323b2df8bda"/>
    <language>en</language>
    <item>
      <title>I built SiteScope, a free technical SEO audit tool that ranks what to fix first</title>
      <dc:creator>Vikram Yadav</dc:creator>
      <pubDate>Thu, 22 Oct 2026 04:29:58 +0000</pubDate>
      <link>https://dev.to/vikram_yadav_2323b2df8bda/i-built-sitescope-a-free-technical-seo-audit-tool-that-ranks-what-to-fix-first-1k1i</link>
      <guid>https://dev.to/vikram_yadav_2323b2df8bda/i-built-sitescope-a-free-technical-seo-audit-tool-that-ranks-what-to-fix-first-1k1i</guid>
      <description>&lt;p&gt;For my college course I built &lt;strong&gt;SiteScope&lt;/strong&gt; (&lt;a href="https://vikramyadav.me" rel="noopener noreferrer"&gt;vikramyadav.me&lt;/a&gt;), a free technical SEO audit tool. You enter a site, it crawls every page it can reach, and it tells you what to fix first. No sign-up needed to run an audit. If you want, you can create an account to have your sites re-audited daily or weekly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Two things bugged me about free SEO checkers.&lt;/p&gt;

&lt;p&gt;First, most test a single URL and hand back an unordered list. SiteScope follows internal links and the XML sitemap and checks every page it finds (up to 300 per audit). Then it ranks issues with one rule:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;priority = severity × share of pages affected&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A missing alt text on every page outranks a broken link on one page; a 5xx on half the site outranks both.&lt;/p&gt;

&lt;p&gt;Second, a lot of sites load their content with JavaScript. SiteScope reads raw HTML, the way most AI crawlers do, so you can check whether your content needs JavaScript to show up. The tool itself uses no JavaScript framework. Every page, including the report, is server-rendered, because a tool that talks about JavaScript SEO should be readable by a crawler that runs none.&lt;/p&gt;

&lt;p&gt;It also checks broken links, duplicate and near-duplicate pages (SimHash), the internal link graph (click depth, PageRank, orphan pages), titles, meta descriptions, H1s, canonicals, noindex and more. Reports download as PDF or JSON, and re-running an audit shows what was fixed and what is new since last time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI&lt;/strong&gt; (Python 3.12) with Jinja2 templates; audits run as FastAPI background tasks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL 16&lt;/strong&gt;, where each finished result is one JSONB column&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt; Compose with an Nginx reverse proxy and Certbot&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure VM&lt;/strong&gt;: one small VM (2 vCPU, 1 GiB RAM) running two containers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare&lt;/strong&gt; for DNS and proxying&lt;/li&gt;
&lt;li&gt;httpx + asyncio for crawling, BeautifulSoup for parsing, NetworkX for the link graph&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A URL fetcher is an SSRF hole.&lt;/strong&gt; If a stranger can type any URL, they will try the cloud metadata service or localhost. The guard refuses private, loopback and reserved addresses, checks every redirect hop, and connects to the exact IP it checked, so DNS rebinding can't swap the address in between.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't trust a header anyone can send.&lt;/strong&gt; My per-IP limit read Cloudflare's &lt;code&gt;CF-Connecting-IP&lt;/code&gt;, but anyone hitting the server's IP directly could send that header with a new value each time. Now it only believes that header when the connection really comes from Cloudflare's IP ranges, and the limit lives in Postgres so a restart doesn't reset it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep dependencies small on a small VM.&lt;/strong&gt; NetworkX's PageRank pulled in numpy and scipy, so I wrote my own PageRank and tested it against the reference. The scheduled re-audits run as a background task inside the app, with no cron, Celery or Redis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Severity alone gets the order wrong.&lt;/strong&gt; Multiplying by the share of pages affected fixed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An SEO tool shouldn't create thin pages.&lt;/strong&gt; Report pages send noindex and are disallowed in robots.txt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS moves need care.&lt;/strong&gt; Switching nameservers to Cloudflare replaces the whole zone, so I recreated the existing records first and issued SSL before turning on the proxy.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Some things are still open: alert emails come from a Gmail address for now (next step is sending from the site's own domain), and external link checks stop at 200 links per audit.&lt;/p&gt;

&lt;p&gt;Try it at &lt;a href="https://vikramyadav.me" rel="noopener noreferrer"&gt;vikramyadav.me&lt;/a&gt; and see the code on &lt;a href="https://github.com/vikramyadav1404/sitescope" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Feedback welcome!&lt;/p&gt;

</description>
      <category>seo</category>
      <category>python</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built SiteScope, a free technical SEO audit tool that ranks what to fix first</title>
      <dc:creator>Vikram Yadav</dc:creator>
      <pubDate>Wed, 21 Oct 2026 12:01:51 +0000</pubDate>
      <link>https://dev.to/vikram_yadav_2323b2df8bda/i-built-sitescope-a-free-technical-seo-audit-tool-that-ranks-what-to-fix-first-3j43</link>
      <guid>https://dev.to/vikram_yadav_2323b2df8bda/i-built-sitescope-a-free-technical-seo-audit-tool-that-ranks-what-to-fix-first-3j43</guid>
      <description>&lt;p&gt;For my college course I built &lt;strong&gt;SiteScope&lt;/strong&gt; (&lt;a href="https://vikramyadav.me" rel="noopener noreferrer"&gt;vikramyadav.me&lt;/a&gt;), a free technical SEO audit tool. You enter a site, it crawls up to 300 pages, and it tells you what to fix first. No sign-up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Two things bugged me about free SEO checkers.&lt;/p&gt;

&lt;p&gt;First, many test a single URL and hand back an unordered list. SiteScope follows internal links and the XML sitemap and checks every page it finds (up to 300 per audit). Then it ranks issues with one rule:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;priority = severity × share of pages affected&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A missing alt text on every page outranks a broken link on one page; a 5xx on half the site outranks both.&lt;/p&gt;

&lt;p&gt;Second, a lot of sites load their content with JavaScript. SiteScope reads raw HTML, the way the major AI crawlers do, so you can check whether your content needs JavaScript to show up. The tool itself uses no JavaScript framework. Every page, including the report, is server-rendered, because a tool that talks about JavaScript SEO should be readable by a crawler that runs none.&lt;/p&gt;

&lt;p&gt;It also checks broken links, duplicate and near-duplicate pages (SimHash), the internal link graph (click depth, PageRank, orphan pages), titles, meta descriptions, H1s, canonicals, noindex and more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI&lt;/strong&gt; (Python 3.12) with Jinja2 templates; audits run as FastAPI background tasks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL 16&lt;/strong&gt;, where each finished result is one JSONB column&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt; Compose with an Nginx reverse proxy and Certbot&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure VM&lt;/strong&gt;: one small VM (2 vCPU, 1 GiB RAM) running two containers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare&lt;/strong&gt; for DNS and proxying&lt;/li&gt;
&lt;li&gt;httpx + asyncio for crawling, BeautifulSoup for parsing, NetworkX for the link graph&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A URL fetcher is an SSRF hole.&lt;/strong&gt; If a stranger can type any URL, they will try the cloud metadata service or localhost. The guard refuses private, loopback and reserved addresses, and it checks every redirect hop, not just the first request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep dependencies small on a small VM.&lt;/strong&gt; NetworkX's PageRank pulled in numpy and scipy, so I wrote my own PageRank and tested it against the reference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Severity alone gets the order wrong.&lt;/strong&gt; Multiplying by the share of pages affected fixed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS moves need care.&lt;/strong&gt; Switching nameservers to Cloudflare replaces the whole zone, so I recreated the existing records first and issued SSL before turning on the proxy.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Some things are still open: a DNS-rebinding gap in the SSRF guard, and no PDF export or saved audits yet.&lt;/p&gt;

&lt;p&gt;Try it at &lt;a href="https://vikramyadav.me" rel="noopener noreferrer"&gt;vikramyadav.me&lt;/a&gt; and see the code on &lt;a href="https://github.com/vikramyadav1404/sitescope" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Feedback welcome!&lt;/p&gt;

</description>
      <category>seo</category>
      <category>python</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
