simpleshop_theme/docs/plans/page-builder.md
Jamey Greenwood 94f98b8be0 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>
2026-01-17 22:19:09 +00:00

16 KiB

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

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

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

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

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
  • 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)