SaaS SEO Keyword Strategy: 12 BOFU Mappings, 9 Drove Demos

9 of 12 BOFU keywords I mapped drove Demo-booked in 30 days. Intent matrix, programmatic SEO route, and 3 misses named. TanStack Ship ships it.

Huifer
Huifer
May 2, 20269 min read
Also available in:Deutsch · 中文

Written by Huifer, solo developer and maintainer of TanStack Ship. Between February and April 2026 I shipped a BOFU keyword-to-page mapping across 5 TanStack Start-built SaaS landing pages and tracked the next 30 days in Google Search Console. Of 12 BOFU keywords I targeted, 9 drove a Demo-booked event, 2 drove signups, and 1 never reached page 1. The pattern reproduces when I follow the same intent-to-page rule, and breaks when I do not. I paid for the Ahrefs subscription myself; no agency reviewed the mapping.

Verified sources: Google Search Console Documentation · Ahrefs Keyword Difficulty · Backlinko On-Page SEO · Moz Search Intent · Search Engine Journal Keyword Mapping · Google Search Central Structured Data · TanStack Start Documentation · TanStack Ship GitHub Organization Last updated: 2026-05-02 · Changelog


TL;DR: I mapped 12 BOFU SaaS keywords to 5 money pages across 5 TanStack Start-built SaaS sites. After 30 days, 9 of 12 keywords reached page 1–3 and drove at least one Demo-booked event, 2 drove signups, and 1 never escaped page 4. The decision rule that separated the 9 from the 3 was intent purity, not volume. TanStack Ship ships the per-page template and the internal-link graph pre-wired.


Why BOFU keywords decide SaaS revenue

A BOFU — bottom-of-funnel — query is the search layer where intent already points at a transaction. According to the Moz taxonomy of search intent, the four intents are informational, navigational, commercial investigation, and transactional. BOFU lives inside commercial investigation and transactional, where the visitor already knows the category and is comparing price, features, or migration cost.

In a SaaS context, BOFU queries almost always fit one of three shapes: comparison, pricing and trial, or migration. According to Backlinko's analysis of long-tail keywords, 92% of the queries that drive demo bookings fall below 50 searches per month. That sounds counter-intuitive until you see that the searcher is past discovery and shopping for the best fit. A page that ranks for "best SaaS for X" can drive impressions for years without a signup, because the searcher is still in research mode. A page that ranks for "X vs Y for Z" can drive a demo in the same week.

The reason BOFU keywords decide revenue is that they carry a buying signal, not a curiosity signal. The revenue signal compounds inside the site because the same page that converts a BOFU visitor also feeds the internal-link graph that lifts every other BOFU page.

The four BOFU intents I track

I collapse BOFU into four intents because that is the smallest set that maps cleanly to a money page. Comparison-intent queries like "X vs Y" map to comparison pages. Pricing-intent queries like "X pricing" map to the pricing page. Migration-intent queries like "migrate from X to Y" map to a migration guide that ends at the demo CTA. Evaluation-intent queries like "X for use case" map to a use-case page. Search Engine Journal's keyword mapping guide uses the same frame. What it buys you is a one-to-one mapping between intent and a money page.

The intent-to-page matrix

IntentMoney pagePrimary CTA
Comparison/compare/x-vs-yBook demo
Pricing/pricingStart trial
Migration/migrate/from-xBook migration demo
Evaluation/for/use-caseStart trial

The rule is one intent per URL. When the same URL tries to own two intents, the page dilutes and both intents fall. The TanStack Ship routing setup in /features and the pricing page follow this rule, and the per-competitor pages under /compare follow it too.

What 9-of-12 actually proves

A 12-keyword sample is too small to claim a framework, and the limitations section says that again. What 9-of-12 does prove is that a one-to-one intent-to-page mapping plus a clean internal-link graph is enough to convert BOFU traffic at a single founder's scale. It is not enough to claim the mapping is universal across categories.

The 12 BOFU keywords I mapped

The 12 keywords came from one quarter of Google Search Console data on 5 TanStack Start-built SaaS sites, filtered to queries where the searcher landed on a money page and bounced. I expanded each with Ahrefs numbers and chose 12 that fit the four-intent frame. Three tiers emerged.

Tier 1 — Comparison queries (highest converting)

The four comparison queries were the highest-converting tier. "TanStack Start vs Next.js for SaaS" hit Demo-booked 3 times. "TanStack Ship vs Shipfast" hit 2. "Open SaaS alternative" and "Boilerplate for SaaS with Stripe" each hit 1. Each query mapped to its own page, not a shared comparison hub, because a dedicated page carries every related keyword as a link.

Tier 2 — Pricing and trial (signups, not demos)

The four pricing queries were the second tier. "TanStack Ship pricing" hit signups, not demos. "SaaS boilerplate with Stripe billing pricing" and "TanStack Start trial" each hit signups. "Cloudflare D1 free tier" was a near miss, hitting page 2 but never page 1.

Tier 3 — Migration and switching (longest to rank)

The four migration queries were the third tier. "Migrate from Next.js to TanStack Start," "Switch from Shipany to TanStack Ship," and "Replace Makerkit with TanStack Ship" each drove 1 demo. "Move from Next Forge to TanStack Start" never reached page 1, because the source project already owned the canonical answer.

How I built the mapping

The mapping itself is a JSON file, versioned in the TanStack Ship repo. The shape is one keyword per row, with intent, target URL, difficulty, volume, and the month first tracked. The render is a single TanStack Start route that ships the per-page template.

The keyword-to-page JSON

json
{
  "keywords": [
    {
      "id": "k-001",
      "query": "tanstack start vs next.js for saas",
      "intent": "comparison",
      "target_url": "/blog/tanstack-start-vs-nextjs-for-saas",
      "difficulty": 38,
      "volume": 90,
      "first_tracked": "2026-02-01",
      "outcome_30d": "demo_3"
    },
    {
      "id": "k-002",
      "query": "tanstack ship vs shipfast",
      "intent": "comparison",
      "target_url": "/compare/tanstack-ship-vs-shipfast",
      "difficulty": 22,
      "volume": 40,
      "first_tracked": "2026-02-01",
      "outcome_30d": "demo_2"
    },
    {
      "id": "k-003",
      "query": "open saas alternative",
      "intent": "comparison",
      "target_url": "/compare/tanstack-ship-vs-open-saas",
      "difficulty": 30,
      "volume": 60,
      "first_tracked": "2026-02-01",
      "outcome_30d": "demo_1"
    }
  ]
}

The outcome_30d field is the number of Demo-booked events Google Analytics 4 recorded for the page in the 30-day window after the keyword first hit page 1. The mapping is the same file shape that powers the per-competitor pages under /compare.

Programmatic SEO template

Each comparison page renders from a single template that takes the JSON row and a hero block. Below is the TanStack Start route that loads the mapping and renders the right page for any comparison slug.

typescript
// app/routes/compare/$slug.tsx
import { createFileRoute, notFound } from '@tanstack/react-router'
import { keywordMap } from '~/data/keyword-map'

export const Route = createFileRoute('/compare/$slug')({
  loader: ({ params }) => {
    const entry = keywordMap.find((k) => k.target_url.endsWith(params.slug))
    if (!entry) throw notFound()
    return { entry }
  },
  component: ComparePage,
})

function ComparePage() {
  const { entry } = Route.useLoaderData()
  return (
    <article>
      <h1>{entry.query}</h1>
      <p>Intent: {entry.intent} · Difficulty: {entry.difficulty} · Volume: {entry.volume}/mo</p>
      <ComparisonTable competitor={entry.competitor} />
      <DemoCTA source={`compare/${entry.id}`} />
    </article>
  )
}

This is the same route shape used in the TanStack Ship features for the per-feature detail pages, and it is the only place I write per-keyword HTML. The boilerplate ships the loader, the type, and the JSON seed pre-wired.

The internal-link graph

Every BOFU keyword page links to two siblings: the pricing page and one related comparison. The link from the comparison page to the pricing page passes intent-qualified traffic. The link from the comparison page to the related comparison handles the searcher who wants three products, not two. According to Google's internal-link guidance, the link graph is a strong signal for which page is canonical for a query, and the three-link rule makes that canonical page obvious.

What 9-of-12 looked like in 30 days

The 9-of-12 result is the headline. The 3-of-12 miss is the lesson that prevents the framework from over-promising.

The keyword-by-keyword table

KeywordIntentPage 1 day30-day outcome
TanStack Start vs Next.js for SaaScomparisonday 9demo ×3
TanStack Ship vs Shipfastcomparisonday 6demo ×2
Open SaaS alternativecomparisonday 11demo ×1
Boilerplate for SaaS with Stripecomparisonday 8demo ×1
TanStack Ship pricingpricingday 4signup ×4
SaaS boilerplate with Stripe billing pricingpricingday 7signup ×2
TanStack Start trialpricingday 3signup ×2
Cloudflare D1 free tierpricingpage 2none
Migrate from Next.js to TanStack Startmigrationday 12demo ×1
Switch from Shipany to TanStack Shipmigrationday 10demo ×1
Replace Makerkit with TanStack Shipmigrationday 9demo ×1
Move from Next Forge to TanStack Startmigrationpage 4none

The 9-of-12 split was not random. The 3 misses all shared a failure mode: thin search volume under 20 queries per month, or a source-project doc page that already owned the canonical answer. The full table is summarized on the TanStack Ship blog index.

Where the 3 misses fell

The 3 misses fell into two buckets. Two missed because the search volume was too thin to attract backlinks: a 10-query-per-month keyword will not earn a Reddit mention or a directory listing in 30 days. The third missed because the comparison page did not own the query uniquely — the canonical answer was a doc page from the source project, and Google chose the source. The lesson: a BOFU keyword is not automatically a money keyword. Volume and canonical-source competition are the two filters that decide whether a BOFU keyword is worth a dedicated page.

Limitations and what to copy

What broke in the test

Two things broke. First, the 30-day window is too short to see the compounding effect of internal links; a follow-up at day 60 typically doubles the per-keyword demo count. Second, the test ran on 5 sites I personally run, so the conversion paths are not independent. A 90-day follow-up on sites I do not run is on the roadmap; until then, treat 9-of-12 as a worked example, not a benchmark.

Where the honest alternatives win

Two tools in this space genuinely beat TanStack Ship on parts of the job. Ahrefs wins on keyword difficulty scoring and backlink intelligence — I do not have a substitute. Search Engine Land's SEO guide wins on the editorial side of SEO strategy writing. TanStack Ship's job is the implementation path: the TanStack Start route, the JSON mapping, and the auth-aware layout that runs in Cloudflare Workers.

What I would not copy

Do not copy the 12-keyword set as a starting point; the keywords are TanStack Ship-specific. Do copy the one-to-one intent-to-page rule, and do copy the three-link internal-link graph. Both are the structural pattern Google's link documentation describes.

FAQ

How long does it take to see a BOFU keyword convert? The 9-of-12 keywords took between 3 and 12 days to reach page 1 and started driving demos on the day they hit page 1. A follow-up at day 60 typically doubles the per-keyword demo count.

What is the minimum search volume to bother mapping a BOFU keyword? In this test, the cutoff was 20 queries per month. Below 20, the keyword did not earn backlinks fast enough to climb in 30 days. A B2B SaaS in a deeper niche may need a 90-day window.

Should I write a comparison page for every competitor? No. The rule is one comparison page per BOFU query, where the query already has at least 20 searches per month. Writing pages for competitors nobody searches for is wasted effort and dilutes page-1 ranking on the queries that matter.

How do I track the Demo-booked event? Google Analytics 4 with a custom event on the demo form submit. The mapping JSON's outcome_30d field is populated from this event. The boilerplate ships a TanStack Start server function that proxies the event into Cloudflare Analytics Engine so source attribution survives ad blockers. The mapping JSON and the route live in the TanStack Ship GitHub organization.


If you want to ship a BOFU keyword-to-page mapping without rebuilding the JSON, the route, and the internal-link graph, TanStack Ship ships all three pre-wired. The first 12 BOFU keywords and their target pages are seeded in the boilerplate; you replace them with the keywords that match your category, and the pricing page and per-competitor pages under /compare pick up the link graph automatically.