docs/developer/storefront/rails/sections.mdx
Sections are the building blocks of all pages in Spree Storefront.
There are two types of sections:
<CardGroup cols={2}> <Card title="Layout sections"> * eg. Header, Footer * Present on all pages </Card> <Card title="Content sections"> * eg. Hero, Featured Products * Present on specific pages * can be managed via the Page Builder </Card> </CardGroup>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 |
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.
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)
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.
<Warning>Documentation on content sections is coming soon</Warning>
Let's dive into the details of how sections work.
Each section's model inherit from Spree::PageSection abstract model class.
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.
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.
This guide walks you through creating a custom section for your Spree storefront. We'll create a "Testimonials" section that displays customer reviews.
Create your section model that inherits from Spree::PageSection. Place it in app/models/spree/page_sections/:
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
Add your section to Spree.page_builder.page_sections in your Spree initializer:
Rails.application.config.after_initialize do
Spree.page_builder.page_sections += [
Spree::PageSections::Testimonials
]
end
Create the admin form partial for configuring the section in the Page Builder. Place it in app/views/spree/admin/page_sections/forms/:
<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 %>
Create the storefront partial that renders the section. Place it in your theme's views directory:
<% 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 %>
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
If your section needs an image, use the built-in asset attachment:
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:
<%= 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 %>
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
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
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 |
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"
Here's a complete example putting it all together - a "Brand Story" section:
<Steps> <Step title="Create the model"> ```ruby 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
```
<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 %>
```
<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 %>
```