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.
| File | Status |
|---|---|
/sitemap.xml | 404 |
/robots.txt | 404 |
/rss.xml | 404 |
Structured data on / | none |
| Structured data on blog posts | none |
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/sitemapintegration. One line inastro.config.mjs. The build now emits/sitemap-index.xmland/sitemap-0.xmlwith 21 URLs.public/robots.txtwith aSitemap:line pointing at the sitemap index.@astrojs/rssand asrc/pages/rss.xml.tsthat iterates the blog collection and emits a feed.<link rel="alternate" type="application/rss+xml">lands inSiteLayout’s head./workbecomes a real 301 viapublic/_redirects. The Astro page is deleted.llms.txtno longer advertises it.- A
BreadcrumbListJSON-LD block on every nested page. Driven by an optionalbreadcrumbsprop onSiteLayoutso each page passes its own trail.
The on-page batch:
PersonJSON-LD on the homepage withsameAsfor GitHub, LinkedIn, X, and Instagram. This is what Google reads when it decides whether to build a Knowledge Panel.WebSiteJSON-LD withSearchAction. This is the sitelinks search box.BlogPostingJSON-LD on every post withheadline,datePublished,dateModified,author,image, andkeywords.- 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 nowBrowser-only Todo with Priority Tiers · Abhiigattywith 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/todonow points at/blog/a-todo-app-that-lives-in-your-browserand the other way round. Same for OTP. twitter:siteandtwitter:creatorfixed to@abhiigatty_.rel="me"added to the outbound social profile anchors. This is IndieWeb identity verification and reinforces thesameAsgraph 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 wanted | How I will measure it | Where to look |
|---|---|---|
| New posts indexed faster | Average time between git push and “Last crawled” in URL Inspection | Google Search Console → URL Inspection |
| All pages discoverable | Indexed count after sitemap submission, vs. 21 URLs in the sitemap | Search Console → Pages |
| Article rich results eligible | Each BlogPosting JSON-LD passes the Rich Results Test, valid count rises in GSC | search.google.com/test/rich-results and Search Console → Enhancements |
| Knowledge Panel for “Abhiigatty” | Panel appears on the SERP, sources my sameAs profiles | Manual SERP check, 4–12 weeks out |
| Sitelinks search box | A search box renders below the homepage SERP result | Manual SERP check, 8–16 weeks out |
| Correct social attribution | Tweets / posts linking to my pages show @abhiigatty_ instead of nothing | X cards preview tool, and any embeds in the wild |
| Internal linking lifts time on site | Average pageviews per session ticks up | Cloudflare Web Analytics |
| RSS reaches readers | Subscriber count appears in Feedly or wherever readers actually live | Hard 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.