Quick Start
Sections are the building blocks of all pages in Spree Storefront.
There are two types of sections:
Layout sections
- eg. Header, Footer
- Present on all pages
Content sections
- eg. Hero, Featured Products
- Present on specific pages
- can be managed via the Page Builder
Let’s take a look at the page structure of Spree Storefront:
As you can see, the page is divided into sections. Each section is a component that is used to create the page.
Most sections are containers that consist of multiple blocks which you can manage via the Page Builder (add/customize/remove/change their order).
Each section consists of:
| File | Description | Example |
|---|
| ActiveRecord model | Defines the section’s preferences | Spree::PageSections::ImageWithText |
| Storefront view | Renders the section in the storefront - each theme can have its own view | image_with_text.html.erb |
| Admin page builder form | Configures the section in the admin panel | image_with_text/_form.html.erb |
Layout Sections
We have two types of layout sections - Header and Footer. As the name suggests, they are rendered at the top and bottom of the page respectively. Between them, we have a main content area that is used to render the page sections.
Announcement Bar
A simple announcement bar section that displays a message to the users.
Header is one of the most important sections in the storefront. It is used to display the store’s logo, navigation menu, search bar, cart icon, and user menu.
Header can have a simple one-level navigation menu or a more complex multi-level menu (aka mega menu)
Newsletter
A newsletter section that allows users to subscribe to the store’s newsletter. If you connected your store to a newsletter provider (eg. Klaviyo, Mailchimp, etc.), you can use this section to collect emails and send them to your provider (Only in Spree 5.1+).
Footer is a section that is used to display the store’s footer. It is typically used to display the store’s logo, copyright information, store policies, and other important links.
Content Sections
Documentation on content sections is coming soon
Architecture
Let’s dive into the details of how sections work.
Active Record Model
Each section’s model inherit from Spree::PageSection abstract model class.
Associations
Each sections has many blocks and links. You can call them via section.blocks and section.links respectively.
Section belongs to a polymorphic parent model called pageable which can be either Spree::Page (content sections) or Spree::Theme (layout sections).
You can access section’s theme by calling section.theme.
Preferences
Each section has set of default preferences
| Name | Description | Default Value |
|---|
text_color | Color of text in the section | nil - uses theme’s text color |
background_color | Background color of the section | nil - uses theme’s background color |
border_color | Color of section borders | nil - uses theme’s border color |
top_padding | Padding space above section content (in pixels) | 40 |
bottom_padding | Padding space below section content (in pixels) | 40 |
top_border_width | Width of top border (in pixels) | 1 |
bottom_border_width | Width of bottom border (in pixels) | 0 |
Particular sections can introduce their own preferences. For example, Spree::PageSections::ImageWithText has desktop_image_alignment and vertical_alignment preferences.
Creating a New Section
This guide walks you through creating a custom section for your Spree storefront. We’ll create a “Testimonials” section that displays customer reviews.
Step 1: Create the Model
Create your section model that inherits from Spree::PageSection. Place it in app/models/spree/page_sections/:
app/models/spree/page_sections/testimonials.rb
module Spree
module PageSections
class Testimonials < Spree::PageSection
# Override default padding if needed
TOP_PADDING_DEFAULT = 60
BOTTOM_PADDING_DEFAULT = 60
# Define section-specific preferences
preference :heading, :string, default: 'What Our Customers Say'
preference :heading_size, :string, default: 'large'
preference :heading_alignment, :string, default: 'center'
preference :max_testimonials, :integer, default: 3
preference :show_rating, :boolean, default: true
# Validation for preferences
before_validation :ensure_valid_heading_size
before_validation :ensure_valid_alignment
# Icon displayed in the Page Builder sidebar
def icon_name
'quote'
end
# Define default blocks that are created with this section
def default_blocks
@default_blocks.presence || [
Spree::PageBlocks::Heading.new(
text: preferred_heading,
preferred_text_alignment: preferred_heading_alignment
)
]
end
# Which block types can be added to this section
def available_blocks_to_add
[
Spree::PageBlocks::Heading,
Spree::PageBlocks::Text
]
end
# Enable block management in admin
def blocks_available?
true
end
# Allow reordering blocks
def can_sort_blocks?
true
end
private
def ensure_valid_heading_size
self.preferred_heading_size = 'medium' unless %w[small medium large].include?(preferred_heading_size)
end
def ensure_valid_alignment
self.preferred_heading_alignment = 'center' unless %w[left center right].include?(preferred_heading_alignment)
end
end
end
end
Step 2: Register the Section
Add your section to Spree.page_builder.page_sections in your Spree initializer:
config/initializers/spree.rb
Rails.application.config.after_initialize do
Spree.page_builder.page_sections += [
Spree::PageSections::Testimonials
]
end
Sections are filtered by their role method. By default, sections have role 'content' which makes them available in the Page Builder. Sections with role 'header' or 'footer' are layout sections.
Create the admin form partial for configuring the section in the Page Builder. Place it in app/views/spree/admin/page_sections/forms/:
app/views/spree/admin/page_sections/forms/_testimonials.html.erb
<div class="form-group">
<%= f.label :preferred_heading, Spree.t(:heading) %>
<%= f.text_field :preferred_heading,
class: 'form-control',
data: { action: 'auto-submit#submit' } %>
</div>
<div class="form-group">
<%= f.label :preferred_heading_size, Spree.t(:heading_size) %>
<%= f.select :preferred_heading_size,
options_for_select([
[Spree.t(:small), 'small'],
[Spree.t(:medium), 'medium'],
[Spree.t(:large), 'large']
], @page_section.preferred_heading_size),
{},
{ class: 'custom-select', data: { action: 'auto-submit#submit' } } %>
</div>
<div class="form-group">
<%= f.label :preferred_heading_alignment, Spree.t(:alignment) %>
<%= f.select :preferred_heading_alignment,
options_for_select([
[Spree.t(:left), 'left'],
[Spree.t(:center), 'center'],
[Spree.t(:right), 'right']
], @page_section.preferred_heading_alignment),
{},
{ class: 'custom-select', data: { action: 'auto-submit#submit' } } %>
</div>
<div class="form-group">
<%= f.label :preferred_max_testimonials, Spree.t(:max_items) %>
<%= f.number_field :preferred_max_testimonials,
class: 'form-control',
min: 1,
max: 10,
data: { action: 'auto-submit#submit' } %>
</div>
<div class="custom-control custom-checkbox mb-3">
<%= f.check_box :preferred_show_rating,
class: 'custom-control-input',
data: { action: 'auto-submit#submit' } %>
<%= f.label :preferred_show_rating, Spree.t(:show_rating), class: 'custom-control-label' %>
</div>
<%# Add design settings to the Design tab %>
<% content_for(:design_tab) do %>
<hr />
<% end %>
The data: { action: 'auto-submit#submit' } attribute enables live preview in the Page Builder - changes are saved automatically as the user types.
Step 4: Create the Storefront View
Create the storefront partial that renders the section. Place it in your theme’s views directory:
app/views/themes/default/spree/page_sections/_testimonials.html.erb
<% cache_unless page_builder_enabled?, spree_storefront_base_cache_scope.call(section) do %>
<div style="<%= section_styles(section) %>" class="testimonials-section">
<div class="page-container">
<%# Heading %>
<% if section.preferred_heading.present? %>
<%
heading_class = case section.preferred_heading_size
when 'small' then 'text-lg'
when 'medium' then 'text-xl lg:text-2xl'
when 'large' then 'text-2xl lg:text-3xl'
end
%>
<h2 class="<%= heading_class %> font-medium mb-8 text-<%= section.preferred_heading_alignment %>">
<%= section.preferred_heading %>
</h2>
<% end %>
<%# Render blocks %>
<% section.blocks.includes(:rich_text_text).each do |block| %>
<% case block.type %>
<% when 'Spree::PageBlocks::Heading' %>
<h3 class="text-xl font-medium" <%= block_attributes(block) %>>
<%= block.text %>
</h3>
<% when 'Spree::PageBlocks::Text' %>
<div class="prose" <%= block_attributes(block) %>>
<%= block.text %>
</div>
<% end %>
<% end %>
<%# Your custom testimonials content %>
<div class="grid grid-cols-1 md:grid-cols-<%= section.preferred_max_testimonials %> gap-6">
<%# Add your testimonials rendering logic here %>
</div>
</div>
</div>
<% end %>
Section Roles
Sections have different roles that determine where they appear:
| Role | Description | Example |
|---|
content | Available in Page Builder, can be added to any page | Rich Text, Image Banner |
system | Core page functionality, usually not removable | Product Details, Cart |
header | Header layout sections | Header, Announcement Bar |
footer | Footer layout sections | Footer, Newsletter |
To set a section’s role, override the self.role class method:
def self.role
'content' # default
end
Section with Image Upload
If your section needs an image, use the built-in asset attachment:
app/models/spree/page_sections/hero_banner.rb
module Spree
module PageSections
class HeroBanner < Spree::PageSection
include Spree::HasImageAltText
preference :image_alt, :string
def icon_name
'photo'
end
end
end
end
Admin form with image upload:
app/views/spree/admin/page_sections/forms/_hero_banner.html.erb
<%= render 'spree/admin/shared/page_section_image', f: f %>
<div class="form-group">
<%= f.label :preferred_image_alt, Spree.t(:alt_text) %>
<%= f.text_field :preferred_image_alt,
class: 'form-control',
data: { action: 'auto-submit#submit' } %>
</div>
Display in storefront:
<% if section.asset.attached? %>
<%= spree_image_tag section.asset,
width: 1200,
height: 600,
alt: section.image_alt,
class: 'w-full object-cover' %>
<% end %>
Section with Links
To add clickable links to your section, include the Spree::HasPageLinks concern (included by default) and define links:
module Spree
module PageSections
class PromoBanner < Spree::PageSection
has_one :link, ->(ps) { ps.links },
class_name: 'Spree::PageLink',
as: :parent,
dependent: :destroy,
inverse_of: :parent
accepts_nested_attributes_for :link
def default_links
@default_links.presence || [
Spree::PageLink.new(label: Spree.t(:shop_now))
]
end
end
end
end
Lazy Loading Sections
For sections that load external data (products, taxons), implement lazy loading to improve page performance:
def lazy?
!Rails.env.test?
end
def lazy_path(variables)
url_options = variables[:url_options] || {}
Spree::Core::Engine.routes.url_helpers.page_section_path(self, **url_options)
end
Helper Methods
These helper methods are available in storefront section views:
| Helper | Description |
|---|
section_styles(section) | Returns inline CSS for section padding, colors, borders |
block_attributes(block) | Returns data attributes for Page Builder editing |
page_builder_enabled? | Returns true when in Page Builder preview mode |
spree_storefront_base_cache_scope | Cache key scope for the section |
section_heading_styles(section) | Returns inline CSS for heading text color |
page_builder_link_to(link, **options) | Renders a link with Page Builder support |
Adding Translations
Add translations for your section in your locale files:
en:
spree:
page_sections:
testimonials:
heading_default: "What Our Customers Say"
text_default: "Read reviews from our happy customers"
Complete Example
Here’s a complete example putting it all together - a “Brand Story” section:
Create the model
app/models/spree/page_sections/brand_story.rb
module Spree
module PageSections
class BrandStory < Spree::PageSection
include Spree::HasImageAltText
TOP_PADDING_DEFAULT = 80
BOTTOM_PADDING_DEFAULT = 80
preference :image_position, :string, default: 'left'
preference :image_alt, :string
def icon_name
'building-store'
end
def default_blocks
[
Spree::PageBlocks::Heading.new(text: 'Our Story'),
Spree::PageBlocks::Text.new(text: 'Share your brand story here...')
]
end
def available_blocks_to_add
[Spree::PageBlocks::Heading, Spree::PageBlocks::Text, Spree::PageBlocks::Buttons]
end
def blocks_available?
true
end
def can_sort_blocks?
true
end
end
end
end
Register the section
config/initializers/spree.rb
Rails.application.config.after_initialize do
Spree.page_builder.page_sections += [Spree::PageSections::BrandStory]
end
Create admin form
app/views/spree/admin/page_sections/forms/_brand_story.html.erb
<%= render 'spree/admin/shared/page_section_image', f: f %>
<div class="form-group">
<%= f.label :preferred_image_alt, Spree.t(:alt_text) %>
<%= f.text_field :preferred_image_alt,
class: 'form-control',
data: { action: 'auto-submit#submit' } %>
</div>
<% content_for(:design_tab) do %>
<div class="form-group">
<%= f.label :preferred_image_position, Spree.t(:image_position) %>
<%= f.select :preferred_image_position,
options_for_select([['Left', 'left'], ['Right', 'right']], @page_section.preferred_image_position),
{},
{ class: 'custom-select', data: { action: 'auto-submit#submit' } } %>
</div>
<hr />
<% end %>
Create storefront view
app/views/themes/default/spree/page_sections/_brand_story.html.erb
<% cache_unless page_builder_enabled?, spree_storefront_base_cache_scope.call(section) do %>
<div style="<%= section_styles(section) %>" class="brand-story">
<div class="page-container">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">
<% image_order = section.preferred_image_position == 'right' ? 'lg:order-2' : '' %>
<div class="<%= image_order %>">
<% if section.asset.attached? %>
<%= spree_image_tag section.asset,
width: 600,
height: 400,
alt: section.image_alt,
class: 'w-full rounded-lg' %>
<% end %>
</div>
<div>
<% section.blocks.each do |block| %>
<% case block.type %>
<% when 'Spree::PageBlocks::Heading' %>
<h2 class="text-2xl lg:text-3xl font-medium mb-4" <%= block_attributes(block) %>>
<%= block.text %>
</h2>
<% when 'Spree::PageBlocks::Text' %>
<div class="prose" <%= block_attributes(block) %>>
<%= block.text %>
</div>
<% when 'Spree::PageBlocks::Buttons' %>
<div class="mt-6" <%= block_attributes(block) %>>
<% if block.link %>
<%= page_builder_link_to block.link,
class: 'btn-primary',
target: (block.link.open_in_new_tab ? '_blank' : nil) %>
<% end %>
</div>
<% end %>
<% end %>
</div>
</div>
</div>
</div>
<% end %>