We shipped a coordinated AI-EO + technical SEO pass on sixteenmilevet.com: allow citation-pathway AI bots and block training-only ones, expose
/llms.txtand/pricing.mdat the root, harden authorship and reviewer JSON-LD, replace the hand-rolled sitemap with one derived from the router, and lift Core Web Vitals on the homepage. The work shipped today; we will update this post with measured results in a few weeks.
Sixteen Mile Veterinary Clinic is a single-location practice in Oakville. The site runs on Astro 5 (server output), React 19, and Tailwind 4, deployed to Vercel. The brief was simple: make the site eligible for citation by AI answer engines, tighten the technical-SEO surface, and lift Core Web Vitals on the homepage.
Most of the value was in the order. We started with the crawler policy because a site no engine can reach is invisible no matter how clean its schema is. Authorship came second, because Google’s vet-content guidance is strict and the answer engines are converging on the same signals; vague attribution costs rich-result eligibility no matter how fast the page loads. With those two layers right, schema and sitemap hygiene fall into place, because every other signal compounds once the entity model is consistent. Performance came last. It is the most familiar lever, and the smallest one when the layer underneath it is broken.
Crawler policy: opt in to citation, opt out of training
We rewrote robots.txt around a single question: does this bot send users back to the source, or does it just train a model?
- Allowed: GPTBot, ChatGPT-User, OAI-SearchBot, ClaudeBot, Claude-Web, anthropic-ai, PerplexityBot, Perplexity-User, Google-Extended, Applebot, Applebot-Extended, Bytespider, Meta-ExternalAgent, Amazonbot.
- Blocked: CCBot. It feeds Common Crawl, a training corpus with no citation pathway, so allowing it costs bandwidth without earning visibility.
- Crawl-delay: SemrushBot, AhrefsBot, DotBot. Useful tools, kept off the critical path.
This is the cheapest change in the pass. Most clinic sites land at one of two extremes: every bot blocked, or every bot allowed.
/llms.txt and /pricing.md
Two flat files now sit at the root of the site, both linked from the sitemap.
/llms.txt follows the proposed standard for giving language models a clean, low-noise summary of the site. Ours covers location, hours, team, plan structure, key URLs, and the disclaimer. A model grounding against the file reads that framing instead of whatever it would otherwise piece together from the marketing pages.
/pricing.md is the wellness-plan pricing in machine-readable Markdown: SMVC Club at $40/month, the P.A.L. Plan, exam fee, plan rules. Pricing is the single most-asked question across vet AI queries, and serving it as plain text lets answer engines quote it accurately rather than reconstruct it from scraps.
Authorship and E-E-A-T
Google’s vet-content guidance is strict, and the answer engines are converging on the same signals.
We built a per-post author registry. Posts written by credentialed staff (Dr. ... DVM) emit Person JSON-LD with the clinic as affiliation; non-credentialed authors fall back to Organization.
Many of the 92 educational posts were drafted by the editorial team and reviewed by a clinician. The schema layer now enforces that every non-draft post carries either an author or a reviews array. Reviewer-only posts render “Reviewed by Dr. …” in the byline and emit reviewedBy Person entries alongside the clinic as author.
A /disclaimer page plus a per-post editorial disclaimer aside, linked from the footer, tells humans and crawlers that the articles are educational and not a substitute for an exam.
Structured-data hygiene
The schema layer needed several small fixes, each of which carries an outsized effect on how Google and the answer engines reconcile entities across the site.
We canonicalised every JSON-LD URL to https://www.sixteenmilevet.com so BlogPosting, the sitemap, and <link rel="canonical"> agree. Mismatched hosts (apex vs. www) silently demote rich-result eligibility.
We removed a stale specialOpeningHoursSpecification for Canada Day 2025 that was still being emitted in 2026. A schema that contradicts the visible page is worse than no schema, because the engine has no way to know which to trust.
We added optional faq frontmatter on blog posts that emits FAQPage JSON-LD. The hardcoded “Sarah Bishop welcome” FAQ block was the first consumer.
We replaced ad-hoc breadcrumb components with BreadcrumbList JSON-LD on detail pages, plus a real <nav aria-label="Breadcrumb"> in the markup. A dead BreadcrumbsContentPages component that was imported but never rendered got deleted on the way through.
One sitemap, derived from the router
The previous setup had sitemap-index.xml.ts + sitemap-0.xml.ts plus a hardcoded list of blog slugs. We replaced both with a single sitemap.xml.ts driven by the router:
const decisions: Record<keyof typeof routes, SitemapDecision> = { ... }The change has two consequences worth flagging. Adding a route without a sitemap decision is now a TypeScript error, which catches the most common cause of stale sitemaps. And because blog posts come from the content collection, new articles appear automatically, including the new /blog/<n> and /blog/topic/<slug>[/<n>] paths.
Astro footnote: pagination pages originally used
getStaticPaths, which never runs underoutput: "server". We swapped to request-timeAstro.params.pageparsing with a redirect-to-page-1 fallback for out-of-range values.
Topic-based blog archive
The blog grew to 92 posts across ticks, heartworm, fleas, dog allergies, safe foods, and clinic updates. We replaced the flat archive with:
/blog, paginated 12 per page, with a topic-chip filter row./blog/topic/<slug>archives for each topic, also paginated.- A single
TOPICSregistry (src/lib/blog/topics.ts) feeding archive pages, post breadcrumbs, the sitemap, and the footer column. One file to update; everything else stays in sync. - Featured-post hero on
/blog; a “Continue Reading” block on each post withsessionStorage-backed visited-state highlighting.
The topic archives matter for AI-EO specifically: they give answer engines clean topical hubs to cite when a user asks a category-shaped question like “tick prevention in Ontario” rather than a long-tail one.
Dynamic Open Graph images
Every page now has its own 1200×630 OG card, generated on the fly via @vercel/og:
- The
/og/<path>.pngendpoint resolves the title and category label from a server-side registry keyed by path. Nothing about the rendered card comes from the query string. The endpoint cannot be coerced into rendering attacker-controlled text on asixteenmilevet.comURL. SEOHeadfalls through: explicitimageprop, then registered dynamic OG, then static/ogimage.png.- Vercel
includeFilesbundles the brand fonts and white-logo SVG into the serverless function so the renderer is self-contained.
Every share, link preview, and og:image lookup ends up with a branded card without anyone hand-designing one per page.
Core Web Vitals on the homepage
LCP and CLS feed into both classical SEO and the freshness of any AI summary that re-fetches the page.
- We preloaded Lexend Deca and Open Sans Latin
woff2subsets in the baseLayoutso above-the-fold text never blocks on font fetch. - We preloaded the homepage hero image with
fetchpriority="high". Below-the-fold<img>tags getloading="lazy"anddecoding="async". - We converted the Get-to-Know-Us carousel’s first slide from a CSS
background-imageto a real<img>so it can be marked high-priority and sync-decoded. - We replaced the hero’s
.jpgwith a hand-tuned.webp(101 KB) for an immediate byte-size win.
What we are tracking
The work shipped today (2026-05-09). Results take weeks, so we will come back and replace this section with a measured update. The watchlist:
- Citation rate in AI answer engines, tracked through ChatGPT search, Perplexity, and Claude search for vet-pricing and topic-shaped queries. We expect
/pricing.mdand the topic archives to surface first. - Rich-result eligibility in Google Search Console for
BlogPosting,FAQPage,BreadcrumbList, andLocalBusiness. The Canada-Day-2025 stale-hours fix should clear an existing warning. - Indexed-page count. With the router-driven sitemap, the new topic archives and pagination pages should appear in coverage reports within the first crawl cycle.
- Core Web Vitals on the homepage. LCP is the one to watch; the hero
.webpand font preload should move it.
Sources
- Sixteen Mile Veterinary Clinic — sixteenmilevet.com
- llms.txt — proposed standard for site summaries written for language models
@vercel/og— Open Graph image generation on Vercel- Astro —
getStaticPathsreference