# SEO & AI Visibility

## SEO & AI Visibility

**Files:** `src/config/config.toml`, page frontmatter · **Skill level:** Beginner

Homio does the technical SEO for you: page titles and descriptions, canonical links, a sitemap, `robots.txt`, structured data for a local business, social preview cards, RSS, and files for AI assistants. Your job is to set your address and business details, and to write good titles and descriptions.

### First, Set Your Address
```toml
[site]
  baseUrl = "https://www.your-domain.com"   # ships as "https://example.com"
```

Canonical links, the sitemap, `robots.txt`, RSS feeds, `llms.txt`, structured data, and social preview URLs all use it. Until you change it, `npm run build` prints a yellow `[site]` warning. Don't deploy while you see it: every page would point search engines at `example.com`.

A `SITE_URL` environment variable on the build takes priority over `baseUrl`. On Vercel, the included `vercel.sh` sets one for you. `npm run generate-og-image` reads only `baseUrl`.

### Business Details For Search
Search engines and map results read your business from `[business]` in `config.toml`: the name, phone, email, address, opening hours, closed dates, and type. Set them once there (see [Business Info](/homio/customization/business-info/)); every page and the structured data use them.

```toml
[business]
  type = "LocalBusiness" # LocalBusiness | HomeAndConstructionBusiness | GeneralContractor | HousePainter | Plumber | Electrician | HVACBusiness | RoofingContractor | Locksmith | MovingCompany | ProfessionalService
  priceRange = "$$"

  [business.address]
    show = true # false = service-area business: street and postal code are hidden everywhere

  [business.geo] # Optional, improves map results; leave empty if unsure
    latitude  = ""
    longitude = ""
```

House cleaning has no schema.org type of its own: `LocalBusiness` is the right choice for it. A handyman or trade business can pick a narrower type from the list. If you serve customers at their homes and don't want your street shown, set `show = false`: your service areas then describe where you work.

### Titles And Descriptions
Every page and entry accepts these frontmatter fields:

```yaml
---
title: "Deep clean"                                # shown on the page
metaTitle: "Deep cleaning, priced by room"         # search result title (optional)
description: "A top-to-bottom reset..."            # page summary
metaDescription: "Deep cleaning with a room-by-room checklist..."  # 140–160 characters (optional)
image: "/images/services/deep-clean.jpg"           # social preview image (optional)
canonical: ""                                      # only if the page is a copy of another URL
robots: ""                                         # for example "noindex, follow"
keywords: ["deep cleaning", "house cleaning North Harbor"]
---
```

Titles get your tagline added: "Deep cleaning, priced by room - Your tagline". Set it in `[site]`:

```toml
[site]
  title            = "Homio"   # your brand name
  description      = "..."     # the default meta description
  tagline          = "Cleaning & Home Services Astro & Tailwind CSS Theme" # replace with a short line about your business
  taglineSeparator = ""        # default is " - "
```

`disableTagline: true` turns the tagline off for one page. `[seo] author` and `keywords` are the site-wide defaults; `[seo] robots` ships with `max-snippet:-1, max-image-preview:large` so search and AI answers can quote your pages and show their images in full. Replace the theme's sample `title`, `tagline`, and `description` before launch.

### Structured Data (JSON-LD)
```toml
[seo.schema]
  enable  = true # JSON-LD on every page: business, website, page, plus services, areas, and prices where they apply
  faqPage = true # FAQPage markup on /faq/
```

Homio builds it from your content, so it always matches what visitors see:

| Page | Structured data |
| --- | --- |
| Every page | Your business (type, name, address, phone, hours, price range, areas served) and the website |
| Service pages | A `Service` with its provider, the areas where it is offered, and its from-price |
| Area pages | The area with its region and postal codes; with a real `office`, a branch with its own address and phone |
| `/areas/` | A list of your area pages |
| `/pricing/` | An offer per priced plan and a catalog per trade of the rate sheet |
| `/book/` | A booking action on your business |
| `/faq/` | `FAQPage` with every question |
| Job pages | `JobPosting` with pay, type, and dates |
| Posts | `BlogPosting` with the author |
| Inner pages | Breadcrumbs |

<Aside type="note" title="No review stars in the markup">
  Homio never marks up reviews or ratings. Reviews hosted on your own site count
  as self-serving for Google, which doesn't show review stars for them. Your
  reviews and rating still show on the page for visitors.
</Aside>

Keep the data true: the job posting data is for real openings only, and the sample areas and prices must be replaced before launch. After launch, test a page with Google's [Rich Results Test](https://search.google.com/test/rich-results).

### Social Preview Cards
```toml
[opengraph]
  image          = "/images/og-image.jpg" # fallback image, public/images/og-image.jpg
  generateImages = true   # a 1200×630 card for every page without its own image
  theme          = "light" # light | dark
  twitter        = ""     # your X (Twitter) handle
```

- A page with its own `image` uses that photo.
- Every other page gets a card generated during `npm run build`, with your logo, the section, the page title, and the description, in your fonts and colours.
- `image` is the fallback card. Run `npm run generate-og-image` after you change the brand, colours, or fonts (and after setting `baseUrl` and `[site]`) to redraw it in your branding.

### Sitemap, Robots, And Hidden Pages
- `/sitemap-index.xml` lists every page. Leave a page out with `excludeFromSitemap: true`, or every page in a folder with `[seo.sitemap] exclude` (it ships with `book/thanks`). Draft pages are removed automatically.
- `/robots.txt` is built from `[seo.robotsTxt]`. Add paths crawlers must skip to `disallow`.

These pages ship hidden from search engines (`noindex`):

| Page | Why |
| --- | --- |
| `/home-cleaning/`, `/home-commercial/`, `/home-handyman/` | Demo homepages. Remove `robots` and `excludeFromSitemap` from the one you make your home |
| `/elements/` | The layout gallery, a reference for you |
| `/book/thanks/` | The thank-you page after a booking without JavaScript |
| 404 pages | Not real pages |
| Blog category and tag pages with fewer than 2 posts | Thin pages |

### AI Search And AI Assistants
People ask AI assistants "who cleans homes near me". Homio lets them read and cite your site, and gives you control over each kind of AI crawler.

**1. Choose which AI crawlers may visit**

```toml
[seo.robotsTxt]
  aiSearch       = true  # AI search and answer engines that fetch pages to cite them (ChatGPT search, Claude, Perplexity, ...)
  aiTraining     = true  # crawlers that collect content to train AI models (GPTBot, ClaudeBot, Google-Extended, CCBot, ...)
  contentSignals = false # true adds a Content-Signal line (contentsignals.org) with the two choices above
```

Each switch writes `robots.txt` rules for a list of crawlers in `src/lib/utils/aiCrawlers.ts`. `aiSearch = false` removes your site from those engines' answers; `aiTraining = false` opts out of training only. Neither affects Google Search or AI Overviews, which use the normal Googlebot and follow your regular search rules. `contentSignals` ships off because Lighthouse doesn't know the line yet and lowers the SEO score.

**2. `llms.txt` and Markdown copies**

```toml
[seo.ai]
  llmsTxt       = true # /llms.txt (index) and /llms-full.txt (all text), per language
  markdownPages = true # a Markdown copy of each service, area, project, post, job, pricing, FAQ, and guarantee page
```

- `/llms.txt` is a Markdown map of your site that some AI tools read; `/llms-full.txt` holds the text of every page. Both open with your business facts from `[business]`, then your services with prices and checklists, prices, areas with ZIP codes, and pages. Each language gets its own (`/fr/llms.txt`).
- With `markdownPages`, a service at `/services/deep-cleaning/` is also served as `/services/deep-cleaning.md`. The included `public/_headers` (Netlify, Cloudflare Pages) and `vercel.json` serve these as text and mark the `.md` copies `noindex`, so search engines don't index them in place of your pages.
- Leave a page out with `excludeFromAi: true`. Pages marked `noindex` and drafts are left out automatically.

Search engines don't document using `llms.txt`, so treat it as a convenience for AI tools, not as an SEO setting.

**3. The Copy page and Ask links on posts**

```toml
[settings.pageActions]
  enable    = true
  footer    = false # a footer row with AI links and llms.txt on every page (off for most local businesses)
  providers = ["chatgpt", "claude", "perplexity", "grok"] # empty hides the "Ask" links
```

Blog posts get a **Copy page** menu: copy the post as Markdown, open the Markdown copy, or ask an assistant about it.

### IndexNow (Bing And Others)
IndexNow tells Bing, Yandex, Seznam, and Naver which pages changed, so they update faster. Bing also feeds ChatGPT search and Copilot. It ships off.

1. Set a key of 8 to 128 letters, numbers, or dashes in `config.toml`: `indexNowKey = "your-key"` under `[seo.ai]`. The build publishes it at `/<key>.txt`.
2. Deploy.
3. After each deploy, run `npm run indexnow`. It sends the pages whose sitemap date changed in the last 7 days. `npm run indexnow -- --all` sends every page (first launch), `--days=30` a longer period, and `--dry-run` lists the pages without sending.

### Do It With AI
```text
Improve SEO for my business. For every content file with a title in src/content/**/english/,
write a metaDescription of 140–160 characters from the page content, and a metaTitle under 60
characters where the title is vague. Don't change other fields. Then do the same in French. Set
[site] title, tagline, and description, and [seo] keywords in src/config/config.toml for
[BUSINESS, SERVICES, TOWNS], and check that [site] baseUrl is "[MY URL]".
```