๐ SEO + AI Discovery Surface
What's wired so AceSense shows up in Google, Perplexity, ChatGPT, and Claude search โ and how to keep it that way.
:::tip TL;DR
- Content:
acesense-landing/src/content/โ 49 blog posts + 37 marketing pages - Pre-render: Build-time Puppeteer step, 92 routes get static HTML
- Schema:
Organization+WebSite+SoftwareApplication+ per-pageArticle/TechArticle/BlogPosting/FAQPage+BreadcrumbList - Bot policy: All bots allowed (search + AI search + training) in
robots.txt - AI agent map:
llms.txtlists all major content surfaces :::
๐ฏ Why this existsโ
The AceSense category โ AI tennis video analysis โ is contested by SwingVision (iOS-only), PlaySight (facility hardware), Baseline Vision (โฌ1,800 hardware), and a handful of smaller apps. The strategic gap is "SwingVision alternative for Android," served by the landing content tree.
For amateur tennis players asking AI search engines "what is similar to SwingVision for Android?", the answer needs to be AceSense, with content the AI engine can ground its answer in.
๐ ๏ธ The piecesโ
1. Pre-rendered HTML (SSG)โ
acesense.io is a Vite SPA. Without pre-rendering, AI bots and social scrapers fetch the raw HTML, which is just index.html shell โ no content, no per-page metadata. Every blog post would look identical to a scraper.
The build pipeline now includes scripts/prerender.mjs:
Why frontmatter overrides: react-helmet-async's document.title updates don't reliably propagate to the DOM <title> element inside Puppeteer + Vite preview. So we drive the rewrite from the markdown frontmatter directly โ same source of truth the runtime React client uses.
2. Sitemapโ
scripts/prerender.mjs reads the content routes at build time and emits dist/sitemap.xml.
3. robots.txtโ
public/robots.txt is all-permissive โ every bot category (search, AI search, training, agents) gets Allow: /. Reasoning lives in ADR-0006.
:::warning Cloudflare override
acesense.io is fronted by Cloudflare. Cloudflare's "Manage AI Bots / Content Signals" feature can override the origin's robots.txt with its own template. The dashboard is the source of truth for the served policy โ toggle every bot to Allowed there. The repo's robots.txt is the fallback for direct origin URLs (acesense-prod.web.app).
:::
4. llms.txtโ
public/llms.txt is the AI-agent content map: an H1 product summary, a blockquote pitch, then linked sections (Product, Use cases, Comparisons, Alternatives, Tutorials, Blog, Optional, Excluded). Modeled on the llmstxt.org proposal.
5. JSON-LD structured dataโ
Per-page schema is injected via Helmet (client-side) and confirmed in pre-rendered HTML:
| Page type | Schemas |
|---|---|
/ (homepage) | Organization + WebSite + SoftwareApplication + offer at โฌ0 |
/blog/:slug | BlogPosting (or Article) + BreadcrumbList |
/compare/:slug, /alternatives/:slug | Article + BreadcrumbList (+ FAQPage if visible FAQ) |
/use-cases/:slug | Article + BreadcrumbList |
/features/:slug, /how-to/:slug, /how-it-works, /accuracy | TechArticle + BreadcrumbList |
/faq | FAQPage (Q&A mirrored from frontmatter) |
Hard rule: no fake reviews, ratings, or aggregate scores. We only emit schema that maps to visible page content.
6. Per-page metadataโ
Every prerendered page ships exactly one <title>, one <meta name="description">, one og:title, one og:description, one og:url, one og:image, one <link rel="canonical">, one twitter:title, one twitter:description, and one <meta name="robots">. The prerender script's dedupeLast pass enforces this โ Helmet sometimes leaves stale entries during the SSG snapshot.
๐ฆ Content treeโ
src/content/
โโโ blog/ โ 49 posts, dated 2025-03 โ 2026-04, by Akshay Sarode
โโโ compare/ โ 7 (vs SwingVision, PB Vision, Baseline Vision, PlaySight, OnForm, TopCourt, pricing-grid)
โโโ alternatives/ โ 3 (SwingVision-Android-led, OnForm, TopCourt)
โโโ use-cases/ โ 5 personas (club players, junior coaches, parents, adult returners, async coaches)
โโโ features/ โ 5 (shot detection, ball tracking, court heatmap, stroke quality, coaching report)
โโโ how-to/ โ 3 (filming, recording serve, sharing report)
โโโ static/ โ 11 pillars (how-it-works, accuracy, pricing, faq, changelog,
android, ios, examples, about, biomechanics, sweden)
Each markdown file has frontmatter (title, description, slug, date, author, category, schema, faq, cta, ogImage, โฆ) consumed by src/lib/content.ts at build time and by scripts/prerender.mjs for the metadata override pass.
๐ Authoring flowโ
To add a new blog post:
-
Drop a markdown file under
src/content/blog/<slug>.mdwith frontmatter:---title: "Your title (โค60 chars for the SERP)"description: "Your meta description, 140โ155 chars, with the target query embedded"slug: "your-slug"date: "2026-04-26"author: "Akshay Sarode"authorBio: "Founder, AceSense. Building AI tennis tools in Europe."category: "How-to" # or Comparison / Methodology / Industry / Use caseschema: "BlogPosting"faq:- q: "..."a: "..."--- -
Body must NOT start with
# Titleโ the page template renders the H1 already, andMarkdownBodystrips a leading H1 to prevent duplication. -
Internal links use absolute paths starting with
/(e.g./pricing,/features/shot-detection). External links use full https URLs. -
Run
pnpm buildโ Vite picks up the new file viaimport.meta.glob, regenerates the sitemap, and prerenders the new route. -
Deploy with
firebase deploy --only hosting:landing.
โ Per-deploy verificationโ
# Sitemap correctly lists every content URL?
curl -s https://acesense.io/sitemap.xml | grep -oE '<loc>[^<]+' | wc -l
# robots.txt + llms.txt accessible?
curl -sI https://acesense.io/robots.txt | head -1
curl -sI https://acesense.io/llms.txt | head -1
# Per-page metadata is page-specific (not the homepage default)?
for p in /blog/swingvision-android-alternative/ /compare/swingvision/ /biomechanics/ /sweden/; do
curl -sL "https://acesense.io$p" | grep -oE '<title>[^<]+</title>' | head -1
done
๐ Relatedโ
- Landing site overview โ what the site contains
- Cross-surface analytics โ PostHog + Firebase setup
- Malicious-upload threat model โ adjacent compliance work