Service Areas
Geographic service areas for curated listing collections and neighborhood pages
Service Areas
Service areas allow agents to define curated geographic regions they specialize in, creating dedicated pages for each neighborhood or community.
Overview
Service areas power:
- Neighborhood pages at
/areas/[slug]on the client site - Listing counts and price statistics per area
- Import task creation for syncing listings in specific regions
- Homepage sections showcasing featured areas
Data Model
Service areas are stored in the workspace settings JSON, not as separate database rows.
ServiceArea Type
interface ServiceArea {
id: string; // UUID
name: string; // "Coconut Grove"
slug: string; // "coconut-grove"
tagline?: string; // "Miami's Bohemian Village"
description?: string; // Rich text for SEO/area page
heroImage?: string; // Area hero image URL
displayOrder: number; // For sorting
filter: ServiceAreaFilter;
}ServiceAreaFilter Type
Filters determine which listings appear in a service area:
interface ServiceAreaFilter {
cities?: string[]; // Normalized city slugs
subdivisions?: string[]; // Subdivision slugs
postalCodes?: string[]; // ZIP codes
buildingNames?: string[]; // Normalized building names
priceMin?: number;
priceMax?: number;
propertyTypes?: string[];
}Storage Location
Service areas are stored in workspaces.settings.serviceAreas:
{
"settings": {
"serviceAreas": [
{
"id": "abc123",
"name": "Coconut Grove",
"slug": "coconut-grove",
"tagline": "Miami's Bohemian Village",
"displayOrder": 1,
"filter": {
"cities": ["miami"],
"subdivisions": ["coconut-grove"]
}
}
]
}
}Typesense Integration
Service areas use normalized fields in Typesense for filtering:
| Field | Purpose |
|---|---|
city_normalized | Lowercase, slugified city name |
subdivision_slug | URL-safe subdivision identifier |
building_name_normalized | Lowercase building name |
Filter String Building
The buildServiceAreaFilter() function converts a ServiceArea to a Typesense filter:
function buildServiceAreaFilter(
area: ServiceArea,
workspaceId: string
): string {
const filters: string[] = [
`workspace_id:=${workspaceId}`,
`is_active:=true`
];
if (area.filter.cities?.length) {
filters.push(`city_normalized:=[${area.filter.cities}]`);
}
if (area.filter.subdivisions?.length) {
filters.push(`subdivision_slug:=[${area.filter.subdivisions}]`);
}
// ... other filter fields
return filters.join(' && ');
}API Endpoints
Dashboard API (Authenticated)
GET /api/dashboard/service-areas
List all service areas for the authenticated agent's workspace.
Response:
{
"serviceAreas": [
{
"id": "abc123",
"name": "Coconut Grove",
"slug": "coconut-grove",
"listingCount": 42,
"priceFloor": 750000
}
]
}POST /api/dashboard/service-areas
Create a new service area.
Request body:
{
"name": "Brickell",
"tagline": "Miami's Financial District",
"filter": {
"cities": ["miami"],
"subdivisions": ["brickell"]
}
}PUT /api/dashboard/service-areas/[id]
Update an existing service area.
DELETE /api/dashboard/service-areas/[id]
Remove a service area.
GET /api/dashboard/service-areas/import-status
Check which service areas have associated import tasks.
Public API
GET /api/public/areas
List service areas for the current workspace (resolved from host).
Response:
{
"areas": [
{
"id": "abc123",
"name": "Coconut Grove",
"slug": "coconut-grove",
"tagline": "Miami's Bohemian Village",
"heroImage": "https://...",
"listingCount": 42,
"priceFloor": 750000
}
]
}GET /api/public/areas/[slug]
Get details for a specific area including stats.
Getting Stats
The getServiceAreaStats() function fetches counts and price floors:
async function getServiceAreaStats(
workspaceId: string,
areas: ServiceArea[]
): Promise<Record<string, { count: number; priceFloor?: number }>>This queries Typesense for each area in parallel, returning:
count: Number of active listings matching the filterpriceFloor: Lowest list price in the area
Creating Import Tasks from Areas
Service areas can be used to automatically create import tasks:
POST /api/dashboard/imports/from-service-areas
Request body:
{
"serviceAreaIds": ["abc123", "def456"]
}This creates import tasks with:
category: 'search'- Filters matching the service area configuration
- Automatic naming:
Service Area: {area.name}
Client Site Integration
Areas Index Page
The /areas page displays a grid of all service areas with:
- Hero image (or gradient fallback)
- Area name and tagline
- Listing count
- Starting price
Area Detail Page
The /areas/[slug] page shows:
- Hero section with area image
- Area description (SEO-friendly)
- Stats (listing count, price floor)
- Filtered listing grid with pagination
- Contact CTA
Homepage Section
The ServiceAreasSection component displays featured areas on the homepage, linking to individual area pages.
Normalization
For consistent filtering, location values are normalized before storage:
// City normalization: "Miami Beach" → "miami-beach"
function normalizeCity(city: string): string {
return city.toLowerCase().replace(/\s+/g, '-');
}
// Subdivision normalization: "Coconut Grove" → "coconut-grove"
function normalizeSubdivision(subdivision: string): string {
return subdivision.toLowerCase().replace(/\s+/g, '-');
}Listings are indexed with these normalized values in Typesense for matching.