Implementing Comprehensive SEO for Property Listings in PropertyWebBuilder

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 (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 (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 (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. This module validates field lengths and presence for each locale, ensuring translated SEO titles and descriptions meet Google's character limit recommendations.


# 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. 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):

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:

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

For individual properties, controllers manually configure SEO:


# 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) provides a comprehensive health overview at /site_admin/seo_audit (route defined in 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:

<!-- 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:

<!-- 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:


# 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.
  • 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 (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 (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 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 (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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →