# Adding Custom Page Parts and Content Blocks in PropertyWebBuilder

> Learn to add custom page parts and content blocks in PropertyWebBuilder. Combine Liquid templates with JSON content blocks for theme-specific components and locale-aware data fields.

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

---

**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`](https://github.com/etewiah/property_web_builder/blob/main/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`](https://github.com/etewiah/property_web_builder/blob/main/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`](https://github.com/etewiah/property_web_builder/blob/main/app/lib/pwb/page_part_library.rb) and [`app/lib/pwb/page_part_definition.rb`](https://github.com/etewiah/property_web_builder/blob/main/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:

```ruby

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

```liquid
{# 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:

```yaml

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

```bash
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:

```ruby

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

```liquid
{% page_part "my_theme/custom_hero" %}

```

The `PagePartTag` implementation in [`app/lib/pwb/liquid_tags/page_part_tag.rb`](https://github.com/etewiah/property_web_builder/blob/main/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.rb`](https://github.com/etewiah/property_web_builder/blob/main/site_admin/page_parts_controller.rb) provides CRUD operations, while Lookbook preview components in `spec/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_part` tag, which automatically handles locale resolution and HTML caching.

## Summary

- **Register** new components in `PagePartLibrary::DEFINITIONS` with category, label, and field specifications
- **Template** files live in `app/themes/<theme>/page_parts/` with fallback to `app/views/pwb/page_parts/`
- **Seed** database records via YAML files in `db/yml_seeds/page_parts/` using `PagePart.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`](https://github.com/etewiah/property_web_builder/blob/main/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.