2026-01-17 22:19:09 +00:00
# Page Builder Feature Plan
## Overview
Transform the static page templates into a database-driven, customizable page builder. Users will be able to add, remove, reorder, and configure page sections through a visual editor similar to the existing theme editor.
## Goals
1. Allow customization of page layouts without code changes
2. Provide real-time preview while editing
3. Support responsive editing (works on mobile and desktop)
4. Maintain theme consistency with existing design system
## Architecture
### Current State
```
PageTemplates/
├── home.html.heex # Static: hero → category_nav → featured_products → image_text
├── collection.html.heex
├── pdp.html.heex
├── cart.html.heex
├── about.html.heex
├── contact.html.heex
└── error.html.heex
```
### Future State
```
Database (page_layouts table)
├── page: "home"
│ └── sections: [
│ {type: "hero", order: 1, settings: {...}},
│ {type: "category_nav", order: 2, settings: {...}},
│ {type: "featured_products", order: 3, settings: {...}},
│ {type: "image_text", order: 4, settings: {...}}
│ ]
├── page: "about"
│ └── sections: [...]
└── ...
PageRenderer module
├── render_page/2 # Renders page from layout + data
└── render_section/1 # Dispatches to component by type
```
## Data Model
### PageLayout Schema
```elixir
2026-02-18 21:23:15 +00:00
defmodule Berrypod.Content.PageLayout do
2026-01-17 22:19:09 +00:00
use Ecto.Schema
schema "page_layouts" do
field :page_type, :string # "home", "about", "collection", etc.
field :name, :string # Display name for the layout
field :is_default, :boolean, default: false
2026-02-18 21:23:15 +00:00
has_many :sections, Berrypod.Content.PageSection
2026-01-17 22:19:09 +00:00
timestamps()
end
end
```
### PageSection Schema
```elixir
2026-02-18 21:23:15 +00:00
defmodule Berrypod.Content.PageSection do
2026-01-17 22:19:09 +00:00
use Ecto.Schema
schema "page_sections" do
field :section_type, :string # "hero", "featured_products", etc.
field :order, :integer
field :settings, :map # JSON settings for the section
field :enabled, :boolean, default: true
2026-02-18 21:23:15 +00:00
belongs_to :page_layout, Berrypod.Content.PageLayout
2026-01-17 22:19:09 +00:00
timestamps()
end
end
```
### Section Types Registry
```elixir
2026-02-18 21:23:15 +00:00
defmodule Berrypod.Content.SectionTypes do
2026-01-17 22:19:09 +00:00
@sections %{
"hero" => %{
name: "Hero Banner",
2026-02-18 21:23:15 +00:00
component: & BerrypodWeb.ShopComponents.hero_section/1,
2026-01-17 22:19:09 +00:00
settings_schema: %{
title: %{type: :string, default: "Welcome"},
description: %{type: :string, default: ""},
cta_text: %{type: :string, default: "Shop now"},
cta_page: %{type: :string, default: "collection"},
background: %{type: :select, options: [:default, :sunken], default: :default}
},
allowed_on: [:home, :about, :contact, :error]
},
"featured_products" => %{
name: "Featured Products",
2026-02-18 21:23:15 +00:00
component: & BerrypodWeb.ShopComponents.featured_products_section/1,
2026-01-17 22:19:09 +00:00
settings_schema: %{
title: %{type: :string, default: "Featured products"},
product_count: %{type: :integer, default: 8}
},
allowed_on: [:home]
},
"category_nav" => %{
name: "Category Navigation",
2026-02-18 21:23:15 +00:00
component: & BerrypodWeb.ShopComponents.category_nav/1,
2026-01-17 22:19:09 +00:00
settings_schema: %{},
allowed_on: [:home]
},
"image_text" => %{
name: "Image + Text Block",
2026-02-18 21:23:15 +00:00
component: & BerrypodWeb.ShopComponents.image_text_section/1,
2026-01-17 22:19:09 +00:00
settings_schema: %{
title: %{type: :string},
description: %{type: :text},
image_url: %{type: :image},
link_text: %{type: :string},
link_page: %{type: :string}
},
allowed_on: [:home, :about]
},
"content_body" => %{
name: "Rich Text Content",
2026-02-18 21:23:15 +00:00
component: & BerrypodWeb.ShopComponents.content_body/1,
2026-01-17 22:19:09 +00:00
settings_schema: %{
image_url: %{type: :image},
content: %{type: :rich_text}
},
allowed_on: [:about, :contact]
},
"reviews_section" => %{
name: "Customer Reviews",
2026-02-18 21:23:15 +00:00
component: & BerrypodWeb.ShopComponents.reviews_section/1,
2026-01-17 22:19:09 +00:00
settings_schema: %{},
allowed_on: [:pdp]
},
"related_products" => %{
name: "Related Products",
2026-02-18 21:23:15 +00:00
component: & BerrypodWeb.ShopComponents.related_products_section/1,
2026-01-17 22:19:09 +00:00
settings_schema: %{},
allowed_on: [:pdp]
}
}
end
```
## Page Renderer
```elixir
2026-02-18 21:23:15 +00:00
defmodule BerrypodWeb.PageRenderer do
2026-01-17 22:19:09 +00:00
use Phoenix.Component
2026-02-18 21:23:15 +00:00
import BerrypodWeb.ShopComponents
2026-01-17 22:19:09 +00:00
2026-02-18 21:23:15 +00:00
alias Berrypod.Content.SectionTypes
2026-01-17 22:19:09 +00:00
@doc """
Renders a page from its layout and data context.
"""
def render_page(assigns) do
~H"""
< div class = "shop-container min-h-screen" style = "..." >
< .skip_link / >
< %= if @theme_settings .announcement_bar do %>
< .announcement_bar theme_settings = {@theme_settings} / >
< % end %>
< .shop_header { . . . } / >
< main id = "main-content" >
< %= for section < - @page_layout . sections do % >
< .render_section
section={section}
data={@data}
theme_settings={@theme_settings}
mode={@mode}
/>
< % end %>
< / main >
< .shop_footer theme_settings = {@theme_settings} mode = {@mode} / >
< .cart_drawer { . . . } / >
< .search_modal { . . . } / >
< / div >
"""
end
defp render_section(assigns) do
section_config = SectionTypes.get(assigns.section.section_type)
component = section_config.component
settings = Map.merge(section_config.default_settings, assigns.section.settings)
assigns = assign(assigns, :settings, settings)
~H"""
< %= if @section .enabled do %>
< %= component.(@settings |> Map.merge(%{mode: @mode , data: @data })) %>
< % end %>
"""
end
end
```
## Editor UI
### Layout Structure
```
┌─────────────────────────────────────────────────────────────┐
│ Desktop (≥1024px) │
├───────────────────┬─────────────────────────────────────────┤
│ │ │
│ Section List │ Live Preview │
│ ┌───────────┐ │ │
│ │ Hero │ │ ┌─────────────────────────────────┐ │
│ │ ≡ ✎ ✕ │ │ │ │ │
│ └───────────┘ │ │ [Hero Section Preview] │ │
│ ┌───────────┐ │ │ │ │
│ │ Products │ │ ├─────────────────────────────────┤ │
│ │ ≡ ✎ ✕ │ │ │ │ │
│ └───────────┘ │ │ [Products Section Preview] │ │
│ ┌───────────┐ │ │ │ │
│ │ Image+Text│ │ └─────────────────────────────────┘ │
│ │ ≡ ✎ ✕ │ │ │
│ └───────────┘ │ │
│ │ │
│ [+ Add Section] │ │
│ │ │
└───────────────────┴─────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Mobile (< 1024px ) │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Page: Home [Preview] ▼ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Sections: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ≡ Hero ✎ ✕ │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ≡ Featured Products ✎ ✕ │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ≡ Image + Text ✎ ✕ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ [+ Add Section] │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ [Preview Panel - expandable/collapsible] │
│ │
└─────────────────────────────────────────────────────────────┘
```
### Key Interactions
1. **Drag & Drop Reordering** : Sections can be reordered via drag handles (≡)
2. **Edit Settings** : Clicking ✎ opens settings panel for that section
3. **Remove Section** : Clicking ✕ removes section (with confirmation)
4. **Add Section** : Opens modal/drawer with available section types
5. **Live Preview** : Updates in real-time as changes are made
6. **Undo/Redo** : Track changes for undo capability (optional)
### Settings Panel
When editing a section, show a form based on its `settings_schema` :
```
┌─────────────────────────────────────────┐
│ Edit: Hero Banner ✕ │
├─────────────────────────────────────────┤
│ │
│ Title │
│ ┌─────────────────────────────────────┐ │
│ │ Original designs, printed on demand │ │
│ └─────────────────────────────────────┘ │
│ │
│ Description │
│ ┌─────────────────────────────────────┐ │
│ │ From art prints to apparel... │ │
│ └─────────────────────────────────────┘ │
│ │
│ Button Text │
│ ┌─────────────────────────────────────┐ │
│ │ Shop the collection │ │
│ └─────────────────────────────────────┘ │
│ │
│ Button Link │
│ ┌─────────────────────────────────────┐ │
│ │ collection ▼ │ │
│ └─────────────────────────────────────┘ │
│ │
│ Background Style │
│ ○ Default ● Sunken │
│ │
├─────────────────────────────────────────┤
│ [Cancel] [Save Changes] │
└─────────────────────────────────────────┘
```
## Implementation Phases
### Phase 1: Data Model & Migration
- Create `page_layouts` and `page_sections` tables
- Create Ecto schemas
- Create seed data from current static templates
- Add context functions (CRUD)
### Phase 2: Page Renderer
- Create `PageRenderer` module
- Create `SectionTypes` registry
- Update shop LiveViews to use renderer
- Update preview to use renderer
- Ensure backward compatibility (fallback to static if no layout)
### Phase 3: Editor UI - Read Only
- Create `PageEditorLive` LiveView
- Display current sections in list
- Show live preview
- Page selector dropdown
### Phase 4: Editor UI - Basic Editing
- Add/remove sections
- Drag & drop reordering (using sortable.js or similar)
- Save changes to database
### Phase 5: Section Settings
- Settings panel component
- Dynamic form generation from schema
- Real-time preview updates
### Phase 6: Polish & UX
- Undo/redo support
- Keyboard shortcuts
- Mobile-optimized editing
- Loading states
- Error handling
## Technical Considerations
### LiveView Communication
The editor needs to:
1. Load page layout on mount
2. Send updates on reorder/add/remove
3. Update preview in real-time
Using `phx-hook` for drag-and-drop and `push_event` for preview updates.
### Caching
Page layouts should be cached since they change infrequently:
- ETS cache similar to `CSSCache`
- Invalidate on save
### Migration Path
1. Deploy data model
2. Seed current layouts as "default" layouts
3. Deploy renderer (uses defaults if no custom layout)
4. Deploy editor
5. Users can customize
### Permissions
- Only authenticated admins can edit layouts
- Consider draft/published states for layouts
- Preview unpublished changes before going live
## Open Questions
1. Should users be able to create multiple layouts per page type (A/B testing)?
2. Should sections support nesting (e.g., columns within a section)?
3. How to handle page-specific data (e.g., PDP needs product, collection needs filter state)?
4. Should there be a "reset to default" option?
## Dependencies
- `sortable.js` or similar for drag-and-drop
- Possibly `@dnd-kit/sortable` if using JS framework
- JSON schema validation for settings
## Related Files
2026-02-18 21:23:15 +00:00
- `lib/berrypod_web/components/shop_components.ex` - Existing section components
- `lib/berrypod_web/components/page_templates/` - Current static templates (will become defaults)
- `lib/berrypod_web/live/theme_live/index.ex` - Theme editor (reference implementation)