A starter for local service businesses — tradespeople, studios, clinics, any business that serves a city and lives or dies on being found in it.
Services and locations are Markdown. Schema, sitemap, robots.txt and llms.txt are generated at
build time. Ships zero JavaScript and one small CSS file, so the Core Web Vitals score that
actually matters (the one Google measures) is not something you have to chase.
7 pages · 0 KB JS · 5.6 KB CSS · one config file to edit
Most "Astro + SEO" starters give you a <head> component and call it done. Local businesses need
four things that a <head> component doesn't provide:
- One page per service and per city. A single "Leistungen" section can't rank for "Sanitär-Notdienst" and "Badsanierung" and "Sanitär in " at the same time.
- A correct
LocalBusinessgraph. Name, address and phone that match the Google Business Profile exactly. Mismatches cost local rankings, and they're invisible until you check. - A sitemap that matches reality. Generated from the same collections the pages come from, so it can't list deleted URLs.
llms.txt. Not for Google — for ChatGPT, Claude and Perplexity, which is where an increasing share of "who should I call for this" questions get answered.
npm create astro@latest -- --template RelayStack/astro-local-business
# or
git clone https://github.com/RelayStack/astro-local-business
cd astro-local-business && npm install
npm run devThen edit src/config.ts. It is the only file you have to touch to make this someone's site:
business name, address, phone, opening hours, price range, service area. The layout, footer,
schema and llms.txt all read from it, so they cannot contradict each other.
export const site = {
name: "Muster Sanitär AG",
url: "https://example.ch", // ← must match the real domain
phone: "+41 00 000 00 00",
address: { street: "Musterstrasse 1", zip: "0000", city: "Musterstadt", country: "CH" },
hours: [{ days: ["Monday", "Tuesday"], opens: "07:30", closes: "17:00" }],
areaServed: ["Musterstadt", "Beispieldorf"],
// ...
};src/config.ts also drives astro.config.mjs's site, which drives every canonical URL. Set it
before you deploy, not after.
Drop a Markdown file in src/content/services/ or src/content/locations/. The filename becomes
the URL.
---
title: Sanitär-Notdienst
description: 120–160 characters. Becomes the meta description and the llms.txt line.
from: "CHF 180 Pauschale"
order: 1
faqs:
- q: Was kostet ein Notdienst-Einsatz?
a: A short, self-contained answer. This is what gets quoted back to you.
---
Your content. Written for a person, not a crawler.The FAQ frontmatter is emitted as FAQPage schema and rendered as a <details> block, from one
source. Editing an answer in the Markdown updates both.
| File | Source | Notes |
|---|---|---|
/sitemap.xml |
services + locations collections | Native endpoint, no @astrojs/sitemap dependency |
/robots.txt |
src/pages/robots.txt.ts |
Allows AI crawlers on purpose |
/llms.txt |
src/config.ts + collections |
Contact details, services, service area |
| JSON-LD | src/lib/schema.ts |
LocalBusiness, Service, FAQPage, BreadcrumbList |
A note on llms.txt: Google ignores it entirely. It exists for AI assistants. If someone tells
you it helps you rank, they're wrong, and this template won't pretend otherwise.
These are the mistakes that quietly cost local sites their rich results:
- One
LocalBusinessnode per page, never several. Competing nodes are worse than none. - One
@idper business. Service pages reference the business by@idinstead of re-declaring a second business with a slightly different name. - Location pages state
areaServed, not a second address. Two addresses for one business reads as two businesses. geois omitted unless you set it. A wrong coordinate is worse than no coordinate.
Static output — any host works. npm run build produces dist/.
- Cloudflare Pages: build
npm run build, outputdist - Netlify: build
npm run build, publishdist - Vercel: detected automatically
Whichever you pick, add the custom domain and check that https://your-domain/sitemap.xml resolves
before you submit it anywhere.
- Submit
/sitemap.xmlin Google Search Console and Bing Webmaster Tools. - Create or claim the Google Business Profile. Business name, address and phone must match
src/config.tscharacter for character — that consistency is most of local SEO. - Check one page in the Rich Results Test.
- Fetch
/robots.txtand confirm it points at the right sitemap.
Location pages only help if each one says something true about that place. A page that swaps the city name into identical paragraphs is a doorway page, and search engines have filtered those for years. Write what's genuinely different: which neighbourhoods, which building stock, how long the drive is, what's common there. This template gives you the structure; it can't give you the facts.
If you generate the SEO files from a Markdown directory outside Astro, or you want IndexNow
submission and a pre-deploy validator, see
astro-seo-toolkit. This template intentionally
uses native Astro endpoints so it has no extra dependency.
MIT — see LICENSE.