Skip to content

Commit 6319d19

Browse files
committed
feat: add rendering mode and client theme management for Astro frontend
- Added rendering_mode to Pwb::Website to distinguish between Rails and client-side rendering - Created Pwb::ClientTheme model to manage Astro-based themes - Added Pwb::WebsiteRenderingMode concern for website rendering logic - Updated database schema and added migrations - Added factories and specs for the new functionality
1 parent c3a6769 commit 6319d19

13 files changed

+1033
-9
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# frozen_string_literal: true
2+
3+
# Concern for managing website rendering modes
4+
# Websites can use either Rails (B themes) or Client (A themes) rendering
5+
#
6+
# Key behaviors:
7+
# - rendering_mode defaults to 'rails'
8+
# - client_theme_name is required when using 'client' mode
9+
# - rendering_mode becomes immutable after website has content
10+
#
11+
module Pwb
12+
module WebsiteRenderingMode
13+
extend ActiveSupport::Concern
14+
15+
RENDERING_MODES = %w[rails client].freeze
16+
17+
included do
18+
# ===================
19+
# Validations
20+
# ===================
21+
validates :rendering_mode, inclusion: { in: RENDERING_MODES }
22+
validates :client_theme_name, presence: true, if: :client_rendering?
23+
validate :client_theme_must_exist, if: :client_rendering?
24+
validate :rendering_mode_immutable, on: :update
25+
end
26+
27+
# ===================
28+
# Instance Methods
29+
# ===================
30+
31+
# Check if using Rails rendering (B themes)
32+
#
33+
# @return [Boolean]
34+
def rails_rendering?
35+
rendering_mode == 'rails'
36+
end
37+
38+
# Check if using client rendering (A themes)
39+
#
40+
# @return [Boolean]
41+
def client_rendering?
42+
rendering_mode == 'client'
43+
end
44+
45+
# Get the client theme object
46+
#
47+
# @return [Pwb::ClientTheme, nil]
48+
def client_theme
49+
return nil unless client_rendering?
50+
51+
@client_theme ||= Pwb::ClientTheme.enabled.by_name(client_theme_name)
52+
end
53+
54+
# Get merged theme config (defaults + website overrides)
55+
#
56+
# @return [Hash]
57+
def effective_client_theme_config
58+
return {} unless client_theme
59+
60+
client_theme.config_for_website(self)
61+
end
62+
63+
# Generate CSS variables for client theme
64+
#
65+
# @return [String]
66+
def client_theme_css_variables
67+
return '' unless client_theme
68+
69+
client_theme.generate_css_variables(effective_client_theme_config)
70+
end
71+
72+
# Check if rendering mode can still be changed
73+
# Locked after website has been provisioned and has content
74+
#
75+
# @return [Boolean]
76+
def rendering_mode_locked?
77+
provisioning_completed_at.present? && page_contents.any?
78+
end
79+
80+
# Check if rendering mode can be changed
81+
#
82+
# @return [Boolean]
83+
def rendering_mode_changeable?
84+
!rendering_mode_locked?
85+
end
86+
87+
private
88+
89+
# Validate that the client theme exists and is enabled
90+
def client_theme_must_exist
91+
return unless client_theme_name.present?
92+
93+
unless Pwb::ClientTheme.enabled.exists?(name: client_theme_name)
94+
errors.add(:client_theme_name, 'is not a valid client theme')
95+
end
96+
end
97+
98+
# Prevent changing rendering_mode after content is created
99+
def rendering_mode_immutable
100+
return unless rendering_mode_changed?
101+
return unless rendering_mode_locked?
102+
103+
errors.add(:rendering_mode, 'cannot be changed after website has content')
104+
end
105+
end
106+
end

app/models/pwb/client_theme.rb

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# frozen_string_literal: true
2+
3+
# == Schema Information
4+
#
5+
# Table name: pwb_client_themes
6+
#
7+
# id :bigint not null, primary key
8+
# name :string not null
9+
# friendly_name :string not null
10+
# version :string default("1.0.0")
11+
# description :text
12+
# preview_image_url :string
13+
# default_config :jsonb not null, default({})
14+
# color_schema :jsonb not null, default({})
15+
# font_schema :jsonb not null, default({})
16+
# layout_options :jsonb not null, default({})
17+
# enabled :boolean default(TRUE), not null
18+
# created_at :datetime not null
19+
# updated_at :datetime not null
20+
#
21+
# Indexes
22+
#
23+
# index_pwb_client_themes_on_enabled (enabled)
24+
# index_pwb_client_themes_on_name (name) UNIQUE
25+
#
26+
module Pwb
27+
class ClientTheme < ApplicationRecord
28+
self.table_name = 'pwb_client_themes'
29+
30+
# ===================
31+
# Validations
32+
# ===================
33+
validates :name, presence: true, uniqueness: true,
34+
format: { with: /\A[a-z][a-z0-9_]*\z/, message: 'must be lowercase letters, numbers, and underscores' }
35+
validates :friendly_name, presence: true
36+
37+
# ===================
38+
# Scopes
39+
# ===================
40+
scope :enabled, -> { where(enabled: true) }
41+
scope :by_name, ->(name) { where(name: name) }
42+
43+
# Find a single theme by name
44+
def self.by_name(name)
45+
find_by(name: name)
46+
end
47+
48+
# ===================
49+
# Instance Methods
50+
# ===================
51+
52+
# Get the merged config for a website (defaults + website overrides)
53+
#
54+
# @param website [Pwb::Website] The website to get config for
55+
# @return [Hash] Merged configuration
56+
def config_for_website(website)
57+
default_config.merge(website.client_theme_config || {})
58+
end
59+
60+
# Generate CSS variables from a configuration
61+
#
62+
# @param config [Hash] Configuration to convert (defaults to default_config)
63+
# @return [String] CSS :root block with variables
64+
def generate_css_variables(config = default_config)
65+
return '' if config.blank?
66+
67+
css_vars = config.map do |key, value|
68+
css_var_name = key.to_s.tr('_', '-')
69+
"--#{css_var_name}: #{value}"
70+
end
71+
72+
":root { #{css_vars.join('; ')}; }"
73+
end
74+
75+
# API serialization
76+
#
77+
# @return [Hash] Theme data for API responses
78+
def as_api_json
79+
{
80+
name: name,
81+
friendly_name: friendly_name,
82+
version: version,
83+
description: description,
84+
preview_image_url: preview_image_url,
85+
default_config: default_config,
86+
color_schema: color_schema,
87+
font_schema: font_schema,
88+
layout_options: layout_options
89+
}
90+
end
91+
92+
# For select options in forms
93+
#
94+
# @return [Array<Array>] Array of [friendly_name, name] pairs
95+
def self.options_for_select
96+
enabled.order(:friendly_name).pluck(:friendly_name, :name)
97+
end
98+
end
99+
end

app/models/pwb/website.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
# analytics_id_type :integer
1111
# available_currencies :text default([]), is an Array
1212
# available_themes :text is an Array
13+
# client_theme_config :jsonb
14+
# client_theme_name :string
1315
# company_display_name :string
1416
# compiled_palette_css :text
1517
# configuration :json
@@ -63,6 +65,7 @@
6365
# raw_css :text
6466
# realty_assets_count :integer default(0), not null
6567
# recaptcha_key :string
68+
# rendering_mode :string default("rails"), not null
6669
# rent_price_options_from :text default(["", "250", "500", "750", "1,000", "1,500", "2,500", "5,000"]), is an Array
6770
# rent_price_options_till :text default(["", "250", "500", "750", "1,000", "1,500", "2,500", "5,000"]), is an Array
6871
# sale_price_options_from :text default(["", "25,000", "50,000", "75,000", "100,000", "150,000", "250,000", "500,000", "1,000,000", "2,000,000", "5,000,000", "10,000,000"]), is an Array
@@ -100,6 +103,7 @@
100103
# index_pwb_websites_on_palette_mode (palette_mode)
101104
# index_pwb_websites_on_provisioning_state (provisioning_state)
102105
# index_pwb_websites_on_realty_assets_count (realty_assets_count)
106+
# index_pwb_websites_on_rendering_mode (rendering_mode)
103107
# index_pwb_websites_on_search_config (search_config) USING gin
104108
# index_pwb_websites_on_selected_palette (selected_palette)
105109
# index_pwb_websites_on_site_type (site_type)
@@ -118,6 +122,7 @@ class Website < ApplicationRecord
118122
include Pwb::WebsiteSocialLinkable
119123
include Pwb::WebsiteLocalizable
120124
include Pwb::WebsiteThemeable
125+
include Pwb::WebsiteRenderingMode
121126
include Pwb::DemoWebsite
122127
include FlagShihTzu
123128

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
3+
# Migration to add rendering mode support to websites
4+
# This enables websites to choose between Rails (B themes) and Client (A themes) rendering
5+
class AddRenderingModeToWebsites < ActiveRecord::Migration[8.0]
6+
def change
7+
# Rendering mode: 'rails' for traditional server-side rendering, 'client' for Astro
8+
add_column :pwb_websites, :rendering_mode, :string, default: 'rails', null: false
9+
10+
# Client theme name (only used when rendering_mode = 'client')
11+
add_column :pwb_websites, :client_theme_name, :string
12+
13+
# Website-specific client theme configuration overrides
14+
add_column :pwb_websites, :client_theme_config, :jsonb, default: {}
15+
16+
# Index for filtering by rendering mode
17+
add_index :pwb_websites, :rendering_mode
18+
19+
# Check constraint to ensure only valid values
20+
add_check_constraint :pwb_websites,
21+
"rendering_mode IN ('rails', 'client')",
22+
name: 'rendering_mode_valid'
23+
end
24+
end
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# frozen_string_literal: true
2+
3+
# Creates the pwb_client_themes table for Astro A themes
4+
# These are database-backed themes used by client-rendered websites
5+
class CreatePwbClientThemes < ActiveRecord::Migration[8.0]
6+
def change
7+
create_table :pwb_client_themes do |t|
8+
# Theme identifier (lowercase, e.g., 'amsterdam')
9+
t.string :name, null: false
10+
11+
# Human-readable name (e.g., 'Amsterdam Modern')
12+
t.string :friendly_name, null: false
13+
14+
# Semantic version
15+
t.string :version, default: '1.0.0'
16+
17+
# Description for admin UI
18+
t.text :description
19+
20+
# Preview image URL for theme selection
21+
t.string :preview_image_url
22+
23+
# Default configuration values
24+
t.jsonb :default_config, default: {}
25+
26+
# Schema for color customization options
27+
t.jsonb :color_schema, default: {}
28+
29+
# Schema for font customization options
30+
t.jsonb :font_schema, default: {}
31+
32+
# Schema for layout customization options
33+
t.jsonb :layout_options, default: {}
34+
35+
# Whether this theme is available for selection
36+
t.boolean :enabled, default: true, null: false
37+
38+
t.timestamps
39+
end
40+
41+
# Unique constraint on name
42+
add_index :pwb_client_themes, :name, unique: true
43+
44+
# Index for filtering enabled themes
45+
add_index :pwb_client_themes, :enabled
46+
end
47+
end

db/schema.rb

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#
1111
# It's strongly recommended that you check this file into your version control system.
1212

13-
ActiveRecord::Schema[8.1].define(version: 2026_01_10_165317) do
13+
ActiveRecord::Schema[8.1].define(version: 2026_01_15_140001) do
1414
# These are extensions that must be enabled in order to support this database
1515
enable_extension "pg_catalog.plpgsql"
1616
enable_extension "pgcrypto"
@@ -166,6 +166,23 @@
166166
t.index ["user_id"], name: "index_pwb_authorizations_on_user_id"
167167
end
168168

169+
create_table "pwb_client_themes", force: :cascade do |t|
170+
t.jsonb "color_schema", default: {}
171+
t.datetime "created_at", null: false
172+
t.jsonb "default_config", default: {}
173+
t.text "description"
174+
t.boolean "enabled", default: true, null: false
175+
t.jsonb "font_schema", default: {}
176+
t.string "friendly_name", null: false
177+
t.jsonb "layout_options", default: {}
178+
t.string "name", null: false
179+
t.string "preview_image_url"
180+
t.datetime "updated_at", null: false
181+
t.string "version", default: "1.0.0"
182+
t.index ["enabled"], name: "index_pwb_client_themes_on_enabled"
183+
t.index ["name"], name: "index_pwb_client_themes_on_name", unique: true
184+
end
185+
169186
create_table "pwb_clients", id: :serial, force: :cascade do |t|
170187
t.integer "address_id"
171188
t.string "client_title"
@@ -1086,6 +1103,8 @@
10861103
t.integer "analytics_id_type"
10871104
t.text "available_currencies", default: [], array: true
10881105
t.text "available_themes", array: true
1106+
t.jsonb "client_theme_config", default: {}
1107+
t.string "client_theme_name"
10891108
t.string "company_display_name"
10901109
t.text "compiled_palette_css"
10911110
t.json "configuration", default: {}
@@ -1141,6 +1160,7 @@
11411160
t.text "raw_css"
11421161
t.integer "realty_assets_count", default: 0, null: false
11431162
t.string "recaptcha_key"
1163+
t.string "rendering_mode", default: "rails", null: false
11441164
t.text "rent_price_options_from", default: ["", "250", "500", "750", "1,000", "1,500", "2,500", "5,000"], array: true
11451165
t.text "rent_price_options_till", default: ["", "250", "500", "750", "1,000", "1,500", "2,500", "5,000"], array: true
11461166
t.text "sale_price_options_from", default: ["", "25,000", "50,000", "75,000", "100,000", "150,000", "250,000", "500,000", "1,000,000", "2,000,000", "5,000,000", "10,000,000"], array: true
@@ -1172,11 +1192,13 @@
11721192
t.index ["palette_mode"], name: "index_pwb_websites_on_palette_mode"
11731193
t.index ["provisioning_state"], name: "index_pwb_websites_on_provisioning_state"
11741194
t.index ["realty_assets_count"], name: "index_pwb_websites_on_realty_assets_count"
1195+
t.index ["rendering_mode"], name: "index_pwb_websites_on_rendering_mode"
11751196
t.index ["search_config"], name: "index_pwb_websites_on_search_config", using: :gin
11761197
t.index ["selected_palette"], name: "index_pwb_websites_on_selected_palette"
11771198
t.index ["site_type"], name: "index_pwb_websites_on_site_type"
11781199
t.index ["slug"], name: "index_pwb_websites_on_slug"
11791200
t.index ["subdomain"], name: "index_pwb_websites_on_subdomain", unique: true
1201+
t.check_constraint "rendering_mode::text = ANY (ARRAY['rails'::character varying, 'client'::character varying]::text[])", name: "rendering_mode_valid"
11801202
end
11811203

11821204
create_table "pwb_widget_configs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|

0 commit comments

Comments
 (0)