Adding Custom Page Parts and Content Blocks in PropertyWebBuilder
PropertyWebBuilder stores reusable UI fragments as database-backed Page Parts that combine Liquid templates with JSON-encoded content blocks, allowing developers to create theme-specific components with locale-aware data fields.
PropertyWebBuilder is a Rails-based CMS for real estate websites that uses a modular architecture to manage content. Adding custom page parts and content blocks enables you to extend themes with reusable components that content editors can customize per locale. This guide walks through the complete implementation based on the source code in the etewiah/property_web_builder repository.
Core Architecture
The system revolves around three primary components that handle persistence, orchestration, and metadata registration.
The PagePart Model
The PagePart model in app/models/pwb/page_part.rb is the ActiveRecord entity that persists the raw template and structured content. It stores editor configuration and JSON-encoded block contents in the database, while implementing a cascading template lookup (lines 65-91). When rendering, the template_content method first checks for a theme-specific file in app/themes/<theme_name>/page_parts/, then falls back to app/views/pwb/page_parts/<key>.liquid.
The PagePartManager Service
Located in app/services/pwb/page_part_manager.rb, this service bridges containers (Websites or Pages) with the Content join model. It handles creation of the page_contents association, seeds initial block data via seed_container_block_content (lines 81-119), and rebuilds final HTML through rebuild_page_content (lines 87-100). The manager ensures tenant isolation by adding website_id to the join model in find_or_create_join_model (lines 43-50), while the PagePart model itself remains tenant-agnostic to support cross-tenant operations.
The PagePartLibrary Registry
app/lib/pwb/page_part_library.rb and app/lib/pwb/page_part_definition.rb define a central registry that describes every available part. This drives the admin UI, validation rules, and the seed-generation process by mapping keys to metadata including category, label, and editable fields.
Step-by-Step Implementation
Creating a custom page part requires progressing through definition, templating, seeding, and population phases.
1. Register the Definition
Add your component to the central registry to make it available in the admin UI:
# app/lib/pwb/page_part_library.rb
DEFINITIONS = {
# existing entries …
'my_theme/custom_hero' => {
category: :heroes,
label: 'Custom Hero',
description: 'Theme‑specific hero variant',
fields: %w[title subtitle background_image]
}
}
This entry defines the admin organization (category), display label, and the editable fields that will appear in the content editor.
2. Create the Liquid Template
Place your markup in the theme directory:
{# app/themes/my_theme/page_parts/custom_hero.liquid #}
<div class="hero" style="background-image:url({{ page_part["background_image"]["content"] }})">
<h1 class="hero-title">{{ page_part["title"]["content"] }}</h1>
<p class="hero-subtitle">{{ page_part["subtitle"]["content"] }}</p>
</div>
The system automatically resolves this file because the filename matches the definition key. If the theme-specific file is missing, it falls back to the default view path.
3. Seed the Database Record
Create a YAML seed file to initialize the database structure:
# db/yml_seeds/page_parts/custom_hero.yml
- page_part_key: "my_theme/custom_hero"
page_slug: "home"
theme_name: "my_theme"
editor_setup:
editorBlocks:
- - label: title
isImage: false
- - label: subtitle
isImage: false
- - label: background_image
isImage: true
block_contents: {}
show_in_editor: true
Execute the seeding task to persist the record:
bundle exec rake pwb:seed_page_parts
Internally, this calls Pwb::PagePart.create_from_seed_yml (lines 55-61 of the model), which creates the database record only if it does not already exist.
4. Populate Content Blocks
Use the service object to fill locale-specific data:
# In a custom Rake task or provisioning script
website = Pwb::Website.find_by(domain: 'example.com')
manager = Pwb::PagePartManager.new('my_theme/custom_hero', website)
# Seed English content
manager.seed_container_block_content('en', {
'title' => 'Welcome to Our Studio',
'subtitle' => 'Designing your future',
'background_image' => 'hero-bg.jpg'
})
The seed_container_block_content method parses the editor configuration, resolves images (uploading local files or passing through remote URLs), and writes the resulting JSON into the block_contents column. It then triggers rebuild_page_content to generate the final HTML and store it in the associated PageContent join model.
5. Render in Frontend Templates
Insert the component using the Liquid tag:
{% page_part "my_theme/custom_hero" %}
The PagePartTag implementation in app/lib/pwb/liquid_tags/page_part_tag.rb retrieves the correct PagePart for the current website, merges the locale's block contents into the template context, and injects the rendered HTML into the page.
Admin and Frontend Integration
Custom page parts surface in two contexts:
- Admin Interface: The
site_admin/page_parts_controller.rbprovides CRUD operations, while Lookbook preview components inspec/components/previews/page_parts/render visual examples based on the library definitions. - Frontend Rendering: Any Liquid template—whether a page layout or another page part—can invoke your component using the
page_parttag, which automatically handles locale resolution and HTML caching.
Summary
- Register new components in
PagePartLibrary::DEFINITIONSwith category, label, and field specifications - Template files live in
app/themes/<theme>/page_parts/with fallback toapp/views/pwb/page_parts/ - Seed database records via YAML files in
db/yml_seeds/page_parts/usingPagePart.create_from_seed_yml - Populate locale-specific data through
PagePartManager#seed_container_block_content, which handles image uploads and JSON serialization - Render components using the
{% page_part "key" %}Liquid tag, which merges block contents and caches the output
Frequently Asked Questions
How do I add a new editable field to an existing page part?
Update the fields array in PagePartLibrary::DEFINITIONS for your specific key, then modify the corresponding Liquid template to reference the new field via {{ page_part["new_field"]["content"] }}. You may need to re-run content seeding or manually update existing block_contents JSON to include the new key for each locale.
Can page parts be shared across multiple websites?
Yes. The PagePart model is tenant-agnostic and stores the base template and structure globally. However, the actual rendered content is tenant-scoped through the PageContent join model, which includes website_id. This allows the same part definition to hold different content values for different domains while sharing the underlying Liquid markup.
What is the difference between block_contents and the template file?
block_contents is a JSON column storing the actual data values (text, image URLs) for each locale, while the template file is the Liquid markup that determines how those values are rendered into HTML. The PagePartManager service merges these two during the rebuild_page_content phase to produce the final cached HTML stored in the join model.
How do I override a template for a specific theme without modifying the default?
Place your override file in app/themes/<theme_name>/page_parts/<key>.liquid. According to the template_content method in app/models/pwb/page_part.rb (lines 65-91), the system checks for theme-specific files first before falling back to the default location in app/views/pwb/page_parts/, allowing per-theme customization while maintaining a base implementation.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →