I built this site to be fast. I forgot that fast does not mean discoverable.

Last week I ran a perf and security pass on the site. The next obvious thing was SEO, so I pointed a specialist agent at it and asked for a ranked list of findings.

It returned 18.

What I wanted

Plain things, mostly:

  • A Knowledge Panel for the personal brand when someone searches “Abhiigatty” or “Abhishek P”.
  • Article rich results on blog posts.
  • An RSS feed for the people who still read this way (I am one of them).
  • Faster indexing of new posts than “wait for Google to crawl you”.

What I got back was a list of the things that block all of that, ranked by severity and effort.

The bones were missing

The first surprise was how little discovery infrastructure the site shipped. Not “needs improvement”. Missing.

FileStatus
/sitemap.xml404
/robots.txt404
/rss.xml404
Structured data on /none
Structured data on blog postsnone

The second surprise was the H1.

The audit flagged that five landing pages had no <h1> at all. I went to look. The contact dialog in SiteHeader.astro renders an <h2>Let's work together</h2> early in the DOM. Because the dialog ships on every page and shows up in DOM order before <main>, the first heading a crawler sees on /, /blog, /projects, /tools, and a couple of tool sub-pages is the dialog title, not the page subject.

That is not the kind of thing you notice by reading the page. You only see it when something walks the DOM.

The third surprise was the blog. Every post linked back to /blog. None linked to each other. No “related posts” block. No newer/older nav. If you landed on one post via search, the only path forward was back to the index.

For PageRank purposes that is a flat structure with one hub and 14 leaves. Search engines do not like that and neither does anyone reading.

There was also a /work page serving a 2-second <meta http-equiv="refresh"> plus a noindex. The page existed only to redirect to /projects. That is the worst-ranked redirect form per Google’s own guidance. It also still appeared in llms.txt as a real page.

And the twitter:site meta tag shipped @abhiigatty. The actual handle (in the footer and llms.txt) is @abhiigatty_ with a trailing underscore. Two years of social-card attribution were going to the wrong account.

What I shipped

I split the work in half and dispatched two implementation agents in parallel, each in its own git worktree so they would not step on each other. The infra agent took the discovery-infrastructure findings. The on-page agent took everything that touches the rendered HTML.

The infra batch:

  • @astrojs/sitemap integration. One line in astro.config.mjs. The build now emits /sitemap-index.xml and /sitemap-0.xml with 21 URLs.
  • public/robots.txt with a Sitemap: line pointing at the sitemap index.
  • @astrojs/rss and a src/pages/rss.xml.ts that iterates the blog collection and emits a feed. <link rel="alternate" type="application/rss+xml"> lands in SiteLayout’s head.
  • /work becomes a real 301 via public/_redirects. The Astro page is deleted. llms.txt no longer advertises it.
  • A BreadcrumbList JSON-LD block on every nested page. Driven by an optional breadcrumbs prop on SiteLayout so each page passes its own trail.

The on-page batch:

  • Person JSON-LD on the homepage with sameAs for GitHub, LinkedIn, X, and Instagram. This is what Google reads when it decides whether to build a Knowledge Panel.
  • WebSite JSON-LD with SearchAction. This is the sitelinks search box.
  • BlogPosting JSON-LD on every post with headline, datePublished, dateModified, author, image, and keywords.
  • Real <h1> on /, /blog, /projects, /tools, and /tools/matrix. The fix that did most of the work was downgrading the shared contact-dialog <h2> to a styled <p>. One change, six pages lifted.
  • Tool sub-page titles and meta descriptions tuned. <title>Todo</title> is now Browser-only Todo with Priority Tiers · Abhiigatty with a real description. The matrix tool got a description at all (it had none before).
  • Related-posts and newer/older nav on every blog post. Related is tag-overlap, sorted by date, top three.
  • Tools cross-linked to their announcement posts and the posts cross-linked back. /tools/todo now points at /blog/a-todo-app-that-lives-in-your-browser and the other way round. Same for OTP.
  • twitter:site and twitter:creator fixed to @abhiigatty_.
  • rel="me" added to the outbound social profile anchors. This is IndieWeb identity verification and reinforces the sameAs graph above.

That is fourteen of the eighteen findings.

What I deferred and why

Four did not land this round.

Per-post OG images. All 14 posts share the same generic og-image.png. Fixing this means either a build-time generator (Satori-based, about 80 lines of code) or one PNG per post folder. Both are worth doing. Neither is a 30-minute job. Saving it for a separate post.

CSP style-src 'unsafe-inline'. The only weak directive in an otherwise tight policy. Closing it means either hashing every emitted <style> block or telling Astro to externalize all styles. The first is brittle. The second is a config flip but needs careful testing. Half-day with risk, not a batch item.

Three Cloudflare dashboard items. Adding a CAA record. Switching the apex SPF from ~all to -all. Strengthening www → apex from a 200 with canonical to a proper 301. None of these are in the repo. They are clicks I owe my own dashboard.

How I will know if any of this worked

This is the part I almost forgot. SEO has delayed feedback. You ship structured data and an <h1> and a sitemap, and the search engine takes weeks to catch up. If I do not write down what I expect to see, I will not know whether the work mattered.

So here is the dashboard I am building, with the instrument for each thing I claimed I wanted.

What I wantedHow I will measure itWhere to look
New posts indexed fasterAverage time between git push and “Last crawled” in URL InspectionGoogle Search Console → URL Inspection
All pages discoverableIndexed count after sitemap submission, vs. 21 URLs in the sitemapSearch Console → Pages
Article rich results eligibleEach BlogPosting JSON-LD passes the Rich Results Test, valid count rises in GSCsearch.google.com/test/rich-results and Search Console → Enhancements
Knowledge Panel for “Abhiigatty”Panel appears on the SERP, sources my sameAs profilesManual SERP check, 4–12 weeks out
Sitelinks search boxA search box renders below the homepage SERP resultManual SERP check, 8–16 weeks out
Correct social attributionTweets / posts linking to my pages show @abhiigatty_ instead of nothingX cards preview tool, and any embeds in the wild
Internal linking lifts time on siteAverage pageviews per session ticks upCloudflare Web Analytics
RSS reaches readersSubscriber count appears in Feedly or wherever readers actually liveHard to measure cleanly; check Feedly subscriber count for the feed URL

I am also adding a baseline checkpoint today. Before the PRs merge:

  • Indexed pages: whatever Google currently shows
  • Average impressions over last 28 days: pull from GSC after I create the property
  • Top 10 queries: same

Then four weeks later I will check the same numbers. The difference is the story.

There is one negative test I want to run too. If BlogPosting is wrong (missing field, bad JSON), GSC will tell me. If a redirect chain forms because _redirects and Astro disagree, GSC will tell me. The presence of no errors in Enhancements and Pages is itself a signal that the structured data is shaped right.

I will not get all of this right on the first pass. The point of writing it down is that next time I run an audit, I have a “before” to compare against and a list of things to verify.

What surprised me about the process

I used two specialist agents in parallel, each in its own worktree, each scoped to a non-overlapping set of files. Each opened its own PR. Both PRs are stacked on the perf and security branch from last week.

The coordination cost was real but small. The bigger win was that “what changed” stayed legible per concern. The infra PR is one story. The on-page PR is a different one. The reviewer (me) reads each at the right level.

The other thing that helped was the SEO plugin I had just installed. Each agent invoked the relevant skill at the start of its work: seo-schema for the JSON-LD blocks, seo-page for the H1 wording, seo-sitemap for the sitemap config. The skills gave the agents canonical shapes to copy from instead of inventing them. That dropped the rate of subtle wrong-field errors meaningfully.

What I learned

Search infrastructure is invisible until you look. The site felt complete to me because everything on the page rendered the way I expected. Crawlers see a different document. None of the missing pieces showed up in any UI test I would have run. They show up only in an audit.

The other thing: defaults matter. @astrojs/sitemap is one line. Once you add it, the existence of a sitemap is no longer a thing you maintain. The same is true of the schema integration once the slot in SiteLayout exists. Closing this category of gap permanently is one decision.

The next things I want to do are the per-post OG images, the CSP tightening, and finally creating the Google Search Console property so I can watch the indexing numbers move.

Those will be their own posts.