Skip to main content

๐Ÿ”Ž 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-page Article/TechArticle/BlogPosting/FAQPage + BreadcrumbList
  • Bot policy: All bots allowed (search + AI search + training) in robots.txt
  • AI agent map: llms.txt lists 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 typeSchemas
/ (homepage)Organization + WebSite + SoftwareApplication + offer at โ‚ฌ0
/blog/:slugBlogPosting (or Article) + BreadcrumbList
/compare/:slug, /alternatives/:slugArticle + BreadcrumbList (+ FAQPage if visible FAQ)
/use-cases/:slugArticle + BreadcrumbList
/features/:slug, /how-to/:slug, /how-it-works, /accuracyTechArticle + BreadcrumbList
/faqFAQPage (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:

  1. Drop a markdown file under src/content/blog/<slug>.md with 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 case
    schema: "BlogPosting"
    faq:
    - q: "..."
    a: "..."
    ---
  2. Body must NOT start with # Title โ€” the page template renders the H1 already, and MarkdownBody strips a leading H1 to prevent duplication.

  3. Internal links use absolute paths starting with / (e.g. /pricing, /features/shot-detection). External links use full https URLs.

  4. Run pnpm build โ€” Vite picks up the new file via import.meta.glob, regenerates the sitemap, and prerenders the new route.

  5. 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