Spina Blocks
Warning: This plugin is not production-ready and may cause issues. Use at your own risk.
A plugin for Spina CMS that adds reusable block components. Blocks are independent content units with their own templates and fields that can be assembled into pages.
Installation
Add to your Gemfile:
gem "spina-blocks"Run:
bundle install
rails generate spina:blocks:installThis copies the plugin's migrations into your app and runs db:migrate.
Upgrading
If you are upgrading from a version that used timestamp migrations (20250101000001–20250101000004), the new migrations will detect the old version numbers in your schema_migrations table, clean them up automatically, and skip any tables that already exist. No manual intervention is needed.
Configuration
Theme setup
In your theme initializer, add block templates and categories:
# config/initializers/themes/my_theme.rb
Spina::Theme.register do |theme|
theme.name = "my_theme"
theme.title = "My Theme"
# Enable the blocks plugin
theme.plugins = ["blocks"]
# Define all available parts (shared between pages and blocks)
theme.parts = [
{ name: "headline", title: "Headline", part_type: "Spina::Parts::Line" },
{ name: "body", title: "Body", part_type: "Spina::Parts::Text" },
{ name: "image", title: "Image", part_type: "Spina::Parts::Image" },
{ name: "button_text", title: "Button Text", part_type: "Spina::Parts::Line" },
{ name: "button_url", title: "Button URL", part_type: "Spina::Parts::Line" }
]
# Block categories (for organizing blocks in the library)
theme.block_categories = [
{ name: "heroes", label: "Heroes" },
{ name: "features", label: "Features" },
{ name: "cta", label: "Call to Action" }
]
# Block templates (which parts each block type uses)
theme.block_templates = [
{
name: "hero",
title: "Hero Section",
description: "Full-width hero with headline and image",
parts: ["headline", "body", "image", "button_text", "button_url"]
},
{
name: "cta_banner",
title: "CTA Banner",
description: "Call to action banner",
parts: ["headline", "body", "button_text", "button_url"]
}
]
# Custom blocks (layout blocks, created automatically during bootstrap)
theme.custom_blocks = [
{ name: "header", title: "Header Block", block_template: "header", category: "heroes" },
{ name: "footer", title: "Footer Block", block_template: "footer", category: "cta" }
]
# Page templates (can use BlockCollection or BlockReference parts)
theme.view_templates = [
{
name: "homepage",
title: "Homepage",
parts: ["headline", "page_blocks"]
}
]
endBlock view templates
Create partials for each block template:
<%# app/views/my_theme/blocks/_hero.html.erb %>
<section class="hero">
<h1><%= block_content(block, :headline) %></h1>
<div><%= block.content(:body) %></div>
</section>Using blocks on pages
Option 1: Page assembled from blocks (via PageBlocks)
In your page template, render all attached blocks:
<%# app/views/my_theme/pages/homepage.html.erb %>
<%= render_blocks %>Manage which blocks appear on a page via Admin > Blocks > (select page).
Option 2: Block as a part type
Use Spina::Parts::BlockReference for a single block or Spina::Parts::BlockCollection for multiple blocks in your theme parts:
theme.parts = [
{ name: "hero_block", title: "Hero Block", part_type: "Spina::Parts::BlockReference" },
{ name: "page_blocks", title: "Page Blocks", part_type: "Spina::Parts::BlockCollection" }
]Filtering blocks by template
By default, BlockReference and BlockCollection show all active blocks in their selects. To limit the list to blocks of a specific template, pass block_template in the part's options:
theme.parts = [
{ name: "hero_block", title: "Hero Block",
part_type: "Spina::Parts::BlockReference",
options: { block_template: "hero" } },
{ name: "sidebar_blocks", title: "Sidebar Blocks",
part_type: "Spina::Parts::BlockCollection",
options: { block_template: "sidebar_widget" } }
]With this configuration, the "Hero Block" select will only show blocks created with the hero template, and "Sidebar Blocks" will only show sidebar_widget blocks. If options is omitted or does not contain block_template, all active blocks are shown (the default behavior).
Then in your template:
<%# Single block reference %>
<%= render_block(content(:hero_block)) %>
<%# Block collection %>
<% content(:page_blocks)&.each do |block| %>
<%= render_block(block) %>
<% end %>Custom blocks (layout blocks)
Custom blocks are non-deletable blocks that are automatically created during rake spina:bootstrap. They work similarly to Spina's custom_pages — once defined in the theme configuration, they are created on bootstrap and cannot be deleted through the admin interface. In the admin UI they are labeled as Layout blocks.
Defining custom blocks
Add custom_blocks to your theme initializer:
theme.custom_blocks = [
{ name: "header", title: "Header Block", block_template: "header", category: "heroes" },
{ name: "footer", title: "Footer Block", block_template: "footer", category: "cta" }
]Each custom block accepts:
| Key | Description |
|---|---|
name |
Immutable machine key (used for lookups in code and templates) |
title |
(Optional) Initial display name shown in admin. Defaults to name.titleize. Editable by users after creation |
block_template |
Must match a template defined in theme.block_templates
|
category |
(Optional) Name of a category from theme.block_categories
|
Key vs Name
Custom blocks have two identifiers:
-
key— an immutable machine identifier (set fromnamein the config). Used byrender_custom_block,BlockReferencewithcustom_blockoption, and for bootstrap lookups. Cannot be changed after creation. Shown as read-only in the block settings tab. -
name— the display name shown in the admin UI. Initially set fromtitle(orname.titleizeiftitleis omitted). Users can freely rename it without breaking any template references.
Regular (non-layout) blocks do not have a key — it is nil for them.
How it works
-
Bootstrap: Custom blocks are created (or updated) automatically whenever
Spina::Accountis saved — this includesrake spina:bootstrapand admin account edits. They are created withdeletable: falseand an immutablekey. -
Idempotent: Running bootstrap multiple times will not duplicate blocks. Existing blocks are found by
keyand updated. The displaynameis only set on initial creation — subsequent bootstraps preserve user edits to the display name. -
Deletion protection: Layout blocks cannot be deleted through the admin UI or programmatically via
destroy. In the block editor, the...menu shows "Block can't be deleted" (matching Spina's page behavior). The controller also rejects delete requests. - Template restriction: Block templates used by custom blocks are excluded from the "New block" form, preventing users from manually creating blocks with layout templates.
-
Admin UI: Layout blocks are marked with a Layout badge (right-aligned) in the block library. A Layout blocks filter is available in the template filter dropdown. On page block lists, the remove button is replaced with a "Layout" label. The
keyis displayed as read-only in the block settings tab.
Using custom blocks in templates
Direct rendering by key
Use render_custom_block to render a system block directly in any template:
<%# app/views/my_theme/pages/homepage.html.erb %>
<%= render_custom_block("header") %>
<main>
<%= render_blocks %>
</main>
<%= render_custom_block("footer") %>This looks up the block by its immutable key and renders it using the block's template partial, just like render_block. Renaming the block's display name in the admin will not affect this lookup.
As a pre-defined BlockReference
Use the custom_block option in a BlockReference part to hardcode which block it points to:
theme.parts = [
{ name: "header_block", title: "Header",
part_type: "Spina::Parts::BlockReference",
options: { custom_block: "header" } },
{ name: "footer_block", title: "Footer",
part_type: "Spina::Parts::BlockReference",
options: { custom_block: "footer" } }
]When custom_block is set:
- The part's
contentmethod returns the block bykey(ignoringblock_id) - In the admin form, instead of a dropdown, a clickable link to the block's edit page is shown
- If the custom block does not exist yet, a warning message is displayed
Then in your template:
<%= render_block(content(:header_block)) %>
<%= render_block(content(:footer_block)) %>Models
| Model | Description |
|---|---|
Spina::Blocks::Block |
Reusable content block with template and parts |
Spina::Blocks::Category |
Block category for organizing the library |
Spina::Blocks::PageBlock |
Join model linking blocks to pages (with position) |
Admin interface
The plugin adds:
- Blocks link in the Content section of the admin sidebar
- Block library with template filter dropdown
- Block editor with content fields (same as page editor)
- Page Blocks management page (per-page block assignment and ordering)
- Layout badge on non-deletable blocks in the block library (right-aligned)
- Layout blocks filter in the template filter dropdown
-
...menu shows "Block can't be deleted" for layout blocks (like Spina pages) - Block templates used by layout blocks are hidden from the "New block" form
- Immutable key shown as read-only in block settings for layout blocks
Helper methods
| Helper | Description |
|---|---|
render_blocks(page) |
Render all blocks attached to a page via PageBlocks |
render_block(block) |
Render a single block using its template partial |
render_custom_block(key) |
Render a custom (layout) block by its immutable key |
block_content(block, :part_name) |
Access a block's content |
block_has_content?(block, :part_name) |
Check if a block has content |
License
The gem is available as open source under the terms of the MIT License.