Cleaning Up My Blog's SEO and AEO with Claude in GitHub Copilot

Page content

I’ve been running this blog on Hugo for a long time, and like most blogs with real history, it’s accumulated some tech debt. Some of it is my own fault. This blog started on WordPress, where it ran for seven years, and I migrated it to Hugo back in 2021. That migration exported those seven years of posts straight from WordPress’s database into raw Markdown, HTML tags, inline styles, and all. I am not a good HTML programmer, and it showed. Broken tags, stray   entities, image markup that only sort of worked, all of it scattered across dozens of WordPress migrated posts.

So I decided, let’s use an LLM to clean this up and improve the blog for configuration best practices, SEO, and AEO. And honestly, I didn’t know AEO was even a thing until some folks at work mentioned it. And I know agents are reading my blog more and more, and people are reading blogs less and less, using AI tools instead. The first two times I sent Claude after this blog were cleanup, not SEO. The first round, earlier this year, fixed the worst of that broken HTML left over from the migration so pages would actually render correctly. The second round was a pass on spelling and grammar across the archive. I want to pull the whole story together here and share it with y’all because you can get a bunch of easy, quick wins to improve your blog’s look and how search and AI engines view it.

Why This Setup Works So Well With an Agent

Let’s talk about the stack underneath the blog. Hugo takes plain Markdown files and builds a static HTML site out of them, no database, no server-side rendering at request time. All of it, the posts, the templates, the site configuration, and the Azure Static Web Apps deployment settings, lives as text files in the same GitHub repo. A GitHub Action builds the Hugo site and publishes it to Azure Static Web Apps on every push to main.

That matters more than it should. There’s no CMS database to query, no admin API to authenticate against, no separate infrastructure repo to find. Everything an agent needs, content, layout logic, redirect rules, deploy config, is just text in one place it can already read, search, and edit directly. That’s the whole reason a coding agent can audit and fix a blog’s SEO and AEO the same way it would refactor a codebase.

Round One: Getting the Basics In Place

Example prompt: “This blog has never had any real SEO work done on it. Look at how the site builds its page headers, and add proper meta tags and a robots.txt. Let’s kick off the process to SEO the blog. Tell me any other ideas you have”

My first SEO pass was small and mostly foundational. I added real meta tags and robots.txt scaffolding to baseof.html, wired up a custom link render hook, and added an author box partial so posts actually show who wrote them. I also found and deleted a stray draft post that had no business being in the content directory. Nothing glamorous, just the plumbing every site needs and mine didn’t have yet.

Round Two: Descriptions, Structured Data, and Performance

Example prompt: “Go through all of the posts and find anything missing a meta description, then write real ones based on what each post actually says. While you’re in there, check for inconsistent category names and clean those up too, and see if the site could use a better 404 page. Tell me any other ideas you have”

This is where the site started actually improving instead of just getting cleaner:

  • Added real meta descriptions to 30 evergreen technical posts
  • Noindexed tag taxonomy pages while keeping categories canonical, which turned out to be the pattern the entire noindex system in this post is built on
  • Built a custom sitemap.xml with per-kind priority and age-based changefreq instead of Hugo’s flat default
  • Added Person JSON-LD to /about/ with sameAs links to LinkedIn, Bluesky, GitHub, and Pluralsight
  • Async-loaded Google Fonts and lazy-loaded images site-wide. Pure performance work, but page speed is a ranking factor too
  • Built a proper custom 404 page with recent posts and category navigation instead of a dead end
  • Consolidated inconsistent category names: Powershell to PowerShell (28 posts), SQL to SQL Server (124 posts), with a 301 redirect so old links kept working
  • Added 10 new taxonomy landing pages with real descriptions instead of Hugo’s bare auto-generated list
  • Cleaned up the header pages: a stale email address on /contact/, broken hyphenation and tracking-junk Amazon links on /publications/, two dead links on /pure-storage-links/

Round Three: The Big One

This time was the deepest pass: an honest audit first, then fixing everything the audit found, then adding the newer answer-engine-optimization layer on top.

The Starting Point

Example prompt: “Do a full SEO and AEO audit of this blog. Don’t change anything yet; give me an honest, categorized list of what’s wrong, things like broken links, missing metadata, bad alt text, or thin content.”

I asked for an honest audit first, no changes, tell me what’s wrong. The agent came back with a real list: broken internal links, meta descriptions falling back to a generic site description on most posts, images with alt text like NewImage or a raw macOS screenshot filename, and a chunk of posts that were dragging down my indexing without giving anything back.

I reviewed the list, and we (Claude and I) started fixing things, one category at a time.

This started because my Google Search Console coverage report showed pages returning 404s, and some pages weren’t being indexed. Some old aliases, some typos in internal links, some pagination artifacts nobody would ever click. We pulled the full list and fixed 14 of them with a mix of corrected links in the source posts and 301 redirects. This change goes into the Azure Static Web App config in staticwebapp.config.json; below is an example of one of the redirect rules it built.

Here’s the pattern for most of them:

{
    "route": "/posts/2026-05-08-using-t-sql-snapshot-backup-hyperv-edition",
    "redirect": "/posts/2026-05-08-using-t-sql-snapshot-backup-hyper-v-edition/",
    "statusCode": 301
}

Important: I made sure to ask Claude to verify these changes as it went along. So in this case, after it pushed the code and the GitHub action built and published the site, Claude verified that every target actually resolves to real content via a 301 redirect, since redirecting to a page that 404s is worse than the original problem. That exact mistake showed up later in this post; more on that below.

Meta descriptions matter more than people think. Without one, Hugo falls back to either the post’s summary or the site-wide description, so many pages end up with duplicate or generic descriptions in search results. On top of the 30 from round two, we found 109 more posts with no description field in the frontmatter, going all the way back to 2014. That’s 139 posts across both passes.

The agent read the intro paragraph of each post and wrote a real, specific description for it, then added it to the frontmatter:

description: "Snapshot SQL Server databases spanning multiple Pure Storage FlashArrays using T-SQL Snapshot Backup with coordinated write I/O freeze across arrays."

109 of these, one at a time, each based on what the post actually says rather than a generic template. A very good use case for an LLM here: summarizing the existing content.

Image Alt Text

This is what surprised me. Fifty images across the archive had alt text like NewImage (a leftover from an old screenshot tool), a raw macOS screenshot filename like Screen Shot 2018-10-13 at 8.24.02 AM, or nothing at all. None of that helps a screen reader, and none of it helps an image show up in search. Claude actually “looked” at each image.

For each one, the agent looked at the actual image and wrote a real description:

<img src="/images/NTFSMFT.png" alt="Diagram of an NTFS Master File Table record showing standard info, file name, and data run headers mapping to disk block numbers" />

Fifty images fixed. Zero remaining generic or missing alt text anywhere on the site, confirmed with a full-site grep before we called it done. This was super cool to see.

Analytics and a Typo Sweep

While we were in there, I also added Ahrefs Web Analytics alongside the Google Analytics tag I already had, so I can cross-check traffic and keyword data between the two instead of relying on a single source. And since the agent was already reading every post to write descriptions and alt text, I had it check the content for common typos while it was at it. It found exactly one: “Enviroment” instead of “Environment,” in both a title and a heading on an old s5cmd post. Small, but free to fix once you’re already looking.

The AEO Layer: Structured Data for AI Answer Engines

Example prompt: “I keep hearing about AEO. What is that, and can you come up with a plan to AEO my content here?”

AEO is the same idea as SEO: structure your content so a machine can extract and cite it, except the machine is Google’s AI Overviews, ChatGPT, or Perplexity instead of a classic search crawler.

Fixing Broken JSON-LD

First we found that Hugo’s built-in schema template was emitting broken structured data, itemprop attributes scattered around with no itemscope or itemtype to tie them together. Not valid, not useful. The Person JSON-LD I’d added to /about/ back in round two was fine, but everything else was broken.

The fix was real BlogPosting and WebSite JSON-LD, built with Hugo’s dict and jsonify functions. Here’s the gotcha that bit us on the first attempt:

{{- $ld := dict
 "@context" "https://schema.org"
 "@type" "BlogPosting"
 "headline" .Title
 "datePublished" (.Date.Format "2006-01-02T15:04:05Z07:00")
}}
<script type="application/ld+json">{{ $ld | jsonify | safeJS }}</script>

Yes, a dict and one jsonify call, but here’s the thing: our first version mixed literal JSON braces with per-field jsonify calls instead of building the whole object at once. Go’s HTML template engine treats <script> blocks as JavaScript context, and it double-encoded every string; "headline" came out as "\"My Title\"" with escaped quotes baked into the value. Building the entire object as one dict and calling jsonify | safeJS exactly once fixed it. We verified by round-tripping the output through Python’s json.loads on the built HTML before shipping.

That brought the site up to five valid schema types: WebSite, BlogPosting, Person, and the two new opt-in ones below.

HowTo and FAQ Schema, Opt-In Per Post

For posts that walk through a procedure, HowTo and FAQPage schema let an answer engine extract the steps or Q&A pairs directly. I didn’t want this forced onto every post, so it’s opt-in through frontmatter:

howto_steps:
 - name: "Connect to Both Arrays"
    text: "Connect to one ActiveCluster member array and to the third array using Connect-Pfa2Array."
faq:
 - question: "Can an ActiveCluster volume be an async replication target?"
    answer: "No. An ActiveCluster volume can be an async replication source, but not a target."

We started with one pilot post, my ActiveCluster async replication walkthrough, to prove it worked with zero visible rendering change, then scaled it out to 26 posts across my T-SQL Snapshot Backup series, the Pure Storage PowerShellSDK2 series, and the Kubernetes failover walkthroughs. Each one was validated as real, parseable JSON before it shipped.

llms.txt

There’s an emerging convention, an llms.txt file at the site root that gives AI crawlers a curated map of your best content instead of making them guess. I added one pointing at my post archive, categories, and a few posts I consider the strongest examples of what this blog is about.

Pruning the Thin Content

Twelve years of blogging means a lot of “Speaking at SQLSaturday X” and “New Pluralsight Course” announcement posts, most under 300 words, most with zero lasting reference value. Google Search Console was quietly flagging a big chunk of my archive as crawled but not indexed, and this thin content is almost certainly why.

Rather than delete a decade of history, we added a noindex flag that a post can opt into, the same pattern from round two’s tag taxonomy work, just extended to individual posts:

noindex: true

Cleaned up 38 posts: event announcements, course launches, award renewals. We also excluded every noindex post from sitemap.xml, since there’s no point telling Google to crawl a page you just told it not to index. Everything stays on the site, still reachable, just not competing for search ranking against posts that actually answer a question.

Here’s a Summary of the Agent’s Work

Here’s the before and after

Category Before After
Broken internal links 14 0
Posts missing meta descriptions 139 0
Images with missing/generic alt text 50 0
Inconsistent category names 152 posts 0
Valid schema types site-wide 0 (broken microdata) 5 (WebSite, BlogPosting, Person, HowTo, FAQPage)
Posts with HowTo/FAQPage schema 0 26
Thin posts diluting search index 38 0 (noindexed, not deleted)

Why This Matters

Here’s what changed:

  1. Zero broken internal links — every redirect target verified to resolve to real content, not just redirect-and-hope
  2. 139 posts with real, specific meta descriptions instead of falling back to a generic site-wide summary
  3. Zero images with missing or generic alt text across the entire archive, up from 50 broken ones
  4. Five valid, real structured data types site-wide (WebSite, BlogPosting, Person, HowTo, FAQPage), replacing structured data that was silently broken
  5. 26 posts with HowTo/FAQPage schema, giving AI answer engines something concrete to extract and cite
  6. 38 thin posts noindexed, without deleting a decade of history, on top of tag taxonomy pages already noindexed and excluded from a custom sitemap.xml
  7. A working llms.txt pointing crawlers at my best content
  8. 152 posts recategorized into consistent, correctly-cased categories, backed by 10 new taxonomy landing pages instead of Hugo’s bare auto-generated lists
  9. A real 404 page with recent posts and category navigation instead of a dead end
  10. A faster, more consistent site, from lazy-loaded images and async fonts to header pages that no longer link to dead addresses or broken URLs

Wrapping Up

The technical SEO work was mechanical once we had the list: broken links, missing descriptions, bad alt text, inconsistent categories. The AEO side is newer territory for me, and it’s clear this is where the next few years of “how do people find my content” are heading. Structured data that a language model can actually parse and cite matters just as much as a page that ranks on page one.

If you’re running a blog with any real history behind it, I’d bet you have some version of this same debt sitting in there. Get an honest audit first, then fix it one category at a time. And check your CI logs after any config change that touches how the site builds, not just your local machine.

Let me know how it works in your environment.