# Implementing Comprehensive SEO for Property Listings in PropertyWebBuilder

> Learn how to implement comprehensive SEO for property listings using PropertyWebBuilder's built-in tools for metadata, JSON-LD, validation, and auditing. Optimize your real estate listings today.

- Repository: [Ed Tee/property_web_builder](https://github.com/etewiah/property_web_builder)
- Tags: best-practices
- Published: 2026-03-01

---

**PropertyWebBuilder provides a full-stack SEO subsystem covering metadata generation, JSON-LD structured data, field validation, and an audit dashboard, enabling search-engine-optimized real estate listings without external gems.**

PropertyWebBuilder (PWB) is a Ruby on Rails engine that implements comprehensive SEO for property listings through a cohesive architecture of models, helpers, and admin interfaces. The system stores SEO attributes directly on content entities, validates them against search engine constraints, and renders complete meta tag suites including Open Graph, Twitter Cards, and schema.org JSON-LD.

## How SEO Data Is Stored in PropertyWebBuilder

The platform persists SEO fields at three levels: site-wide defaults, CMS pages, and individual properties.

**Website-level defaults** live in [`app/models/pwb/website.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/models/pwb/website.rb) (line 28), which exposes `default_seo_title` and `default_meta_description` columns on the `pwb_websites` table. These values serve as fallbacks when specific pages or properties lack custom SEO content.

**CMS pages** defined in [`app/models/pwb/page.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/models/pwb/page.rb) (line 52) store `seo_title`, `meta_description`, and `meta_keywords` directly on the `pwb_pages` table. Administrators edit these through the Site Admin interface at `/site_admin/website/settings/seo`.

**Property listings** utilize a hybrid approach. The base `Pwb::Prop` model in [`app/models/pwb/prop.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/models/pwb/prop.rb) (line 71) provides a `seo_title` column for direct storage. Additionally, specific listing types—`Pwb::SaleListing` (line 64), `Pwb::RentalListing` (line 67), and `Pwb::SppListing`—leverage the **Mobility** gem to store translated SEO content in `seo_title` and `meta_description` fields per locale.

## Validating SEO Content with SeoValidatable

To enforce search engine best practices, PWB includes the `SeoValidatable` concern in [`app/models/concerns/seo_validatable.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/models/concerns/seo_validatable.rb). This module validates field lengths and presence for each locale, ensuring translated SEO titles and descriptions meet Google's character limit recommendations.

```ruby

# app/models/concerns/seo_validatable.rb

module SeoValidatable
  extend ActiveSupport::Concern

  included do
    validate :validate_seo_field_lengths
  end

  def validate_seo_title_for_locale(locale)
    field_name = "seo_title_#{locale}"
    # Enforces length constraints per translation

  end
end

```

The concern is mixed into any model declaring translatable SEO attributes, automatically running checks at save time (lines 17–30).

## Rendering Meta Tags and Structured Data

All meta tag generation logic resides in [`app/helpers/seo_helper.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/helpers/seo_helper.rb). This helper provides a fluent API for setting and rendering SEO data across the application.

### Core Helper Methods

- **`set_seo`** (lines 14–17): Stores an options hash for the current request in `@seo_data`, accepting `:title`, `:description`, `:canonical_url`, `:og_type`, and `:image`.
- **`seo_title`** (lines 24–40): Resolves the page title using a precedence chain: explicit SEO title → entity title → website default.
- **`seo_meta_tags`** (lines 107–156): Assembles the complete `<head>` output including favicons, verification meta tags, Open Graph (`og:*`) properties, Twitter Card tags, robots directives, and `alternate` hreflang links for multilingual sites.
- **`generate_alternate_urls`** (lines 74–93): Generates hreflang URLs for each available locale to signal language variants to search engines.

### JSON-LD Structured Data Generators

The helper includes generators for schema.org markup:

- **`property_json_ld`** (lines 59–145): Injects RealEstateListing structured data for rich snippets in Google search results.
- **`organization_json_ld`**: Provides Organization schema for the real estate agency.
- **`breadcrumb_json_ld`**: Enables breadcrumb rich results.

## Integrating SEO in Controllers

Controllers invoke helper methods to populate SEO data before rendering views.

For **CMS pages**, use `set_page_seo` (lines 98–114):

```ruby
def show
  @page = Pwb::Page.find_by!(slug: params[:slug])
  set_page_seo(@page)
  render :show
end

```

For **search and listing pages**, `set_listing_page_seo` (lines 19–47) constructs dynamic titles based on operation type, location, and pagination:

```ruby
def index
  set_listing_page_seo(
    operation: params[:type],
    location:  params[:city],
    page:      params[:page]
  )
end

```

For **individual properties**, controllers manually configure SEO:

```ruby

# app/controllers/properties_controller.rb

class PropertiesController < ApplicationController
  include SeoHelper

  def show
    @property = Pwb::Prop.find(params[:id])
    canonical = property_url(@property)

    set_seo(
      title:       @property.seo_title.presence || @property.title,
      description: @property.meta_description.presence,
      canonical_url: canonical,
      og_type:    'product',
      image:      @property.photos.first
    )
  end
end

```

## Monitoring SEO Health with the Audit Dashboard

The `SiteAdmin::SeoAuditController` ([`app/controllers/site_admin/seo_audit_controller.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/controllers/site_admin/seo_audit_controller.rb)) provides a comprehensive health overview at `/site_admin/seo_audit` (route defined in [`config/routes.rb`](https://github.com/etewiah/property_web_builder/blob/main/config/routes.rb), line 286).

The controller computes:

- **Property statistics**: Percentage of listings with populated SEO titles and descriptions (lines 32–55).
- **Page statistics**: Coverage metrics for CMS pages (lines 66–78).
- **Image statistics**: Count of property photos containing `alt` text (lines 87–107).
- **Overall score**: A weighted aggregate score and letter grade (lines 119–133).

Instance variables `@property_stats`, `@page_stats`, `@image_stats`, and `@overall_score` expose this data to the admin view, guiding content editors toward incomplete listings.

## Complete Implementation Workflow

### 1. Configure SEO in the Layout

Render the complete meta tag suite in your application layout:

```erb
<!-- app/views/layouts/application.html.erb -->
<head>
  <meta charset="utf-8">
  <%= seo_meta_tags %>
  <%= property_json_ld(@property) if defined?(@property) %>
  <%= organization_json_ld %>
</head>

```

### 2. Admin Form for SEO Fields

Allow editors to customize SEO content via standard Rails forms:

```erb
<!-- app/views/site_admin/pages/_form.html.erb -->
<div class="field">
  <%= f.label :seo_title, "SEO Title (optional)" %>
  <%= f.text_field :seo_title, class: "form-control", placeholder: "e.g. Luxury Villa in Barcelona" %>
</div>

<div class="field">
  <%= f.label :meta_description, "Meta Description (optional)" %>
  <%= f.text_area :meta_description, rows: 3, class: "form-control" %>
</div>

```

### 3. Programmatic SEO Health Checks

Create Rake tasks to audit SEO coverage across websites:

```ruby

# lib/tasks/seo.rake

namespace :seo do
  desc "Print SEO health summary for all websites"
  task summary: :environment do
    Pwb::Website.find_each do |website|
      puts "Website: #{website.subdomain}"
      puts "  Default title: #{website.default_seo_title}"
      puts "  Pages with SEO title: #{website.pages.where.not(seo_title: nil).count}"
      puts "  Props with SEO title: #{website.props.where.not(seo_title: nil).count}"
    end
  end
end

```

This leverages the same metrics used by the audit dashboard controller.

## Summary

- **PropertyWebBuilder** stores SEO data on `Pwb::Website`, `Pwb::Page`, and `Pwb::Prop` models, with listing types supporting translated fields via Mobility.
- The **SeoValidatable** concern enforces length constraints and locale-specific validation rules.
- **SeoHelper** centralizes meta tag generation, canonical URLs, hreflang links, and JSON-LD structured data in [`app/helpers/seo_helper.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/helpers/seo_helper.rb).
- Controllers use `set_seo`, `set_page_seo`, or `set_listing_page_seo` to populate per-request SEO data.
- The **SEO Audit Dashboard** at `/site_admin/seo_audit` tracks completion rates for titles, descriptions, and image alt text across all listings.

## Frequently Asked Questions

### How do I add custom SEO titles to property listings in PropertyWebBuilder?

Set the `seo_title` attribute on the `Pwb::Prop` model or its translated variants via the admin interface. The system falls back to the property's standard title if the SEO field is blank. According to [`app/models/pwb/prop.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/models/pwb/prop.rb) (line 71), this column stores the override value used by `SeoHelper#seo_title`.

### Where does PropertyWebBuilder store site-wide default SEO values?

Site defaults reside in the `pwb_websites` table, accessible through the `Pwb::Website` model in [`app/models/pwb/website.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/models/pwb/website.rb) (line 28). The `default_seo_title` and `default_meta_description` columns provide fallback content for pages and properties that lack specific SEO data, configurable via `/site_admin/website/settings/seo`.

### Does PropertyWebBuilder support structured data for Google rich snippets?

Yes. The `SeoHelper` module in [`app/helpers/seo_helper.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/helpers/seo_helper.rb) includes `property_json_ld` (lines 59–145) which generates schema.org RealEstateListing JSON-LD. When rendered in the layout alongside `seo_meta_tags`, this enables rich results including property images, prices, and locations in search engine results.

### How can I validate SEO field lengths across multiple languages?

Include the `SeoValidatable` concern in models with translated SEO attributes. Defined in [`app/models/concerns/seo_validatable.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/models/concerns/seo_validatable.rb) (lines 17–30), this module validates character limits for each locale separately, ensuring that `seo_title_en`, `seo_title_es`, and other translations comply with search engine display limits before saving.