<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vishal Chauhan</title>
    <description>The latest articles on DEV Community by Vishal Chauhan (@siteory).</description>
    <link>https://dev.to/siteory</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%2F4172824%2F52f514e0-5c5a-408e-b644-1df812518d0b.png</url>
      <title>DEV Community: Vishal Chauhan</title>
      <link>https://dev.to/siteory</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/siteory"/>
    <language>en</language>
    <item>
      <title>Turning an SEO audit into tasks your AI coding agent can actually fix (Claude Code, Cursor, Codex)</title>
      <dc:creator>Vishal Chauhan</dc:creator>
      <pubDate>Fri, 09 Oct 2026 08:07:48 +0000</pubDate>
      <link>https://dev.to/siteory/turning-an-seo-audit-into-tasks-your-ai-coding-agent-can-actually-fix-claude-code-cursor-codex-386j</link>
      <guid>https://dev.to/siteory/turning-an-seo-audit-into-tasks-your-ai-coding-agent-can-actually-fix-claude-code-cursor-codex-386j</guid>
      <description>&lt;p&gt;A typical SEO audit export is a spreadsheet with a few hundred rows: "Missing canonical (18 pages)," "Duplicate title (11 pages)," "Orphan page," "Redirect chain." Pasting that into Claude Code, Cursor, or Codex with "fix these" usually goes badly. The agent edits the wrong template, "fixes" a warning that didn't matter, or changes &lt;code&gt;robots.txt&lt;/code&gt; in a way nobody reviews.&lt;/p&gt;

&lt;p&gt;Coding agents are good at SEO fixes because most technical SEO problems are template problems, and a template is code. The trick is the translation step between the audit and the agent. This post is the process I use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Sort findings into three buckets
&lt;/h2&gt;

&lt;p&gt;Not every SEO finding is a coding task. Before anything goes to an agent, I sort the list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delegate to the agent&lt;/strong&gt; (deterministic, lives in code, easy to verify):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Missing, duplicate, or self-contradicting &lt;code&gt;rel="canonical"&lt;/code&gt; tags&lt;/li&gt;
&lt;li&gt;Duplicate or empty &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; and meta descriptions produced by a template&lt;/li&gt;
&lt;li&gt;Leftover &lt;code&gt;noindex&lt;/code&gt; (meta tag or &lt;code&gt;X-Robots-Tag&lt;/code&gt; header) on production routes&lt;/li&gt;
&lt;li&gt;Sitemap problems: wrong host, non-canonical URLs, URLs that 404 or redirect&lt;/li&gt;
&lt;li&gt;Internal links pointing at redirects or 404s&lt;/li&gt;
&lt;li&gt;Redirect chains (A→B→C) that should be a single hop&lt;/li&gt;
&lt;li&gt;Missing or invalid JSON-LD (&lt;code&gt;Organization&lt;/code&gt;, &lt;code&gt;SoftwareApplication&lt;/code&gt;, &lt;code&gt;BlogPosting&lt;/code&gt;, &lt;code&gt;BreadcrumbList&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Missing &lt;code&gt;alt&lt;/code&gt; attributes on content images, and heading-level order in components&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Do it with the agent, but decide yourself&lt;/strong&gt; (code changes that encode a business decision):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;robots.txt&lt;/code&gt; rules, especially for AI crawlers&lt;/li&gt;
&lt;li&gt;Which URL is canonical when two pages overlap&lt;/li&gt;
&lt;li&gt;Redirect maps after a URL restructure&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hreflang&lt;/code&gt; sets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Keep away from the agent&lt;/strong&gt; (not code problems):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Content quality, search intent, and topic gaps&lt;/li&gt;
&lt;li&gt;Backlinks and authority&lt;/li&gt;
&lt;li&gt;Anything that's really a keyword-strategy decision&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first bucket usually covers most of what a technical audit flags. The second bucket is where an agent can do real damage if you let it decide on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Group by cause, not by URL
&lt;/h2&gt;

&lt;p&gt;Audits report symptoms per URL. Agents fix causes. "Missing canonical on 18 pages" isn't 18 tasks. It's usually one layout or one &lt;code&gt;generateMetadata&lt;/code&gt;-style function that never sets a canonical. Before writing tasks, look at the affected URLs and ask what they share: a route pattern (&lt;code&gt;/blog/[slug]&lt;/code&gt;), a layout, a CMS content type.&lt;/p&gt;

&lt;p&gt;One task per cause keeps diffs small and reviewable, and it stops the agent from patching 18 pages one by one, which you'd then have to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Write each task so it can be verified
&lt;/h2&gt;

&lt;p&gt;This template has worked well for me with all three agents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Task: Add self-referencing canonical to blog posts&lt;/span&gt;

&lt;span class="gs"&gt;**Finding:**&lt;/span&gt; 18 URLs under /blog/&lt;span class="err"&gt;*&lt;/span&gt; have no &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;"canonical"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;.
&lt;span class="gs"&gt;**Evidence:**&lt;/span&gt; &lt;span class="sb"&gt;`curl -s https://example.com/blog/hello-world | grep -i canonical`&lt;/span&gt; returns nothing.
&lt;span class="gs"&gt;**Likely cause:**&lt;/span&gt; app/blog/[slug]/page.tsx builds metadata without &lt;span class="sb"&gt;`alternates.canonical`&lt;/span&gt;.
&lt;span class="gs"&gt;**Change:**&lt;/span&gt; Set the canonical to the absolute production URL of the post
(https://example.com/blog/&lt;span class="nt"&gt;&amp;lt;slug&amp;gt;&lt;/span&gt;), built from the site's configured base URL.
&lt;span class="gs"&gt;**Scope:**&lt;/span&gt; Only files under app/blog/. Do not touch robots.txt, redirects, or other routes.
&lt;span class="gs"&gt;**Acceptance criteria:**&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Every /blog/&lt;span class="nt"&gt;&amp;lt;slug&amp;gt;&lt;/span&gt; page renders exactly one canonical tag.
&lt;span class="p"&gt;-&lt;/span&gt; The canonical is absolute, uses https, the production host, and no query string.
&lt;span class="p"&gt;-&lt;/span&gt; No other route's &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt; output changes.
&lt;span class="gs"&gt;**Verify:**&lt;/span&gt; &lt;span class="sb"&gt;`npm run build`&lt;/span&gt;, then run scripts/check_seo.py against urls-blog.txt.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each part does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Evidence&lt;/strong&gt; stops the agent from "fixing" something that isn't broken. If it can't reproduce the evidence, it should say so.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Likely cause&lt;/strong&gt; is a hint, not an order. Agents are good at confirming or correcting it once they open the file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope&lt;/strong&gt; is the most important line. Without it, agents tidy up nearby code, and that's how a canonical fix turns into a 40-file diff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Acceptance criteria&lt;/strong&gt; are what you'll review against. Make them observable in rendered HTML, not in source code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify&lt;/strong&gt; gives the agent a way to check its own work before handing it back.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Give the repo standing instructions
&lt;/h2&gt;

&lt;p&gt;Rules that apply to every SEO task belong in the repository, not in each prompt. Codex and Cursor read an &lt;code&gt;AGENTS.md&lt;/code&gt; file in the repo. Claude Code reads &lt;code&gt;CLAUDE.md&lt;/code&gt;, and you can pull in a shared file from there with an &lt;code&gt;@AGENTS.md&lt;/code&gt; import line. I keep the SEO rules in one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## SEO rules&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Production base URL: https://example.com (never localhost, preview, or staging hosts).
&lt;span class="p"&gt;-&lt;/span&gt; Canonicals: absolute, https, production host, no query strings, self-referencing by default.
&lt;span class="p"&gt;-&lt;/span&gt; Never add &lt;span class="sb"&gt;`noindex`&lt;/span&gt; or change robots.txt without an explicit instruction in the task.
&lt;span class="p"&gt;-&lt;/span&gt; Titles come from the page's own data; never hardcode the site name as the full title.
&lt;span class="p"&gt;-&lt;/span&gt; JSON-LD must only state facts visible on the page (no prices or ratings that aren't shown).
&lt;span class="p"&gt;-&lt;/span&gt; After any &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt; change, run: python3 scripts/check_seo.py urls.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last rule matters most. It turns "looks right" into a command with an exit code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Verify against rendered HTML, not the diff
&lt;/h2&gt;

&lt;p&gt;SEO bugs live in the HTML a crawler receives. A diff can look perfect while a parent layout overrides the canonical, or a client component injects a second &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;. So the check should fetch real pages. Here's a small standard-library Python script I give agents (and use myself). It takes a file of URLs, fails on missing or duplicate canonicals, canonicals pointing elsewhere, &lt;code&gt;noindex&lt;/code&gt;, redirects, error status codes, and titles duplicated across pages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;check_seo.py: verify basic SEO invariants for a list of URLs (one per line).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.error&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlsplit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urlunsplit&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;html.parser&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTMLParser&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HeadParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HTMLParser&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;titles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;canonicals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;robots&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="p"&gt;[]&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_starttag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# ignore &amp;lt;title&amp;gt; inside inline SVGs
&lt;/span&gt;            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;titles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;canonical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;a&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;canonicals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;href&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;a&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;robots&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;robots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_endtag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;titles&lt;/span&gt;&lt;span class="p"&gt;[&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="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Treat https://a.com and https://a.com/ as the same URL.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;urlunsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&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="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seo-check/1.0&lt;/span&gt;&lt;span class="sh"&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="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x_robots&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;geturl&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Robots-Tag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HeadParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redirects to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;titles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;titles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;titles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &amp;lt;title&amp;gt; tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;canonicals&lt;/span&gt;&lt;span class="p"&gt;)&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="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;canonicals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; canonical tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;canonicals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;canonical -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;canonicals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;noindex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;robots&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x_robots&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;noindex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;titles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;titles&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
    &lt;span class="n"&gt;by_title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;by_title&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAIL &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok   &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;by_title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;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;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAIL duplicate title &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt; on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; URLs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it against a local production build (&lt;code&gt;npm run build &amp;amp;&amp;amp; npm start&lt;/code&gt;) with a URL list that covers one page per template, then again against production after deploy. List URLs in their canonical form. A trailing-slash mismatch counts as a failure on purpose, because it's a real inconsistency.&lt;/p&gt;

&lt;p&gt;It's deliberately narrow. It doesn't judge title wording or content. It checks the invariants an agent is most likely to break.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Review like it's a migration
&lt;/h2&gt;

&lt;p&gt;Even with good tasks, review SEO pull requests more strictly than ordinary UI changes, because mistakes are quiet. A wrong canonical or a stray &lt;code&gt;noindex&lt;/code&gt; doesn't throw an error. It slowly removes pages from search. What I look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The diff stays inside the stated scope.&lt;/li&gt;
&lt;li&gt;No changes to &lt;code&gt;robots.txt&lt;/code&gt;, redirects, &lt;code&gt;middleware&lt;/code&gt;, or sitemap generation unless the task asked for them.&lt;/li&gt;
&lt;li&gt;The verification output is pasted into the PR description.&lt;/li&gt;
&lt;li&gt;Structured data passes Google's Rich Results Test for one example URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After deploying, re-crawl the affected URLs and use URL Inspection in Search Console on one or two of them. Search engines take days to weeks to reprocess changes, so judge the fix by the HTML now and the rankings later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where an audit tool fits
&lt;/h2&gt;

&lt;p&gt;Most of the work above is translation: turning "18 pages missing canonical" into one scoped, verifiable task. That's the part I got tired of doing by hand, and it's why I built &lt;a href="https://siteory.co" rel="noopener noreferrer"&gt;Siteory&lt;/a&gt;. It audits a site for SEO, AI-search readiness, and security, and every finding comes with evidence and a fix prompt you can paste into Claude, Codex, Cursor, or OpenCode. Paid reports also export Markdown files like &lt;code&gt;prioritized-fixes.md&lt;/code&gt;. It doesn't edit your code. You review the change, ship it, and re-scan.&lt;/p&gt;

&lt;p&gt;Whether you use a tool or a spreadsheet, the rules are the same. One cause per task, a hard scope, acceptance criteria you can see in the HTML, and a command that proves it. Agents are fast. Give them something they can be checked against.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>ai</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A 20-minute website security audit: CSP, HSTS, SPF/DMARC, and CAA explained for developers</title>
      <dc:creator>Vishal Chauhan</dc:creator>
      <pubDate>Fri, 09 Oct 2026 08:02:08 +0000</pubDate>
      <link>https://dev.to/siteory/a-20-minute-website-security-audit-csp-hsts-spfdmarc-and-caa-explained-for-developers-43gm</link>
      <guid>https://dev.to/siteory/a-20-minute-website-security-audit-csp-hsts-spfdmarc-and-caa-explained-for-developers-43gm</guid>
      <description>&lt;p&gt;Most of a site's basic security posture is visible from outside: HTTP response headers and a few DNS records. You don't need access to the codebase to check them, and you don't need a scanner to read them. You need &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;dig&lt;/code&gt;, and twenty minutes.&lt;/p&gt;

&lt;p&gt;This isn't a penetration test. It won't find an injection bug in your checkout. It tells you whether the cheap, standard protections are switched on and configured sensibly, and those are the protections most often missing.&lt;/p&gt;

&lt;p&gt;Set two variables and keep a notes file open:&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="nv"&gt;SITE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://example.com
&lt;span class="nv"&gt;DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;example.com
curl &lt;span class="nt"&gt;-sI&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SITE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; headers.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Minutes 0–3: HTTPS and HSTS
&lt;/h2&gt;

&lt;p&gt;First check that plain HTTP redirects to HTTPS:&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;-sI&lt;/span&gt; http://&lt;span class="nv"&gt;$DOMAIN&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"^(HTTP|location)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want a &lt;code&gt;301&lt;/code&gt; or &lt;code&gt;308&lt;/code&gt; to the &lt;code&gt;https://&lt;/code&gt; URL. Then look for HSTS:&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;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; strict-transport-security headers.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Strict-Transport-Security&lt;/code&gt; tells browsers to use HTTPS for your host for &lt;code&gt;max-age&lt;/code&gt; seconds, even if someone types &lt;code&gt;http://&lt;/code&gt; or clicks an old link. Browsers ignore it when it arrives over plain HTTP, so it only counts on HTTPS responses.&lt;/p&gt;

&lt;p&gt;A reasonable target is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Strict-Transport-Security: max-age=31536000; includeSubDomains
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be careful with two parts of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;includeSubDomains&lt;/code&gt;&lt;/strong&gt; applies to every subdomain. If a legacy &lt;code&gt;status.&lt;/code&gt; or &lt;code&gt;mail.&lt;/code&gt; host only serves HTTP, browsers will refuse to reach it. Check your subdomains before adding it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;preload&lt;/code&gt;&lt;/strong&gt; asks to be included in the HSTS preload list built into browsers. hstspreload.org requires a valid certificate, an HTTP→HTTPS redirect on the same host, HTTPS on all subdomains, and a header with &lt;code&gt;max-age&lt;/code&gt; of at least 31536000 plus both &lt;code&gt;includeSubDomains&lt;/code&gt; and &lt;code&gt;preload&lt;/code&gt;. Getting removed from the list is slow, so treat preload as a one-way door.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Minutes 3–9: Content-Security-Policy
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"content-security-policy"&lt;/span&gt; headers.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CSP is the most valuable header and the hardest to get right. It tells the browser which sources of script, style, frames, and so on are allowed, and that limits the damage when an XSS bug lets an attacker inject markup.&lt;/p&gt;

&lt;p&gt;What to look for when a policy exists:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;'unsafe-inline'&lt;/code&gt; in &lt;code&gt;script-src&lt;/code&gt;&lt;/strong&gt; without a nonce or hash. That undoes most of CSP's protection against XSS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wildcards&lt;/strong&gt; like &lt;code&gt;script-src https:&lt;/code&gt; or &lt;code&gt;*&lt;/code&gt;, which allow script from anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;object-src 'none'&lt;/code&gt; and no &lt;code&gt;base-uri&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;&amp;lt;base&amp;gt;&lt;/code&gt; tag injection can redirect relative script URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;frame-ancestors&lt;/code&gt;.&lt;/strong&gt; This is the modern way to stop other sites framing yours (clickjacking).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If there's no policy at all, don't write a strict one and ship it in one go. Something will break. Roll it out in report-only mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'; report-to csp
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Collect the violation reports for a week, fix the legitimate sources, then switch the header name to &lt;code&gt;Content-Security-Policy&lt;/code&gt;. Nonce-based policies need a fresh random nonce on every response, so pages served entirely from a static cache need hashes instead.&lt;/p&gt;

&lt;p&gt;Two gotchas: &lt;code&gt;frame-ancestors&lt;/code&gt; and reporting directives don't work in a &lt;code&gt;&amp;lt;meta http-equiv&amp;gt;&lt;/code&gt; CSP, only in a real header. And a policy that's only report-only gives no protection at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minutes 9–12: the small headers
&lt;/h2&gt;

&lt;p&gt;These take a line each and are easy to check:&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;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"x-content-type-options|referrer-policy|permissions-policy|x-frame-options|x-xss-protection"&lt;/span&gt; headers.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;X-Content-Type-Options: nosniff&lt;/code&gt;&lt;/strong&gt; stops browsers guessing a response's MIME type. Turn it on everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Referrer-Policy: strict-origin-when-cross-origin&lt;/code&gt;&lt;/strong&gt; sends only your origin, not full URLs with paths and query strings, to other sites. Modern browsers already default to this, but setting it explicitly protects you from older clients and accidental overrides.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Permissions-Policy&lt;/code&gt;&lt;/strong&gt; turns off browser features you don't use, for example &lt;code&gt;camera=(), microphone=(), geolocation=()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;X-Frame-Options: DENY&lt;/code&gt; or &lt;code&gt;SAMEORIGIN&lt;/code&gt;&lt;/strong&gt; is the legacy clickjacking control. Keep it for older browsers. When CSP &lt;code&gt;frame-ancestors&lt;/code&gt; is present, modern browsers use that instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;X-XSS-Protection&lt;/code&gt;&lt;/strong&gt; is deprecated. The old browser XSS filters it controlled have been removed, and in some cases they caused leaks of their own. Omit it or set it to &lt;code&gt;0&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Minutes 12–14: cookies
&lt;/h2&gt;

&lt;p&gt;Look at every &lt;code&gt;Set-Cookie&lt;/code&gt; header, especially after logging in:&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;-sI&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SITE&lt;/span&gt;&lt;span class="s2"&gt;/login"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; set-cookie
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Session cookies should have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Secure&lt;/code&gt;, so they're only sent over HTTPS.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HttpOnly&lt;/code&gt;, so page JavaScript can't read them. This is your second line of defence if XSS happens.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SameSite=Lax&lt;/code&gt; or &lt;code&gt;Strict&lt;/code&gt;, which helps against cross-site request forgery.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;__Host-&lt;/code&gt; prefix is worth knowing: a cookie named &lt;code&gt;__Host-session&lt;/code&gt; is only accepted if it's &lt;code&gt;Secure&lt;/code&gt;, has &lt;code&gt;Path=/&lt;/code&gt;, and has no &lt;code&gt;Domain&lt;/code&gt; attribute. That stops a compromised subdomain from setting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minutes 14–18: email authentication (SPF, DKIM, DMARC)
&lt;/h2&gt;

&lt;p&gt;These are DNS records, not HTTP headers, but they belong in a website audit. If your domain can be spoofed, attackers can send convincing phishing "from" your brand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short TXT &lt;span class="nv"&gt;$DOMAIN&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; spf
dig +short TXT _dmarc.&lt;span class="nv"&gt;$DOMAIN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;SPF&lt;/strong&gt; lists the servers allowed to send mail for the domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=spf1 include:_spf.google.com include:sendgrid.net ~all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Things that go wrong: &lt;strong&gt;two SPF records&lt;/strong&gt; (you're only allowed one, and two makes SPF fail), and going over the &lt;strong&gt;10 DNS-lookup limit&lt;/strong&gt; that SPF sets. Every &lt;code&gt;include&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;mx&lt;/code&gt;, &lt;code&gt;exists&lt;/code&gt;, and &lt;code&gt;redirect&lt;/code&gt; counts, including nested ones. Too many includes cause a &lt;code&gt;permerror&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DKIM&lt;/strong&gt; signs outgoing mail. Your email provider gives you a selector and a public key to publish at &lt;code&gt;selector._domainkey.example.com&lt;/code&gt;. You can't find the selector by guessing, so check your provider's settings or the &lt;code&gt;DKIM-Signature&lt;/code&gt; header (&lt;code&gt;s=&lt;/code&gt;) of a message you sent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DMARC&lt;/strong&gt; tells receivers what to do when SPF or DKIM fail alignment, and where to send reports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start at &lt;code&gt;p=none&lt;/code&gt; to collect reports, then move to &lt;code&gt;p=quarantine&lt;/code&gt; and finally &lt;code&gt;p=reject&lt;/code&gt; once every legitimate sender passes. Since February 2024, Gmail and Yahoo require bulk senders to have DMARC in place, so this isn't optional if you send marketing email at volume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Domains that never send mail&lt;/strong&gt; should say so explicitly, because parked domains get spoofed too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com.         TXT  "v=spf1 -all"
_dmarc.example.com.  TXT  "v=DMARC1; p=reject;"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Minutes 18–19: CAA
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short CAA &lt;span class="nv"&gt;$DOMAIN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A CAA record lists which certificate authorities may issue certificates for your domain. CAs have been required to check it before issuing since 2017. Without it, any publicly trusted CA may issue for you. With it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com.  CAA 0 issue "letsencrypt.org"
example.com.  CAA 0 issuewild ";"
example.com.  CAA 0 iodef "mailto:security@example.com"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That allows Let's Encrypt, forbids wildcard certificates, and says where to report violations. CAs look up CAA records starting at the exact hostname and climb the DNS tree until they find one, so a record on the apex covers subdomains that don't have their own.&lt;/p&gt;

&lt;p&gt;The common mistake is forgetting a CA you actually use. If your CDN or hosting provider issues certificates for you, include its CA(s), or the next automatic renewal will fail. Check the provider's docs before you publish the record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minute 19–20: security.txt and writing it up
&lt;/h2&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;"&lt;/span&gt;&lt;span class="nv"&gt;$SITE&lt;/span&gt;&lt;span class="s2"&gt;/.well-known/security.txt"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RFC 9116 defines &lt;code&gt;security.txt&lt;/code&gt; as the place researchers look for how to report a vulnerability. &lt;code&gt;Contact&lt;/code&gt; and &lt;code&gt;Expires&lt;/code&gt; are required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Contact: mailto:security@example.com
Expires: 2027-10-01T00:00:00.000Z
Policy: https://example.com/security-policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then write the findings down in priority order, and separate &lt;strong&gt;vulnerabilities&lt;/strong&gt; from &lt;strong&gt;observations&lt;/strong&gt;. A missing CAA record isn't the same as a session cookie without &lt;code&gt;HttpOnly&lt;/code&gt;. I'd order the fixes like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No HTTPS redirect or HSTS on login and app pages.&lt;/li&gt;
&lt;li&gt;Session cookies missing &lt;code&gt;Secure&lt;/code&gt; or &lt;code&gt;HttpOnly&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;No DMARC, or SPF broken by duplicates or the lookup limit.&lt;/li&gt;
&lt;li&gt;No CSP, or a CSP with &lt;code&gt;'unsafe-inline'&lt;/code&gt; scripts. Start report-only.&lt;/li&gt;
&lt;li&gt;Missing small headers.&lt;/li&gt;
&lt;li&gt;CAA and security.txt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For each finding, record the exact header or record you saw, the line you want, and how you'll verify it (the &lt;code&gt;curl&lt;/code&gt; or &lt;code&gt;dig&lt;/code&gt; command above). That last part is what turns an audit into a pull request.&lt;/p&gt;

&lt;p&gt;If you'd rather not run the HTTP side of this by hand on every site you look after, &lt;a href="https://siteory.co" rel="noopener noreferrer"&gt;Siteory&lt;/a&gt;'s security audit checks the header, CSP, and cookie rules automatically. It labels each result as a finding or an observation and gives you the fix and a re-check. The DNS checks (SPF, DMARC, CAA) are still a &lt;code&gt;dig&lt;/code&gt; away, and doing the whole thing by hand once is the best way to learn what each control actually does.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>devops</category>
      <category>dns</category>
    </item>
    <item>
      <title>How to check if your website shows up in AI search: an AEO/GEO checklist for ChatGPT, Perplexity, and Google AI Overviews</title>
      <dc:creator>Vishal Chauhan</dc:creator>
      <pubDate>Fri, 09 Oct 2026 08:00:43 +0000</pubDate>
      <link>https://dev.to/siteory/how-to-check-if-your-website-shows-up-in-ai-search-an-aeogeo-checklist-for-chatgpt-perplexity-4kn</link>
      <guid>https://dev.to/siteory/how-to-check-if-your-website-shows-up-in-ai-search-an-aeogeo-checklist-for-chatgpt-perplexity-4kn</guid>
      <description>&lt;p&gt;When someone asks me why ChatGPT or Perplexity never mentions their site, the cause is usually boring. The crawler was blocked, the page was empty until JavaScript ran, or a stray &lt;code&gt;noindex&lt;/code&gt; was sitting in a template.&lt;/p&gt;

&lt;p&gt;You can't force an AI engine to cite you. You can make sure nothing technical stops it from fetching, reading, and quoting your page. That part is checkable. Work through this list in order, because each step depends on the one before it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Know which bots you're dealing with
&lt;/h2&gt;

&lt;p&gt;"AI search" isn't one crawler. Each vendor runs separate user agents for search, training, and live user requests, and robots.txt treats each one separately.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Bot for search/answers&lt;/th&gt;
&lt;th&gt;Bot for model training&lt;/th&gt;
&lt;th&gt;User-triggered fetches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT (OpenAI)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OAI-SearchBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GPTBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ChatGPT-User&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perplexity&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PerplexityBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(not used for training, per Perplexity)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Perplexity-User&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude (Anthropic)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Claude-SearchBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ClaudeBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Claude-User&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google AI Overviews / AI Mode&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Googlebot&lt;/code&gt; (same as Search)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Google-Extended&lt;/code&gt; (a robots.txt token, not a crawler)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few details matter here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI's docs say a site that blocks &lt;code&gt;OAI-SearchBot&lt;/code&gt; won't be shown in ChatGPT search answers, apart from navigational links. You can block &lt;code&gt;GPTBot&lt;/code&gt; for training and still allow &lt;code&gt;OAI-SearchBot&lt;/code&gt; for search. The two settings are independent.&lt;/li&gt;
&lt;li&gt;Google says AI Overviews and AI Mode are part of Search, so &lt;code&gt;Googlebot&lt;/code&gt; is the control. &lt;code&gt;Google-Extended&lt;/code&gt; covers Gemini training and grounding in some of Google's other systems. It doesn't decide whether you appear in AI Overviews.&lt;/li&gt;
&lt;li&gt;User-triggered fetchers (&lt;code&gt;ChatGPT-User&lt;/code&gt;, &lt;code&gt;Perplexity-User&lt;/code&gt;) act for a person asking a question. Both vendors note that robots.txt may not apply to them the way it applies to crawlers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Decide your policy first. A common one is to allow the search bots and decide separately about training bots.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Read your robots.txt the way a crawler does
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;https://yoursite.com/robots.txt&lt;/code&gt; and read it carefully. The rule that catches people is that &lt;strong&gt;a crawler follows only the group that matches its user agent most specifically.&lt;/strong&gt; If no group names it, it falls back to &lt;code&gt;User-agent: *&lt;/code&gt;. It doesn't merge the two.&lt;/p&gt;

&lt;p&gt;So this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-agent: *
Disallow: /admin
Disallow: /checkout

User-agent: GPTBot
Allow: /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;gives GPTBot access to &lt;code&gt;/admin&lt;/code&gt; and &lt;code&gt;/checkout&lt;/code&gt;, because the &lt;code&gt;GPTBot&lt;/code&gt; group replaces the &lt;code&gt;*&lt;/code&gt; group for that bot. If you add named groups for AI bots, repeat your &lt;code&gt;Disallow&lt;/code&gt; lines in each of them.&lt;/p&gt;

&lt;p&gt;The reverse mistake is just as common: a blanket &lt;code&gt;Disallow: /&lt;/code&gt; under a named bot, copied from a "block AI" snippet years ago, which now also blocks the search bot you want. Check for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No &lt;code&gt;Disallow&lt;/code&gt; for &lt;code&gt;OAI-SearchBot&lt;/code&gt;, &lt;code&gt;PerplexityBot&lt;/code&gt;, &lt;code&gt;Claude-SearchBot&lt;/code&gt;, &lt;code&gt;Googlebot&lt;/code&gt;, or &lt;code&gt;Bingbot&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;Sitemap:&lt;/code&gt; line pointing at the &lt;strong&gt;production&lt;/strong&gt; host, not staging.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Check the layer robots.txt doesn't control: your CDN and firewall
&lt;/h2&gt;

&lt;p&gt;robots.txt is a request. Your CDN or WAF actually enforces access. A site can allow &lt;code&gt;OAI-SearchBot&lt;/code&gt; in robots.txt and still serve it a 403 or a challenge page.&lt;/p&gt;

&lt;p&gt;Cloudflare, for example, announced in July 2025 that new domains block AI crawlers by default unless the owner allows them, and other CDNs have similar toggles. Check your CDN's bot settings, your WAF's user-agent and country rules, and any rate limits a burst of crawler requests could trip.&lt;/p&gt;

&lt;p&gt;A quick sanity test from your terminal:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;This only tests rules keyed on the user-agent string. Real crawlers also come from published IP ranges (OpenAI and Perplexity both publish JSON files of them), and some WAFs verify those ranges. Your server logs are the real proof. Search them for &lt;code&gt;OAI-SearchBot&lt;/code&gt;, &lt;code&gt;PerplexityBot&lt;/code&gt;, and &lt;code&gt;Claude-SearchBot&lt;/code&gt; and look at the status codes they got.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Make sure the content exists without JavaScript
&lt;/h2&gt;

&lt;p&gt;Googlebot renders JavaScript. Many AI fetchers don't. In December 2024 Vercel published an analysis of crawler traffic and found that the crawlers from OpenAI and Anthropic downloaded JavaScript files but didn't execute them. If your key copy, pricing, or docs only show up after client-side rendering, those bots may see an empty shell.&lt;/p&gt;

&lt;p&gt;The test takes ten seconds:&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; https://yoursite.com/pricing | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"your exact pricing sentence"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the grep finds nothing but you can see the sentence in a browser, that content is rendered on the client. Fixes, in rough order of effort:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use server-side rendering or static generation for marketing pages, docs, and pricing. In Next.js, Nuxt, Astro, or SvelteKit this is often a configuration change rather than a rewrite.&lt;/li&gt;
&lt;li&gt;Put headings, the first paragraph, and any JSON-LD in the initial HTML.&lt;/li&gt;
&lt;li&gt;Don't hide answer content behind tabs or accordions that only load their content on click.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Confirm the page is indexable and snippet-eligible
&lt;/h2&gt;

&lt;p&gt;For Google, the requirement is stated plainly. To show up as a supporting link in AI Overviews or AI Mode, a page has to be indexed and eligible to appear in Search with a snippet. Google also says there are no extra technical requirements beyond that.&lt;/p&gt;

&lt;p&gt;So look for the things that quietly remove eligibility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;meta name="robots" content="noindex"&amp;gt;&lt;/code&gt; or an &lt;code&gt;X-Robots-Tag: noindex&lt;/code&gt; header left over from staging.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nosnippet&lt;/code&gt;, or a very small &lt;code&gt;max-snippet&lt;/code&gt;, which limits what can be shown or quoted.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;rel="canonical"&lt;/code&gt; pointing to a different URL, so the page you care about isn't the one that gets indexed.&lt;/li&gt;
&lt;li&gt;Redirect chains, soft 404s, or pages that return 200 with an error message.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use the URL Inspection tool in Google Search Console to see what Googlebot actually got. Do the same in Bing Webmaster Tools. Bing's index sits behind Bing search and Microsoft Copilot, and you can import your verified sites from Search Console in a couple of clicks.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Write passages that can be lifted out and quoted
&lt;/h2&gt;

&lt;p&gt;Once the plumbing works, the question is whether there's a sentence worth quoting. Answer engines pull passages, not whole pages. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Answer the question in the first sentence or two under a heading, then give detail.&lt;/li&gt;
&lt;li&gt;Phrase headings the way people ask ("How long does X take?").&lt;/li&gt;
&lt;li&gt;Keep paragraphs self-contained, with specific units, versions, and dates.&lt;/li&gt;
&lt;li&gt;Show a "last updated" date and a named author, and don't keep the only copy of a fact inside an image.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is AI-specific. It's what won featured snippets for years, which is why I treat AEO and GEO as SEO done carefully rather than as a separate channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Add structured data that matches the page
&lt;/h2&gt;

&lt;p&gt;Google says you don't need any special schema to appear in AI features. Structured data still helps machines tell what a page is about, as long as it &lt;strong&gt;matches the visible text&lt;/strong&gt;. For most product sites that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Organization&lt;/code&gt; and &lt;code&gt;WebSite&lt;/code&gt; on the homepage.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SoftwareApplication&lt;/code&gt; or &lt;code&gt;Product&lt;/code&gt; on the product page, with a price only if the price is shown on the page.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Article&lt;/code&gt; or &lt;code&gt;BlogPosting&lt;/code&gt; (with &lt;code&gt;author&lt;/code&gt; and &lt;code&gt;dateModified&lt;/code&gt;) on posts.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BreadcrumbList&lt;/code&gt; on deeper pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Validate with Google's Rich Results Test. Markup that claims things the page doesn't show is worse than none.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Treat llms.txt as optional
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;llms.txt&lt;/code&gt; is a proposed convention, a Markdown file at your site root that maps your important pages for language models. It's cheap to add, but don't expect it to change rankings or citations. Google says you don't need new machine-readable or AI text files to appear in its AI features. If you add one, keep it short and accurate.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Measure what you can
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search Console:&lt;/strong&gt; clicks and impressions from AI Overviews and AI Mode count toward the normal Performance report under the "Web" search type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server logs:&lt;/strong&gt; count requests and status codes for &lt;code&gt;OAI-SearchBot&lt;/code&gt;, &lt;code&gt;PerplexityBot&lt;/code&gt;, &lt;code&gt;Claude-SearchBot&lt;/code&gt;, &lt;code&gt;ChatGPT-User&lt;/code&gt;, and &lt;code&gt;Perplexity-User&lt;/code&gt;. Steady 200s mean you're reachable. 403s mean step 3 needs work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics:&lt;/strong&gt; watch referral traffic from &lt;code&gt;chatgpt.com&lt;/code&gt;, &lt;code&gt;perplexity.ai&lt;/code&gt;, and &lt;code&gt;claude.ai&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spot checks:&lt;/strong&gt; once a month, ask the engines the questions your pages answer and record which URLs they cite. It's crude but honest.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ol&gt;
&lt;li&gt;Policy decided: which search bots, training bots, and user fetchers you allow.&lt;/li&gt;
&lt;li&gt;robots.txt allows the search bots, with no accidental &lt;code&gt;Disallow: /&lt;/code&gt; and named groups that repeat your private paths.&lt;/li&gt;
&lt;li&gt;The CDN and WAF don't block or challenge those bots, which the logs show getting 200s.&lt;/li&gt;
&lt;li&gt;Key content is in the server HTML, so a &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;grep&lt;/code&gt; finds it.&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;noindex&lt;/code&gt; or &lt;code&gt;nosnippet&lt;/code&gt; and no wrong canonical. URL Inspection is clean in Google and Bing.&lt;/li&gt;
&lt;li&gt;Answer-first headings and self-contained paragraphs, with dates and author shown.&lt;/li&gt;
&lt;li&gt;Structured data matches the visible text and passes validation.&lt;/li&gt;
&lt;li&gt;llms.txt is optional, short, and accurate.&lt;/li&gt;
&lt;li&gt;Measurement set up in Search Console, logs, and referrals.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I built &lt;a href="https://siteory.co" rel="noopener noreferrer"&gt;Siteory&lt;/a&gt; to run a lot of these checks in one pass. It crawls a bounded sample of your public pages, checks robots rules, sitemaps, canonicals, JSON-LD, thin copy, and llms.txt, and explains each finding along with how to fix it. It doesn't promise citations, and nothing honest can. Everything above also works by hand with &lt;code&gt;curl&lt;/code&gt;, Search Console, and an hour of attention. Either way, fix the plumbing before you spend money on prompt-tracking tools. A monitoring dashboard won't help if the crawler is getting a 403.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>ai</category>
      <category>webdev</category>
      <category>aeo</category>
    </item>
  </channel>
</rss>
