1 OAK MLS

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:

FieldPurpose
city_normalizedLowercase, slugified city name
subdivision_slugURL-safe subdivision identifier
building_name_normalizedLowercase 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 filter
  • priceFloor: 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.

On this page