Skip to main content
Links are used to create navigation throughout the storefront. They can be assigned to sections and blocks, enabling users to navigate to different pages, products, categories, or external URLs. The Spree::PageLink model provides a flexible way to link to various content types:
  • Internal pages - Link to Pages, Products, Taxons, Posts
  • External URLs - Link to any external website
  • Special links - Email (mailto:) and phone (tel:) links
Links are managed through the Page Builder interface, where store staff can select what each link points to. The Spree::PageLink model has these key attributes:
AttributeTypeDescription
labelStringDisplay text for the link
urlStringCustom URL (for external links)
linkablePolymorphicReference to internal content (Page, Product, etc.)
parentPolymorphicSection or Block the link belongs to
open_in_new_tabBooleanWhether to open in new browser tab
positionIntegerOrder when multiple links exist

Linkable Types

Links can point to various Spree models:
Linkable TypeDescription
Spree::PageInternal pages (Home, Shop All, Custom pages)
Spree::ProductProduct detail pages
Spree::TaxonCategory/collection pages
Spree::PostBlog posts
Custom URLAny external or internal URL
For sections that need one link (like a banner):
app/models/spree/page_sections/promo_banner.rb
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 need multiple links (like navigation):
app/models/spree/page_sections/footer.rb
module Spree
  module PageSections
    class Footer < Spree::PageSection
      # Links are provided by Spree::HasPageLinks concern
      # which is included in Spree::PageSection by default

      def default_links
        @default_links.presence || [
          Spree::PageLink.new(label: 'About Us'),
          Spree::PageLink.new(label: 'Contact'),
          Spree::PageLink.new(label: 'Privacy Policy')
        ]
      end
    end
  end
end
Blocks can also have links. Use the Spree::HasOneLink concern for single links:
app/models/spree/page_blocks/cta_button.rb
module Spree
  module PageBlocks
    class CtaButton < Spree::PageBlock
      include Spree::HasOneLink

      preference :button_style, :string, default: 'primary'

      # Called when the link is deleted
      def link_destroyed(_link)
        destroy if page_links_count.zero?
      end
    end
  end
end
# Single link on a section
section.link
section.link.label
section.link.linkable_url

# Multiple links on a section
section.links
section.links.each do |link|
  link.label
  link.linkable_url
end

# Link on a block
block.link
block.link.label if block.link.present?

Using the Helper

The page_builder_link_to helper renders links with Page Builder support:
<%# Basic usage %>
<%= page_builder_link_to section.link %>

<%# With custom label %>
<%= page_builder_link_to section.link, label: 'Click Here' %>

<%# With CSS class %>
<%= page_builder_link_to section.link, class: 'btn-primary' %>

<%# Open in new tab %>
<%= page_builder_link_to section.link,
    target: (section.link.open_in_new_tab ? '_blank' : nil),
    rel: (section.link.open_in_new_tab ? 'noopener noreferrer' : nil) %>

<%# With block content %>
<%= page_builder_link_to section.link do %>
  <span class="icon"></span>
  <%= section.link.label %>
<% end %>
For more control, you can render links manually:
<% if section.link.present? %>
  <%= link_to section.link.linkable_url,
      target: (section.link.open_in_new_tab ? '_blank' : nil),
      rel: (section.link.open_in_new_tab ? 'noopener noreferrer' : nil),
      class: 'btn-primary' do %>
    <%= section.link.label %>
  <% end %>
<% end %>
<nav class="footer-links">
  <% section.links.each do |link| %>
    <%= page_builder_link_to link, class: 'footer-link' %>
  <% end %>
</nav>
In your section’s admin form, render the link editor:
app/views/spree/admin/page_sections/forms/_promo_banner.html.erb
<%= render 'spree/admin/shared/page_section_image', f: f %>

<div class="py-2">
  <%= f.fields_for :link do |lf| %>
    <div class="form-group">
      <%= lf.label :linkable_type, Spree.t(:link) %>
      <%= lf.select :linkable_type,
          @page_section.allowed_linkable_types,
          { include_blank: false },
          { class: 'custom-select mb-3', data: { action: 'auto-submit#submit' } } %>

      <div id="linkable_type_dropdown">
        <%= render 'spree/admin/page_links/linkable_type_dropdown',
            page_link: lf.object,
            form_name: 'page_section[link_attributes]' %>
      </div>
    </div>

    <div class="custom-control custom-checkbox">
      <%= lf.check_box :open_in_new_tab,
          class: 'custom-control-input',
          data: { action: 'auto-submit#submit' } %>
      <%= lf.label :open_in_new_tab, class: 'custom-control-label' %>
    </div>
  <% end %>
</div>
The linkable_url method returns the appropriate URL:
link = Spree::PageLink.new(linkable: product)
link.linkable_url
# => "/products/red-shirt"

link = Spree::PageLink.new(url: "https://example.com")
link.linkable_url
# => "https://example.com"

link = Spree::PageLink.new(url: "example.com")
link.formatted_url
# => "http://example.com"  # Adds protocol automatically

link = Spree::PageLink.new(url: "mailto:[email protected]")
link.formatted_url
# => "mailto:[email protected]"  # Preserves mailto: protocol
# Link to an internal page
link = Spree::PageLink.create!(
  parent: section,
  label: 'Shop Now',
  linkable: store.pages.find_by(type: 'Spree::Pages::ShopAll')
)

# Link to a product
link = Spree::PageLink.create!(
  parent: section,
  label: product.name,
  linkable: product
)

# Link to a taxon
link = Spree::PageLink.create!(
  parent: section,
  label: 'New Arrivals',
  linkable: store.taxons.find_by(name: 'New Arrivals')
)

# External link
link = Spree::PageLink.create!(
  parent: section,
  label: 'Visit Our Blog',
  url: 'https://blog.example.com',
  open_in_new_tab: true
)

Automatic Label Setting

When a link’s linkable is set, the label is automatically populated from the linked resource:
link = Spree::PageLink.new(linkable: product)
link.valid?
link.label
# => "Red T-Shirt" (from product.name)
The label is derived from (in order):
  1. linkable.title
  2. linkable.display_name
  3. linkable.name
  • Sections - Adding links to sections
  • Blocks - Adding links to blocks
  • Pages - Understanding internal page linking