Ship Data Apps with AI

📣 Grow & reach·Lesson 21 of 26·~20 min·Intermediate

🔎 Get found on Google

Give search engines real pages beside your app: guides, a front door at /, honest pages generated from your data, sitemaps and structured data.

You'll leave with

  • An About/FAQ page and two guides people actually search for
  • A sitemap, robots.txt and JSON-LD you've audited
  • A plan for honest pages generated from your own data

Search engines read what your server (the always-on computer your app lives on) sends, and for a single-page app that can be little more than a title and links to code. Today: real pages beside your app, a landing page at /, maybe hundreds of honest pages from your data, and the boring tags, audited. Claude Code writes; you decide what's true.

What are VS Code and Claude Code?

VS Code is a free code editor. Claude Code is Anthropic's AI assistant that writes files and runs commands in your project folder, with your permission. Set up your workshop installs both.

What are HTML, JavaScript and a single-page app?

HTML is the text and structure a server sends; JavaScript is code the browser runs afterwards. A single-page app is one HTML page whose JavaScript draws everything else: nearly blank to any program that doesn't run it.

What are a crawler, the index and SEO?

A crawler is a search engine's program that fetches pages and follows their links. The index is its library of pages read; only indexed pages appear in results. SEO, search engine optimization, is making pages it can find and trust.

1

Give search engines something to read

Open your live app in Chrome or Edge on a computer and press Ctrl+U (Mac: Cmd+Option+U): View Source, the HTML exactly as your server sent it. (Safari needs its developer menu turned on; phones can't.) Press Ctrl+F (Mac: Cmd+F) and search for a sentence you see in the app. Not found? A crawler that doesn't run JavaScript can't see it either.

The fix is a few plain pages beside the app. First Bites, my first-foods tracker for babies, got an About page with an FAQ and two guides, on starting solids and on introducing allergens: the first words on its address a search engine could read.

  • about.html: what the app is, who made it, and an FAQ with each answer in a <details> click-to-open block, still in the HTML when folded.
  • Two guides, each answering one real search: look in Google's suggestions and "People also ask" box, or in questions users send you.

⚠️ Claude drafts; you vouch for every sentence. First Bites' guides stay factual: no prevention or cure claims.

Real links, a sitemap and robots.txt

Crawlers follow links, and Google's docs say it can generally follow only an <a> tag with an href:

❌ A guide behind these may never get indexed

  • <button onclick="openGuide()">
  • <a onclick="go('about')">, with no href

✅ Crawlers follow these

  • <a href="/about.html">About</a> in a plain footer inside the app
  • The same pages, listed in sitemap.xml
What are sitemap.xml and robots.txt?

sitemap.xml lists every page you want found. robots.txt, at your site's top level, tells crawlers what they may fetch and where the sitemap is. Both are plain files you upload.

EXAMPLE — YOU ASK CLAUDE CODE

Claude Code
YouAdd an About/FAQ page and two guide pages that answer YOUR FIRST SEARCH and YOUR SECOND SEARCH, all sharing one pages.css, linked from a footer in index.html with normal <a href> links, plus sitemap.xml and robots.txt. Use only facts from sources.md; mark gaps TODO.
ClaudeAdded about.html, two guides, pages.css (their shared look), robots.txt, a sitemap of 4 addresses and a three-link footer. Three sentences are marked TODO where sources.md was silent.↳ Read · sources.md↳ Write · about.html, 2 guides, pages.css, sitemap.xml, robots.txt↳ Edit · index.html (footer)

Replace the bold searches. First create sources.md in your app's folder: your notes and links to trusted pages. In VS Code, Ctrl+Shift+F (Mac: Cmd+Shift+F) finds every TODO. Add your analytics tag from Know if it's working to each new page.

What's happening underneath: two small files. Replace myapp.yourdomain.com and the guide names; name each guide after its search, like guide-starting-solids.html.

User-agent: *
Allow: /
Sitemap: https://myapp.yourdomain.com/sitemap.xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://myapp.yourdomain.com/</loc></url>
  <url><loc>https://myapp.yourdomain.com/about.html</loc></url>
  <url><loc>https://myapp.yourdomain.com/guide-TOPIC-ONE.html</loc></url>
  <url><loc>https://myapp.yourdomain.com/guide-TOPIC-TWO.html</loc></url>
</urlset>

Each new page adds a <url> line. Upload as in Deploy behind one front door and fetch robots.txt live (replace YOUR_VPS_IP and the guide names; in PowerShell, type curl.exe):

scp index.html about.html guide-TOPIC-ONE.html guide-TOPIC-TWO.html pages.css sitemap.xml robots.txt root@YOUR_VPS_IP:/root/myapp/
curl -s https://myapp.yourdomain.com/robots.txt
User-agent: * Allow: / Sitemap: https://myapp.yourdomain.com/sitemap.xml

Now Ctrl+U on the live about.html shows the FAQ's words in the source.

Tell Google: Search Console

Search Console is Google's free dashboard for site owners: prove a domain is yours, submit your sitemap, then see what got indexed and which searches showed you. It needs your Google account, so you click, not Claude:

  1. Sign in at search.google.com/search-console.
  2. In the property menu (top left), click Add property, pick Domain (not URL prefix), type yourdomain.com and press Continue. It covers every subdomain, future apps included.
  3. Copy the TXT record it shows (google-site-verification=…) and leave the tab open.
What's a TXT record?

A line in your domain's DNS (its entry in the internet's address book) holding text, not an address. Services read one to confirm you control the domain. Your email's SPF line, which says who may send as you, is a TXT record too.

Claude adds it with the Hostinger MCP, the plug-in from Your own server. Replace YOUR_CODE:

EXAMPLE — YOU ASK CLAUDE CODE

Claude Code
YouUsing the Hostinger MCP, list my DNS records for yourdomain.com, add a TXT record at @ with the value google-site-verification=YOUR_CODE, TTL 300, overwrite false, and list them again. Then ask 1.1.1.1 for the TXT records.
ClaudeAdded at @. The new list is the old one plus that line; your SPF record (v=spf1…) is still there. 1.1.1.1 already returns your code.↳ hostinger · execute · list DNS records, update DNS records (overwrite: false), list again↳ Bash · nslookup -type=TXT yourdomain.com 1.1.1.1

What's happening underneath: @ is the bare domain; overwrite false adds instead of replacing. Check in any terminal (replace yourdomain.com); one line should hold your code:

nslookup -type=TXT yourdomain.com 1.1.1.1

🛑 Overwrite true would delete every TXT record at @, SPF included, and your email could start landing in spam. Line missing from the new list? Ask Claude to restore the DNS snapshot from before, or re-add it from the first list. DNS hosted elsewhere? Add the same record (TXT, @, your code) on that company's DNS page.

Back in Search Console, press Verify; it should say ownership is verified. If not, wait ten minutes and retry. Keep the record: Google rechecks it.

Then open Sitemaps (left menu, under Indexing), enter https://myapp.yourdomain.com/sitemap.xml (if the box shows the start, type the rest) and press Submit. Success: the status Success, with as many discovered pages as you listed. Couldn't fetch? If the address opens in your browser, wait a day.

Days later, open Pages (under Indexing): indexed versus not, each with a reason, like "Discovered – currently not indexed" (known, not yet read); a new site can wait weeks. Performance shows the searches that showed you. Menus move; if these have, search Search Console Help.

🦥 Skipped: keyword tools, backlinks and anyone selling SEO. Chase particular searches once Performance shows which ones find you.

2

Front door at /, app at /app/

Once an app works, the address people type should explain it. (Front door here means a landing page, not your server's front door from Deploy.) Baby Signs, my baby sign language app, moved to /app/ and gave / a landing page: a live sign sequence on top, why and how, a gallery and one Open the app button, plus two guides marked up as articles (step 5). Launch, honestly covers a landing page's order.

EXAMPLE — YOU ASK CLAUDE CODE

Claude Code
YouCommit, then move index.html to app/index.html and fix the paths it loads. New index.html at /: live demo, why and how, FAQ, one Open the app button. Add /app/ to sitemap.xml. If we have a manifest and service worker: start_url /app/, new cache name, offline fallback /app/. Commit.
ClaudeDone, committed before and after. start_url is /app/, the cache is myapp-v3 (was v2), the fallback /app/. To check, open /app/ locally with DevTools' Network tab open and reload: no red rows.↳ Bash · git commit, git mv index.html app/index.html↳ Write · index.html · Edit · manifest, sw.js, sitemap.xml

What's happening underneath: paths are relative to the page, so from /app/, style.css means /app/style.css; each file moves too or gets a leading slash. DevTools is the browser's inspector (Ctrl+Shift+I; Mac: Cmd+Option+I); a red Network row is a file that didn't load. Not Found for /app/ locally? Ask Claude to serve /app/index.html; Caddy already does. Then upload what changed (scp -r copies the app/ folder) and open /app/ live. To undo: git revert HEAD, then upload again.

What are a manifest and a service worker?

Two files that make a site installable on phones (the next lesson, Make it an app on their phone). The manifest names the start page; the service worker, a script the browser keeps, saves your files for offline use.

Have both? Baby Signs' move shows all three settings must follow the app:

  • start_url left at /: the installed app opens the landing page.
  • Cache name unchanged: returning visitors keep the old app.
  • Offline fallback left at /: offline, people get the landing page.

One app can also choose. The List, my explorer for New York's pied-à-terre tax roll, gives a bare visit a landing page (the headline count, one big search box, the two key findings), while a link carrying URL parameters opens the workspace as the sender saw it.

What are URL parameters?

The part of an address after ?, like ?view=method: name=value pairs the page reads to decide what to show.

The trap: write defaults into the address before checking, and every visit looks like a shared link. Check first (showLanding and showWorkspace are yours):

const hadParams = location.search.length > 1;   // before anything writes defaults
// ...your code that reads filters and writes defaults stays here...
if (hadParams) showWorkspace(); else showLanding();
3

Programmatic pages, honestly

If your data has natural combinations people search together, like procedure and city, one template (a page layout your data fills in) makes a page for each. My hospital price tool grew from one search page to about 1,600 indexable pages that way, more as states were added, like /arizona/phoenix/mri-brain: each with a specific title, a visible FAQ, JSON-LD and a canonical link.

What's JSON-LD?

A hidden block in a page's HTML that tells search engines, in schema.org's shared vocabulary, what the page is: an article by this author, a set of questions and answers. Step 5 shows one.

What's a canonical link?

A line in a page's head (the HTML visitors don't see) naming its one official address, so two addresses showing the same page count as one.

  1. One address per page. The price tool's billing-code addresses, like /phoenix/procedure/70551, name the readable one as canonical: the cure for duplicate-content warnings.
  2. Titles a person would write: "What does an MRI of the brain cost in Tucson?" The price tool's once read like a robot's ("What does a MRI Brain cost"): data labels pasted mid-sentence. The fix: lowercase mid-sentence, except a keep-caps list of acronyms and proper names, with a test.
  3. No data, no page. An empty combination stays out of the sitemap and answers an honest 404; an empty page that answers 200 (here it is) is thin content.
What's a status code?

The three-digit number that starts every server answer: 200 means here it is, 301 moved for good (a redirect), 404 no such page.

import assert from 'node:assert/strict';
const KEEP_CAPS = ['MRI', 'CT', 'X-ray', 'Achilles'];      // acronyms and proper names: add yours
const keep = new Map(KEEP_CAPS.map(w => [w.toLowerCase(), w]));
export const midSentence = label =>
  label.split(' ').map(w => keep.get(w.toLowerCase()) ?? w.toLowerCase()).join(' ');

assert.equal(midSentence('MRI Brain'), 'MRI brain');
assert.equal(midSentence('X-RAY Chest'), 'X-ray chest');
assert.equal(midSentence('ACHILLES Tendon Repair'), 'Achilles tendon repair');
console.log('titles ok');

node titles.mjs prints titles ok or stops at the first wrong case (add one per keep-caps word); your page generator imports it, so a broken rule stops the build. (It can't fix "a MRI": hand-write your top phrases.) In a static app, the generator is build.mjs from Build static-first: no data, no file, so the server answers 404 itself.

EXAMPLE — YOU ASK CLAUDE CODE

Claude Code
YouDon't build pages yet. From data.json, count my CITY × PROCEDURE combinations and how many have data. Plan in NOTES.md: URL pattern, a title template a person would type, keep-caps words from my labels, what we leave out, and the counting command.
ClaudePlan in NOTES.md with both counts, /STATE/CITY/PROCEDURE, a title template, keep-caps words and the command. No pages created. Run the command; its numbers should match the plan.↳ Bash · node -e (count combinations in data.json)↳ Edit · NOTES.md

What's happening underneath: Claude groups rows by your two columns (replace CITY × PROCEDURE) and counts groups with and without data. NOTES.md is the notes file Claude reads each session. If command and plan disagree, the plan is wrong.

🦥 The outcome is a plan, not pages. Twenty good pages beat two thousand thin ones; the price tool's readable addresses cover only its top 10 procedures.

4

Pages that don't rot

A resources page made of searches

Acento's resources page (my Argentine Spanish trainer) is plain HTML, no JavaScript needed: seven sections of starter links. Videos and music link to a YouTube or Spotify search, never one video: single videos vanish, and a page of them can be half 404s within a year. Each link is one line (replace SHOW+NAME, + between words, and the text):

<a class="res" href="https://www.youtube.com/results?search_query=SHOW+NAME">Show Name — why it's good</a>

Ask Claude: "Make resources.html with no JavaScript, one line per link, linking to searches or official channels, never single videos; link it from the top bar and footer and add it to sitemap.xml." Then click each link once.

An FAQ in your readers' words

The List's methodology page opens with its FAQ: questions copied word for word from people's messages ("Do I owe this?"), and every number computed from the loaded data, never typed in, so it can't drift. Readers wanted to cite "how this works", so ?view=method opens it. Keep view parameters apart from filter parameters, or opening a view resets the filters.

5

The boring tags, and the audit

Every page you want found gets these lines in its head (example values; replace them all):

What are a title, a meta description and noindex?

The title names the page in the tab and is usually the blue link in results; the meta description is the sentence often shown under it. noindex (<meta name="robots" content="noindex">) asks search engines to leave a page out.

<title>How to start solids: a first-week plan | MyApp</title>
<meta name="description" content="What to offer, how much and when, in the first week of solids.">
<link rel="canonical" href="https://myapp.yourdomain.com/guide-TOPIC-ONE.html">
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "How to start solids: a first-week plan",
  "author": { "@type": "Person", "name": "YOUR NAME", "url": "https://myapp.yourdomain.com/about.html" },
  "datePublished": "2026-10-01"
}
</script>

@type says what a page is: Article for a guide, Course for a course, Organization or Person (usually on the About page) for who's behind it. Paste each address into validator.schema.org: it should list your Article with no errors. One stray comma and the whole block is ignored.

One canonical per page, pointing at itself unless another address is official. Give anything unlisted noindex and leave it out of the sitemap, but don't block it in robots.txt: a crawler that can't fetch a page never sees its noindex.

The audit

📍 An audit of traqqit.com, my hub page, found its About link, and the JSON-LD with it, pointing at my business's own website, whose domain had lapsed without anyone noticing. Anyone checking who I was, right as they decided whether to trust me, got a hosting company's "Website Expired" page.

What are curl and grep?

Command-line tools: curl fetches an address and can print its status code; grep finds text in files. Git Bash and the Mac terminal have both.

EXAMPLE — YOU ASK CLAUDE CODE

Claude Code
YouCheck every outbound link and JSON-LD URL on the site and list any that redirect, expire or 404, with status codes and, for domains I don't own, page titles.
ClaudeTwo need you: one redirects (301), and one answers 200 titled "Website Expired". The rest answer 200 with fitting titles.↳ Bash · grep -ohE 'https?://…' *.html, then curl each address

What's happening underneath (rerun it in Git Bash or the Mac terminal, not PowerShell, in your app's folder):

grep -ohE 'https?://[a-zA-Z0-9./_-]+' *.html | sort -u > links.txt
while read -r u; do
  rm -f page.tmp
  code=$(curl -s -o page.tmp -w '%{http_code} %{redirect_url}' --max-time 10 "$u")
  title=$(grep -o '<title>[^<]*' page.tmp 2>/dev/null | head -1)
  echo "$code | $title | $u"
done < links.txt
rm -f page.tmp links.txt

Each line is status, title, address. Point a redirected link at its new address; 404 is gone, and 000 means nothing answered, often a dead domain. Then read the titles: a lapsed domain's parking page can answer 200. Run it monthly.

The cure is boring: auto-renew every domain you own (they're rented by the year; the switch is on the domain's page in hPanel or your registrar's account), and keep key pages on infrastructure you control, like a subdomain of one you already renew. And days after step 1, reread the Pages report: every sitemap address indexed, or with a reason you understand.

Do it with Claude: the prompts

Paste one, check it as its step shows, then the next. Replace the CAPS parts; H1 is a page's main heading.

Add an About/FAQ page and two guide pages that answer YOUR FIRST SEARCH and YOUR SECOND SEARCH, linked with normal <a href> links from the app's footer, plus sitemap.xml and robots.txt.

Give every guide its own title, meta description and canonical link in its head, plus Article JSON-LD with its headline, author YOUR NAME and datePublished.

Generate one page per YOUR COMBINATION from a template with a specific title, H1, FAQ, JSON-LD and canonical link; skip combinations with no data and leave them out of the sitemap.

Check every outbound link and JSON-LD URL on the site and list any that redirect, expire or 404.

What's happening underneath: crawlers read the HTML your server sends; anything only JavaScript draws later may never be seen. Push back when Claude links pages with buttons, fills in text with JavaScript, lists empty combinations, builds titles from raw labels, or leaves noindex on a page you want found. The check never changes: View Source, find the words.

Your turn

  • Put about.html (with an FAQ), two guides for real searches, robots.txt and sitemap.xml live, the pages linked from your app's footer with <a href>. Done when View Source on each page shows its words and the live robots.txt shows the Sitemap line.
  • Verify your domain in Search Console and submit the sitemap. Done when its status says Success and, days later, you can explain every not-indexed reason in Pages.
  • Give each guide a specific title, description, canonical link and Article JSON-LD. Done when validator.schema.org shows no errors.
  • Run the link audit. Done when no line shows 404, 000 or an unfixed redirect, every outside title looks right, and auto-renew is on.
  • Write your page plan in NOTES.md. Done when it has the URL pattern, a title template, the count of combinations with data (from a command you re-ran) and what's left out.

Next, Make it an app on their phone: a manifest, icons and a service worker, so the people Google sends can keep your app on their home screen.