Site Theming¶
Stagemgr ships generic, unbranded copy. Making it sound like your house is three mechanisms, in descending order of preference:
| Kind of string | Where it belongs | Example |
|---|---|---|
| A fact | the theater: block of config/server.yml |
phone, street address, doors-open time, social URLs, ticket-pickup window |
| A proper noun | nothing to do -- it is already TheaterInfo#name |
"Thank you for supporting Palisade Theater" |
| Editorial copy | a view override in a site theme | "…and the many storefront theaters that make our home so special" |
Reach for a theme only for the third. A fact is read on nearly every public page and in nearly every email; a config key means one place to change a phone number instead of a grep across a theme.
sites/README.md in the repository is the reference for theme authors and goes
into more detail than this page.
The theater: block¶
Under all: in config/server.yml. Every key may be left blank -- the sentence
that would have used it is omitted rather than printed empty, so a house with no
phone number simply never shows a "call us" line.
| Key | Meaning | Blank falls back to |
|---|---|---|
phone |
Box office phone, printed on order pages and in mail | the sentence is omitted |
street_address |
Street line | -- |
city_state_zip |
City/state/postcode line | -- |
pickup_window_text |
How the box office describes when tickets can be collected, e.g. "ninety minutes before curtain" | the sentence is omitted |
doors_open_minutes_before |
Minutes before curtain the house opens (an integer, or a string of digits) | the sentence is omitted |
website_url |
Marketing site | the Default theater row's url from the admin form |
facebook_url, twitter_url, instagram_url |
Social links; each button appears only if its URL is set | the button is omitted |
logo_url |
Absolute URL of a logo for email -- email clients cannot fetch this app's assets | the Default theater's uploaded logo, then the house name as text |
mailing_list_blurb |
One sentence under the mailing-list opt-in checkbox | the sentence is omitted |
box_office_display_name |
How the box office signs itself | "<house name> Box Office" |
artistic_director_name |
Who signs follow-up mail | see below |
artistic_director_title |
Their title, printed under the name in the signature block | nothing -- the title line is omitted. (server.yml.example pre-fills "Artistic Director"; there is no fallback in the template) |
artistic_director_email |
Their address | see below |
signature_image_url |
Absolute URL of a signature image for the sign-off | no image, just the typed name |
stripe_billing_portal_url |
Stripe customer portal link for members managing their own subscription | the button is omitted |
The box office email address is deliberately not here: it is the existing
email: addresses: box_office: key, read by TheaterInfo#box_office_email.
The house's name is database-driven
It is the name of the Default theater row, not a config key and not a
theme string. Renaming that row in the admin renames your house in every
public page and every email at once. That is intended -- and it means a
careless rename is a very visible edit. Before any theater row exists,
TheaterInfo#name falls back to app_name from server.yml.
"The Default theater row" means Theater.default_theater: the theater whose
theater_class is Default (the other classes are Co-production, Resident
Company, Visiting Company and Guest Artist) and, if there is more than one, the
oldest -- it is where(theater_class: 'Default').order(:id).first. A second
Default row is therefore inert as far as the house name goes, which is why
rake setup:theater renames the existing one instead of adding another.
Reading facts in a view¶
ApplicationHelper#theater_info builds one TheaterInfo per render, and
ActionMailer includes the same helper:
- if theater_info.phone.present?
%p= "Questions? Call the box office at #{theater_info.phone}."
| Method | Returns |
|---|---|
name |
The house's proper name |
| Any key from the table above | That fact, or nil |
full_address |
"1229 W Belmont, Chicago, IL 60657", or nil when neither half is set |
box_office_email |
From email: addresses: box_office: |
box_office_from |
An RFC-2822 From: header, e.g. "Palisade Box Office" <boxoffice@palisade.example>. Never nil |
artistic_director_from |
The same for the artistic director, falling back to box_office_from |
artistic_director? |
Both a name and an address are configured |
first_person? |
A name is configured |
default_theater |
The Default Theater row, or nil |
The two predicates ask different questions and are not interchangeable:
first_person?decides pronouns. A name is enough to say "I hope you enjoyed the show" -- the letter is signed by whoever the house named, whether or not patrons can write back. Every pronoun in the mailer views is chosen by this one predicate, so a house cannot end up with a letter that says "I" in one paragraph and "we" in the next.artistic_director?is stricter, because it decides the From: header and whether to print an address to reply to. Neither works without both halves.
A mistyped fact name raises KeyError listing the known facts, rather than
returning nil and quietly dropping a sentence.
Themes¶
A site theme is a directory of view overrides carrying one house's editorial copy. Themes live in this repository on purpose: house copy is editorial, it gets reviewed, and it should promote with a git branch like every other change.
bundle exec rake setup:site[myhouse]
That copies sites/example/ to sites/myhouse/ and inserts
site_theme: myhouse under all: in config/server.yml. What you get is
deliberately close to empty:
sites/myhouse/
├── README.md how to override something, and what not to
├── views/.keep shadows app/views/, same relative paths
├── locales/en.yml loaded last, so these keys win (all commented out)
└── images/.keep source-of-truth artwork, NOT served
Then fill it in -- a populated theme looks like this:
sites/myhouse/
├── views/
│ ├── shared/_house_thanks_line.html.haml
│ └── order_mailer/_seating_policy.html.haml
├── locales/en.yml
└── images/signature.png
Restart to activate it
site_theme is read while the environment file loads, so the process that
ran setup:site is not using the theme yet -- setup:doctor will still say
"site_theme not set". Restart (docker compose restart stagemgr) and the
boot log says [SiteTheme] 'myhouse' active.
The override rule¶
Any file under sites/<slug>/views/ shadows the same relative path under
app/views/ -- public pages, mailer templates and layouts alike, because the
theme directory is prepended to the view paths of both ActionController::Base
and ActionMailer::Base at boot.
To override a file, copy the generic one and edit the copy:
cp app/views/order_mailer/_seating_policy.html.haml \
sites/myhouse/views/order_mailer/
Never edit app/views/ for house copy -- the next house to install Stagemgr gets
whatever is there.
Keep overrides small
An override is a replacement, not a patch: your file renders, the generic
one does not, and yours never receives later fixes to the file you copied.
Override the smallest file that contains the words you want to change. If
the string is buried in a large template, extract it into a partial in
app/views/ first and override that. shared/_house_thanks_line is the
model.
sites/example/ deliberately ships no view files: a blank override is worse
than no override, because it would render an empty section rather than the
generic one. A new theme starts empty and you add to it.
sites/example/README.md lists the files houses usually want to override --
transportation, dining, refunds, seating policy, amenities, the follow-up
letter's pride paragraph, the donation receipt.
One override reaches further than you expect¶
app/views/donation_orders/show.html.erb renders order_mailer/_signature. So
a theme override of that mailer partial also restyles the donation thank-you
web page, not just the email. That is a feature -- one signature block, one
definition -- but it is worth knowing before you tune the partial for email
clients only.
Locales¶
sites/<slug>/locales/*.yml are appended last to I18n.load_path, so their
keys beat config/locales/. Today that carries exactly one string: the donation
receipt's subject line.
A new locale file needs a restart
The load path is built at boot and I18n only watches files it already knows about. Editing a file that is already there is picked up on the next request in development; adding one is not.
Slugs, and what stops the boot¶
A slug may contain lowercase letters, digits, hyphens and underscores. Anything
else raises at boot, which is what keeps site_theme: ../../etc from being a
path -- a malformed slug is always a typo or an attack, never a deployment that
should be allowed to serve generic pages.
A slug whose directory is simply absent only warns, and the app serves
generic copy. That asymmetry is deliberate: a production checkout that has never
had sites/ deployed to it must keep booting.
A sites/<slug> that is itself a symlink still points wherever it points,
including outside the checkout. That is a file a developer created on purpose in
their own working tree, and it is not treated as an attack.
Images¶
Themes serve no assets. sites/<slug>/images/ keeps the artwork of record (a
signature, a logo) versioned alongside the copy that uses it -- nothing serves
it. Email clients cannot fetch anything from this application, so image URLs in
mail must be absolute and publicly reachable: set theater: logo_url: and
theater: signature_image_url: to URLs on your marketing site, or hard-code an
absolute URL in a theme partial.
Testing a theme¶
The suite runs with no theme active, against the generic copy, so a broken theme cannot mask a broken default. To exercise one:
SiteTheme.with_theme('myhouse') do
# views and locales installed for the duration of the block
end
spec/mailers/theme_render_spec.rb uses that to render every customer-facing
email under the theaterwit theme and assert its copy is intact. Add your own
house to it: a partial renamed under app/views/ but not in your theme raises
only when that theme is active, so nothing else in the suite would catch it.
The grep that keeps it honest¶
One house's name, phone number, street or people may appear only in:
sites/<slug>/**-- that house's own themedocs/**-- documentation and examples- test sentinels (
config/server.yml.example'stest:block, specs, features)
Anything under app/, lib/ or config/ that names a specific theater is a
bug. Before merging a change to public pages or mail, grep for it:
git grep -nEi 'theaterwit|theater wit|975-8150|belmont' -- app lib config