docs: add page builder feature plan
Document the architecture and implementation phases for transforming static page templates into a database-driven, customizable page builder with visual editing capabilities. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c25953780a
commit
94f98b8be0
399
docs/plans/page-builder.md
Normal file
399
docs/plans/page-builder.md
Normal file
@ -0,0 +1,399 @@
|
||||
# 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
|
||||
defmodule SimpleshopTheme.Content.PageLayout do
|
||||
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
|
||||
|
||||
has_many :sections, SimpleshopTheme.Content.PageSection
|
||||
|
||||
timestamps()
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### PageSection Schema
|
||||
|
||||
```elixir
|
||||
defmodule SimpleshopTheme.Content.PageSection do
|
||||
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
|
||||
|
||||
belongs_to :page_layout, SimpleshopTheme.Content.PageLayout
|
||||
|
||||
timestamps()
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Section Types Registry
|
||||
|
||||
```elixir
|
||||
defmodule SimpleshopTheme.Content.SectionTypes do
|
||||
@sections %{
|
||||
"hero" => %{
|
||||
name: "Hero Banner",
|
||||
component: &SimpleshopThemeWeb.ShopComponents.hero_section/1,
|
||||
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",
|
||||
component: &SimpleshopThemeWeb.ShopComponents.featured_products_section/1,
|
||||
settings_schema: %{
|
||||
title: %{type: :string, default: "Featured products"},
|
||||
product_count: %{type: :integer, default: 8}
|
||||
},
|
||||
allowed_on: [:home]
|
||||
},
|
||||
"category_nav" => %{
|
||||
name: "Category Navigation",
|
||||
component: &SimpleshopThemeWeb.ShopComponents.category_nav/1,
|
||||
settings_schema: %{},
|
||||
allowed_on: [:home]
|
||||
},
|
||||
"image_text" => %{
|
||||
name: "Image + Text Block",
|
||||
component: &SimpleshopThemeWeb.ShopComponents.image_text_section/1,
|
||||
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",
|
||||
component: &SimpleshopThemeWeb.ShopComponents.content_body/1,
|
||||
settings_schema: %{
|
||||
image_url: %{type: :image},
|
||||
content: %{type: :rich_text}
|
||||
},
|
||||
allowed_on: [:about, :contact]
|
||||
},
|
||||
"reviews_section" => %{
|
||||
name: "Customer Reviews",
|
||||
component: &SimpleshopThemeWeb.ShopComponents.reviews_section/1,
|
||||
settings_schema: %{},
|
||||
allowed_on: [:pdp]
|
||||
},
|
||||
"related_products" => %{
|
||||
name: "Related Products",
|
||||
component: &SimpleshopThemeWeb.ShopComponents.related_products_section/1,
|
||||
settings_schema: %{},
|
||||
allowed_on: [:pdp]
|
||||
}
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
## Page Renderer
|
||||
|
||||
```elixir
|
||||
defmodule SimpleshopThemeWeb.PageRenderer do
|
||||
use Phoenix.Component
|
||||
import SimpleshopThemeWeb.ShopComponents
|
||||
|
||||
alias SimpleshopTheme.Content.SectionTypes
|
||||
|
||||
@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
|
||||
|
||||
- `lib/simpleshop_theme_web/components/shop_components.ex` - Existing section components
|
||||
- `lib/simpleshop_theme_web/components/page_templates/` - Current static templates (will become defaults)
|
||||
- `lib/simpleshop_theme_web/live/theme_live/index.ex` - Theme editor (reference implementation)
|
||||
Loading…
Reference in New Issue
Block a user