Implementation guide
Carepatron AI search and agent UX audit
What to ship, and in what order, to get Carepatron well-represented in AI Overviews, ChatGPT Search, and Perplexity, and to be readable by the new generation of browsing agents hitting our marketing site. Audited against Google's AI optimization guide and web.dev's agent-friendly sites guide. May 2026.
The scorecard
Already strong
- Crawlable and indexable, open robots, valid sitemap
- Server-rendered HTML, agents can read the body without running JS
- hreflang across 24 locales with x-default
- Native
<button>and<a>on most CTAs - Unique title and meta description on every sampled page
- One H1 per page, sensible H2 and H3 hierarchy
Gaps to close
- Zero JSON-LD structured data on any page
- 13 of 15 home images have empty alt text
- /help-center/ and /signup/ return 404
- /templates/ ships 51 MB of HTML
- Long-tail titles like "ICD-10-CM Codes | 2023" look stale
- Repeated CTA text with no aria-label disambiguation
P0Add JSON-LD to every template
The single biggest unlock. Google's guide says structured data is not required for AI Overviews, but it is part of the overall SEO strategy and the rich result eligibility list. For Carepatron's content mix of clinical reference pages, procedure codes, guides, and blogs, this is exactly the kind of content AI Overviews surfaces, and right now we hand the parser nothing.
Where it goes: one JSON-LD block per Astro layout under src/layouts/, placed in <head>. Inject dynamic fields from each Strapi entry.
Home page: Organization, WebSite, SoftwareApplication
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://www.carepatron.com/#organization",
"name": "Carepatron",
"url": "https://www.carepatron.com/",
"logo": "https://www.carepatron.com/images/carepatron-logo.svg",
"sameAs": [
"https://twitter.com/CarepatronHQ",
"https://www.linkedin.com/company/carepatron",
"https://www.facebook.com/CarepatronHQ"
]
},
{
"@type": "WebSite",
"@id": "https://www.carepatron.com/#website",
"url": "https://www.carepatron.com/",
"name": "Carepatron",
"publisher": { "@id": "https://www.carepatron.com/#organization" }
},
{
"@type": "SoftwareApplication",
"name": "Carepatron",
"applicationCategory": "HealthApplication",
"operatingSystem": "Web, iOS, Android",
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" }
}
]
}
</script>
/pricing/: Product with three Offers, plus FAQPage
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Carepatron",
"description": "Healthcare practice management software for scheduling, documentation, billing, and telehealth.",
"brand": { "@type": "Brand", "name": "Carepatron" },
"offers": [
{ "@type": "Offer", "name": "Free", "price": "0", "priceCurrency": "USD" },
{
"@type": "Offer",
"name": "Plus",
"price": "39",
"priceCurrency": "USD",
"priceSpecification": {
"@type": "UnitPriceSpecification",
"price": "39", "priceCurrency": "USD", "unitText": "MONTH"
}
},
{
"@type": "Offer",
"name": "Advanced",
"price": "49",
"priceCurrency": "USD",
"priceSpecification": {
"@type": "UnitPriceSpecification",
"price": "49", "priceCurrency": "USD", "unitText": "MONTH"
}
}
]
}
</script>
Pair the Product block with a FAQPage block built from the existing "Frequently asked questions" section. The template is the same as the FAQPage snippet below.
/icd/* pages: MedicalCondition with MedicalCode
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "MedicalCondition",
"name": "{{ strapi.condition.name }}",
"alternateName": {{ strapi.condition.synonyms | json }},
"code": {
"@type": "MedicalCode",
"codeValue": "{{ strapi.condition.icd10 }}",
"codingSystem": "ICD-10-CM"
},
"associatedAnatomy": "{{ strapi.condition.anatomy }}",
"url": "https://www.carepatron.com/icd/{{ slug }}/"
}
</script>
This is the highest-leverage block in the whole site. ICD lookup queries are exactly the long-tail clinical pattern AI Overviews answers, and right now we have hundreds of /icd/ pages serving zero schema.
/procedure-code/* pages: MedicalProcedure with MedicalCode
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "MedicalProcedure",
"name": "{{ strapi.procedure.name }}",
"code": {
"@type": "MedicalCode",
"codeValue": "{{ strapi.procedure.cpt }}",
"codingSystem": "CPT"
},
"url": "https://www.carepatron.com/procedure-code/cpt-code-{{ strapi.procedure.cpt }}/"
}
</script>
/guides/* and /blog/* pages: Article with author and dates
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "{{ strapi.post.title }}",
"image": ["{{ strapi.post.cover.url }}"],
"author": {
"@type": "Person",
"name": "{{ strapi.post.author.name }}",
"url": "{{ strapi.post.author.profile_url }}"
},
"publisher": {
"@type": "Organization",
"name": "Carepatron",
"logo": {
"@type": "ImageObject",
"url": "https://www.carepatron.com/images/carepatron-logo.svg"
}
},
"datePublished": "{{ strapi.post.publishedAt | date: 'iso' }}",
"dateModified": "{{ strapi.post.updatedAt | date: 'iso' }}"
}
</script>
Author and dateModified are the two fields the Helpful Content system uses to gauge freshness. If a post has no author in Strapi, populate with a Carepatron team byline rather than omitting the field.
FAQPage: reusable block for any page with a Q&A section
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "{{ q.question }}",
"acceptedAnswer": { "@type": "Answer", "text": "{{ q.answer | strip_html }}" }
}
]
}
</script>
At least six page templates have a "Frequently asked questions" or "Commonly asked questions" section. Wire this block to those once and it ships everywhere.
BreadcrumbList: for /icd/, /procedure-code/, /guides/, /blog/, /features/
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "https://www.carepatron.com/" },
{ "@type": "ListItem", "position": 2, "name": "ICD codes", "item": "https://www.carepatron.com/icd/" },
{ "@type": "ListItem", "position": 3, "name": "{{ strapi.condition.name }}" }
]
}
</script>
P0Fix the empty image alt text
Before
<img src="/images/calendar-screenshot.webp" alt="">
After
<img src="/images/calendar-screenshot.webp"
alt="Carepatron weekly calendar view with an appointment card and side panel for new bookings">
Where the worst gaps are:
- Home: 13 of 15 images have empty alt
- /professionals: 6 of 12 empty
- /features/schedule: 2 of 12 empty (better, but the screenshots still need real descriptions)
- /pricing/: 2 of 10 empty
Decorative chrome such as background patterns or dividers should keep alt="". Product screenshots and feature illustrations need a one-line description of what is shown.
[alt] attributes" audit should pass and accessibility score should sit above 95.
P0Fix the 404 paths
/help-center/returns 404. Either build the route, or 301 to the Intercom help destination./signup/returns 404. 301 tohttps://app.carepatron.com/Register.
curl -sI https://www.carepatron.com/help-center/ | head -3
curl -sI https://www.carepatron.com/signup/ | head -3
Both should return 200 or 301 with a Location header.
P1Shrink the 51 MB on /templates/
Root cause, confirmed. 49 MB of the page is one HTML attribute: the props on the <astro-island> for TemplateIsland.D_7l0qZ4.js. Astro serialises the entire Strapi response for every template into the DOM so the React component can hydrate without a fetch. The intent is fine. The size is wrong.
The numbers:
- Total page weight: 49.2 MB (377 lines, but one line is 49.2 MB)
- One
<astro-island>opening tag: 48.8 MB - Its
propsattribute: 37.3 MB JSON, 49 MB once HTML-encoded - Template entries serialised: 4,598 distinct templates
- Roughly 20+ fields per entry, including a
parentPageOldfield that looks deprecated and anisICD,isApp,isComparisonflag set repeated on every row
Why this is happening: the Astro layout passes initialTemplates as a prop to the client island. Astro serialises every prop into the DOM as the hydration payload. The Strapi populate query is returning the full entity tree, not just card-essential fields, so the hydration payload is 4,598 × ~8 KB per entry.
Pick one of these fixes, in order of preference:
Option A — narrow the Strapi populate (best). Change the populate query to return only fields the index card needs: name, slug, thumbnailImage.url, category.name. Drop introduction, parentPageOld, the six boolean type flags, documentId, locale, createdAt, publishedAt. Expected payload: under 1 MB.
// src/pages/templates/index.astro (or wherever Strapi is queried)
const templates = await strapi.find('templates', {
fields: ['name', 'slug'],
populate: {
thumbnailImage: { fields: ['url', 'alternativeText'] },
category: { fields: ['name', 'slug'] }
},
pagination: { pageSize: 4600 }, // current full set
locale
});
Option B — paginate hydration. Send the first 60 entries as initialTemplates, hydrate, then fetch the rest from a Strapi API route on scroll or filter. Expected payload: ~50 KB.
Option C — server-render the grid, hydrate only interactive bits. Astro static-renders all 4,598 cards as HTML (good for SEO and AI Overviews), and only the filter and search controls hydrate. Expected payload: closer to 5 MB of HTML (cards as DOM, not JSON), but with much better Core Web Vitals because the markup is parseable in stream.
Bonus cleanup: the parentPageOld field on the Strapi schema is almost certainly a leftover from a migration. Confirm it's unused everywhere, then drop the field from Strapi entirely. Saves a column on every entity returned by every populate query, not just on /templates/.
curl -sI https://www.carepatron.com/templates/ | grep -i content-length
# inspect the prop size on the deployed page
curl -sL https://www.carepatron.com/templates/ | \
grep -oE 'props="[^"]{100,}"' | head -1 | wc -c
Target: content-length under 1 MB. Lighthouse Performance score above 70 on mobile. The prop blob should be under 100 KB.
P1Audit the chat widget overlay
web.dev's guide warns that fixed-position overlays can hide CTAs from agents using the vision modality. Intercom, Calendly, and Growsumo all load on every page.
How to check:
- Load the home page in Chrome
- Open DevTools, Accessibility tab, inspect the "Get started for free" button
- At viewport widths 375px, 768px, and 1280px, confirm the Intercom launcher does not visually overlap or capture pointer events on the primary CTA
If they overlap, drop the Intercom z-index below the CTA region, or hide the launcher on hero sections.
P1Disambiguate the repeated CTA links
The home has five copies of the "Get started for free" anchor, all pointing to the same Register URL with the same visible text. Humans skim past it. Agents parsing the link graph see five identical entries and have to deduplicate by context.
Fix: add a unique aria-label to each.
<a href="https://app.carepatron.com/Register"
aria-label="Sign up from the homepage header">
Get started for free
</a>
<a href="https://app.carepatron.com/Register"
aria-label="Sign up from the homepage hero">
Get started for free
</a>
<a href="https://app.carepatron.com/Register"
aria-label="Sign up from the mid-page feature row">
Get started for free
</a>
P1Convert the remaining div-as-button
Every sampled page contains one <div role="button">. It is almost certainly in a shared header or footer component. web.dev's guide is explicit: prefer <button> and <a> over modified <div> or <span>.
// before
<div role="button" tabindex="0" class="cta-button">Book a demo</div>
// after
<button type="button" class="cta-button">Book a demo</button>
P2Refresh the stale year stamps
The /icd/ page for anal fissure is titled Anal Fissure ICD-10-CM Codes | 2023. In May 2026 that looks three years out of date even if the underlying codes are current. Google's freshness signals and AI Overviews both penalize this.
Where: Strapi, ICD collection, title field. Filter for entries containing | 2023 or | 2024 and batch-update to | 2026, or drop the year stamp entirely on evergreen codes. Same pattern likely exists on /procedure-code/ and older blog posts.
P2Add a Carepatron-unique block to long-tail content
Google's AI optimization guide is explicit about "commodity content" and "scaled content abuse". A page that only restates the ICD-10 entry for anal fissure is commodity content. Each long-tail page should carry at least one section that only Carepatron could write.
Template to add to every /icd/, /procedure-code/, and reference page:
- "How clinicians use this in Carepatron" with a link to the matching feature or template
- An embedded preview of a Carepatron template that handles the condition or procedure
- One usage stat or quote from real practitioners on the platform
This converts each commodity page into a non-commodity page in the eyes of Google's helpful-content systems, without spinning up a new content team.
P3Optional: ship /llms.txt
Google's guide says explicitly to skip llms.txt for Google Search. But Anthropic, Perplexity, and a handful of other AI products do read it. About 30 minutes of work to ship a minimal version listing canonical surfaces.
# https://www.carepatron.com/llms.txt
# Carepatron canonical content surfaces
/ Home
/features/ Feature index
/pricing/ Pricing and plans
/professionals/ Profession landing pages
/icd/ ICD-10 code reference
/procedure-code/ CPT code reference
/guides/ Clinical guides
/blog/ Blog
/templates/ Free clinical templates
Order of attack
- Ship JSON-LD templates for all page types. One PR, touches a handful of Astro layouts. Biggest single ranking unlock.
- Image alt-text sweep on home, pricing, features index, professionals.
- Redirect or build
/help-center/and/signup/. - Diagnose
/templates/payload. One change. - Audit chat widget z-index against CTAs at three viewport widths.
- Add aria-label to every duplicate CTA on the home.
- Convert the remaining
<div role="button">in shared layout. - Strapi sweep on stale year stamps in /icd/ and /procedure-code/ titles.
- Content team adds Carepatron-unique blocks to long-tail pages.
- Optional: ship
/llms.txt.
Verification checklist
- Rich Results Test shows detected schema on home, pricing, an /icd/ page, a /procedure-code/ page, a /guides/ page, a /blog/ page
- Lighthouse accessibility score above 95 on home and pricing
/help-center/and/signup/return 200 or 301/templates/content-length under 1 MB- Intercom launcher does not overlap the hero CTA at 375px, 768px, or 1280px
- Every "Get started for free" anchor on the home has a unique
aria-label - No
<div role="button">elements remain in shared layout - No live /icd/ or /procedure-code/ title contains "| 2023" or "| 2024"
- At least one /icd/ page renders a "How clinicians use this in Carepatron" block
- Optional:
/llms.txtreturns 200