# Service Areas

## Service Areas

**Files:** `src/content/areas/english/` (one file per area, plus `-index.md`), `src/content/areas/french/` · **Skill level:** Beginner

<div class="border border-dark">
  ![A Homio area page](/images/homio/area.jpg)
</div>

A **service area** is a town, district, or group of postal codes your crews cover. Each area is one file in `src/content/areas/english/` and one page at `/areas/<file-name>/`. From those files Homio builds:

- the areas page `/areas/`, with an area finder (type a ZIP code or a town),
- one page per area, with its local phone, hours, neighbourhoods, prices, reviews, and a recent job,
- the **Areas** menu in the header and the **Areas** column in the footer,
- the ZIP check on the booking form,
- `areaServed` in the structured data.

The four shipped areas (North Harbor, Elm Park, Westfield Heights, Old Mill) are **fictional**, with sample ZIP codes. Replace them with your real areas before you launch.

### An Area File
From `src/content/areas/english/elm-park.md`:

```yaml
title: "Elm Park" # area name: lists, menus, breadcrumbs, schema `name`
heading: "" # optional H1; empty uses "Home services in {area}" (i18n `areas.heading`)
description: "Older family houses under big trees, three ZIP codes, and the streets where our first van parked." # lists + meta description (≤ 155 characters)
metaTitle: "Cleaning and handyman services in Elm Park"
region: "East side" # sub-line in area lists
code: "02" # ticket number ("AREA Nº 02"); empty uses `order`
order: 2 # position everywhere (lowest first)
image: "/images/areas/elm-park.jpg" # 16:9 street or houses, no people. Optional
map: "" # optional static map image of YOUR real area; the samples are fictional

zips: ["55504", "55505", "55507"] # sample; the area finder and the booking ZIP check match these
neighborhoods: # sample; the finder matches them too (case and accents ignored)
  - "Elm Park Village"
  - "Linden Row"
aliases: ["Elmpark"] # other spellings or nearby towns the finder matches

# Local contact. Each field falls back to [business] in config.toml.
phone: "+15550100162" # sample (555-01xx fiction range), E.164 for tel links and schema
phoneDisplay: "(555) 010-0162" # sample
hours: {} # same per-day format as [business.hours]: { mon: "07:30-18:00", …, sun: "" }. Empty uses the business hours
office: {} # ONLY a real, staffed location (street, locality, region, postalCode). Never invent one
crews: 3 # sample: crews based in this area
since: 2014 # sample: first year served

services: [] # service ids offered here; empty = every service
serviceNotes:
  - service: "gutter-cleaning"
    note: "Busy late Oct to Nov: book two weeks ahead"

introTitle: "Working in Elm Park"
notes: # 3–4 local facts; `label` is a short mono tag
  - label: "Parking"
    title: "Permit zone on weekdays"
    text: "Parts of Linden Row are permit-only from 8:00 to 18:00."
faq:
  - question: "Do you work in the Fernwood side streets?"
    answer: "Yes, every street in 55507, including the private lanes off Cedar Commons."
nearby: ["north-harbor", "old-mill"] # area ids for "Also nearby"
draft: false
---

Elm Park is where the company started: one van, a written checklist, and the houses around
Schoolhouse Green. …
```

| Field                            | What it does                                                                                          |
| -------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `title`                          | The area name everywhere: menus, lists, breadcrumbs, structured data                                  |
| `heading`                        | The page title. Empty gives "Home services in Elm Park"                                               |
| `description`                    | The line under the title and the meta description. Keep it under 155 characters                       |
| `region`, `code`, `order`        | Sub-line in lists, the number on the area ticket, and the position (lowest first)                     |
| `image`, `map`                   | A 16:9 photo of the area, and an optional static map of your real area                                |
| `zips`                           | The postal codes this area covers. The finder and the booking form match them exactly                 |
| `neighborhoods`, `aliases`       | Place names the finder also matches. They are listed on the page under "Neighbourhoods we cover"      |
| `phone`, `phoneDisplay`, `hours` | A local phone line and hours. Each empty field uses `[business]` in `src/config/config.toml`          |
| `office`                         | Only for a real, staffed office in this area. It adds the address to the page and the structured data |
| `crews`, `since`                 | Facts on the area ticket ("Crews based here", "Serving Elm Park since 2014")                          |
| `services`, `serviceNotes`       | The services offered here (empty means all) and a short note per service                              |
| `introTitle`, the Markdown body  | The intro section ("Working in Elm Park")                                                             |
| `notes`                          | 3 or 4 local facts, each with a short `label`                                                         |
| `faq`                            | Questions for this area                                                                               |
| `nearby`                         | Area ids shown under "Also nearby"                                                                    |

On an area page, the header phone, the phone bar on mobile, and the area ticket use the area's own `phone` when it is set. The ticket's price line ("Standard clean from $110") is the "from" price of the first rate card the area offers (see [Pricing & The Estimator](/homio/customization/pricing-estimator/)).

The page also shows the services offered there, reviews whose `area` is this area, and the newest project whose `area` is this area (hidden when there is none). To change those sections on one area, add a named block to its file, for example `reviewsSection: { limit: 5 }`.

<Aside type="caution" title="Never invent an office">
  Leave `office: {}` empty unless you have a real, staffed location there.
  Search engines treat a listed address as a place customers can visit. Without
  an office, the area page describes a service offered in that area, with its
  postal codes and no street address.
</Aside>

### The Areas Page
`/areas/` is set by `src/content/areas/english/-index.md`. Its page header counts your areas, neighbourhoods, and ZIP codes (`summary: true`), and the area list comes from the area files:

```yaml
areasSection: # defaults: areas-section.md
  finder: true # ZIP code or town lookup above the rows
  options:
    layout: "list" # default (finder + area grid) | list (finder strip + one directory row per area)
```

The same section can appear on other pages with four layouts: `default` (area pills and the finder beside a photo), `list` (one row per area), `finder` (intro beside the finder panel), and `tabs` (one tab per area with hours, address, phone, and email). `/home-cleaning/` uses it. Its defaults are in `src/content/sections/english/areas-section.md`; `areas: []` shows every area, or list ids to show some.

### The Area Finder
The finder is the box where a visitor types a ZIP code or a place name. It matches:

- **An exact ZIP code** from `zips`: it goes straight to that area.
- **The start of a ZIP code** (3 digits or more): it suggests the areas with matching ZIP codes.
- **A name**: the area `title`, its `neighborhoods`, and its `aliases`. Case, accents, hyphens, and apostrophes are ignored, so "saint eloi" finds "Saint-Éloi". Names that start with the text come first.

It shows up to 6 suggestions. Every area is built into the page, so with more than 150 areas the build prints an `[areaFinder]` warning; use the `list` layout then.

### Menus And Footer
You don't add areas to the menus by hand. In `src/config/menu.en.json` (and `menu.fr.json`), the header item with `"source": "areas"` lists the published areas, and the footer column with `"source": "areas"` lists them before its own "All areas" link:

```json
{
  "enable": true,
  "name": "Areas",
  "url": "/areas/",
  "hasChildren": true,
  "source": "areas",
  "limit": 8
}
```

`limit` is the number of areas in the header menu (8 by default); the menu always ends with an "All areas" link. Above 6 areas the list splits into two columns. See [Menus](/homio/customization/menus/).

### Areas In Structured Data
Homio writes `areaServed` from your published area files only. It never invents a place.

- **Every page:** the business lists each area as an `AdministrativeArea` with its name and page URL.
- **Service pages:** the service lists the areas that offer it (areas whose `services` is empty or includes it).
- **Area pages:** a service named after your business and the area ("Your Company in Elm Park") with the area's `region` and postal codes, and the services offered there. With a real `office`, the page instead describes a branch of the business with that address, phone, and hours.

With no area files, `areaServed` is left out. See [SEO & AI Visibility](/homio/customization/seo-ai-visibility/).

### Add An Area
<Steps>

1. Copy an area file in `src/content/areas/english/`, for example `elm-park.md`, and rename it. The file name is the URL: `riverside.md` is `/areas/riverside/`.
2. Change `title`, `description`, `region`, `code`, `order`, and the photo.
3. Set `zips` to the real postal codes, written exactly as people type them. Add `neighborhoods` and `aliases`.
4. Set or clear the local `phone`, `hours`, `crews`, and `since`. Keep `office: {}` unless you have a real office there.
5. Write the `notes`, `faq`, and the Markdown body about the area. Set `nearby` to the ids of neighbouring areas.
6. Copy the file to `src/content/areas/french/` with the **same file name** and translate the text. Keep `zips`, ids, and numbers the same.
7. Run `npm run dev`, open `/areas/`, and type one of the new ZIP codes in the finder.

</Steps>

The header menu, the footer column, the finder, the booking ZIP check, and the structured data pick up the new area on their own.

### Remove An Area
Delete the file in both `english/` and `french/`, or set `draft: true` in both to hide it. Then search your content for its id and update:

- `nearby` in the other area files,
- `area:` in reviews (`src/content/reviews/`) and projects (`src/content/projects/`). A review whose area id no longer exists shows the id as plain text.

Also check the copy that names your areas: the metaDescription and the questions in `src/content/areas/english/-index.md`, and the description in `src/content/sections/english/areas-section.md` ("Four areas, each with its own crew base").

### Do It With AI
```text
Replace the sample service areas with mine: [AREA NAME: ZIP CODES, NEIGHBOURHOODS, LOCAL PHONE
OR "use the main number"; ...]. Use src/content/areas/english/elm-park.md as the model, one file
per area named after the area, in order [ORDER]. Keep office: {} empty. Delete the sample areas
I don't use, and fix every `nearby`, review `area:`, and project `area:` that pointed at them.
Do the French files with the same file names, then update the area counts in the copy of
src/content/areas/english/-index.md and src/content/sections/english/areas-section.md.
```