Skip to content

Define page templates and layout parts in dedicated Ruby files - #1437

Open
simmerz wants to merge 1 commit into
SpinaCMS:mainfrom
simmerz:theme_config
Open

simmerz wants to merge 1 commit into
SpinaCMS:mainfrom
simmerz:theme_config

Conversation

@simmerz

@simmerz simmerz commented May 25, 2026

Copy link
Copy Markdown
Member

Theme initializers had grown into long lists of parts and view templates that were hard to read and painful to maintain. This moves those definitions into app/templates/spina/{theme}/, one file per page template plus an optional layout.rb for site-wide parts. PageTemplate.define and LayoutParts.define provide a small DSL for declaring parts and repeaters. Built-in part types use symbols; custom parts keep their full class name as a string. At boot, Spina compiles these files into the same structures the admin already expects, so the rest of the engine stays largely unchanged.

Part definitions are scoped to their template (and separately for layout), so the same name can mean different things in different places without conflict. The old theme.parts and theme.view_templates configuration still works for now, with a deprecation warning and a spina:theme:migrate_templates task to convert existing themes. The migrator backs up the original initializer as .rb.bak so Rails won't load it.

The install generator and dummy app are updated to use the new layout.

@simmerz
simmerz force-pushed the theme_config branch 2 times, most recently from 2ef6242 to fffb14b Compare May 25, 2026 18:54
@Bramjetten

Copy link
Copy Markdown
Contributor

Woah, that’s a lot of changes!

@simmerz

simmerz commented May 26, 2026

Copy link
Copy Markdown
Member Author

It needed a bit to change how the parts worked - when I got into it, I realised we needed to solve for conflicts in naming where two pages or the layout have parts with the same name - those shouldn't matter and should work, but wouldn't have.

@wakproductions

Copy link
Copy Markdown
Contributor

Sheeeeesh! This is a significant change. I like the idea of the abstractions. I think this is a very difficult program to work on architecturally as is. It's hard for me to tell right now as so much of this I still haven't figured out, but I hope that this new organization for the page template metadata will make it easier to contribute new modules to Spina.

@Bramjetten

Copy link
Copy Markdown
Contributor

The migration task crashes: lib/tasks/theme.rake:20 uses bare ThemeMigrator instead of Spina::ThemeMigrator, which raises NameError.

Instead of aliasing PageTemplate and LayoutParts onto Object so template files can use the bare names, what do you think about exposing the DSL as Spina.define_template :homepage do ... end (and Spina.define_layout_parts)? Since Spina is already a real top-level constant, the files can be executed with a plain Kernel.load, no Object.const_set.

@simmerz

simmerz commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

Yeah I like that approach. Done that and fixed the rake.

@Bramjetten

Bramjetten commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

A few remaining items from my review:

  1. Migration task can fail halfway and brick boot: the .bak and initializer rewrite only happen after all template files are generated, so a failure during generation (e.g. KeyError from a view_templates entry referencing a part missing from theme.parts) leaves template files on disk with the legacy initializer intact, the both-styles state that raises at boot. Validate part references up front, or clean up generated files on error. (lib/tasks/theme.rake:16-25)
  2. Dev reload gaps: newly created template files don't trigger reloads (watcher snapshots the file list at boot); custom load_templates_from paths aren't watched at all. (lib/spina/theme_reloader.rb)
  3. Migrator silently lossy on nested repeaters and repeater options: keys.
  4. Nits: indentation in install_generator.rb:62-68; shadowed attr_readers in page_template.rb:55; unused indent: kwarg in theme_migrator.rb:135; dead exclude_from branch; duplicated definitions_compatible?/merge_metadata logic; Zeitwerk private API in test.

@Bramjetten

Copy link
Copy Markdown
Contributor

Regarding the dev reload gap: the fix is to watch directories instead of a snapshot of files. file_watcher.new(files, dirs) takes a second argument — a hash of directories to extensions — and directory watching is recursive and picks up files that are added or removed later, not just edits to files that existed at boot:

def updater
  @updater ||= Rails.application.config.file_watcher.new([], watched_dirs) do
    reload!
  end
end

def watched_dirs
  {
    Rails.root.join("config/initializers/themes").to_s => ["rb"],
    Rails.root.join("app/templates/spina").to_s => ["rb"]
  }
end

This also removes the boot-time globs. One caveat: custom theme.load_templates_from locations still wouldn't be watched, since themes register after the watcher is built. Either rebuild the watcher once after the initial theme load (so Spina::Theme.all paths can be included), or document that custom template paths don't hot-reload in development.

@simmerz

simmerz commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

Force pushed again to keep the PR as a single commit

@Bramjetten Bramjetten left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sectioned layout parts break. theme.layout_parts can be a Hash since #1307 (tabs in the Layout screen). The DSL can't express sections, and the migrator crashes on them — validate_part_references! does Array(@theme.layout_parts) which turns a Hash into [key, value] pairs (theme_migrator.rb:82):

MissingPartError: Theme "sectioned" references undefined parts: "[:general, [\"footer\"]]", "[:seo, [\"footer\"]]"

current_theme_name isn't thread-safe. It's a class-level ivar (page_template.rb:36), so concurrent Theme.register calls (common in multi-tenant setups that register themes at runtime) interleave: thread A's template files register under thread B's theme, and the ensure blocks restore in the wrong order. Result: parts leak across tenants, the other theme ends up empty. Rails >= 7.0 is required anyway, so ActiveSupport::IsolatedExecutionState plus a Mutex around registration fixes it. The registries (@registry ||= {}, clear_for_theme racing readers) have the same problem.

Zeitwerk test is vacuous. refute Object.const_defined?(:Homepage) passes regardless — the file would map to Spina::Demo::Homepage, not Homepage. Assert Rails.autoloaders.main.ignores?(...) instead.

Ignore is broader than needed. engine.rb:20 ignores all of app/templates; scoping to app/templates/spina avoids breaking host apps using that directory.

Theme initializers had grown into long lists of parts and view
templates that were hard to read and painful to maintain. This moves
those definitions into app/templates/spina/{theme}/, one file per page
template plus an optional layout.rb for site-wide parts.
Spina.define_template and Spina.define_layout_parts provide a small
DSL for declaring parts and repeaters without aliasing constants onto
Object. Built-in part types use symbols; custom parts keep their full
class name as a string. At boot, Spina compiles these files into the
same structures the admin already expects, so the rest of the engine
stays largely unchanged.

Part definitions are scoped to their template (and separately for
layout), so the same name can mean different things in different
places without conflict. The old theme.parts and theme.view_templates
configuration still works for now, with a deprecation warning and a
spina:theme:migrate_templates task to convert existing themes. The
migrator backs up the original initializer as .rb.bak so Rails won't
load it.

The install generator and dummy app are updated to use the new layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
@simmerz

simmerz commented Jul 31, 2026

Copy link
Copy Markdown
Member Author

Hopefully that's the last of them :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants