In our Ember/Rails application, we are generating CSS files for each client based on some settings in the database. For example, our Tenant
model has two fields:
{
primary_color: 'ff3300',
secondary_color: '00ff00'
}
We expose routes
scope '/stylesheets', format: 'css' do
get 'tenant/:tenant_id', to: 'stylesheets#tenant_css'
end
And our controller looks something like this:
class StylesheetsController < ApplicationController
layout nil
rescue_from 'ActiveRecord::RecordNotFound' do
render nothing: true, status: 404
end
def tenant_css
# fetch model
tenant = Tenant.find(params[:tenant_id])
# cache the css under a unique key for tenant
cache_key = "tenant_css_#{tenant.id}"
# fetch the cache
css = Rails.cache.fetch(cache_key) do
# pass sass "params"
render_css_for 'tenant', {
primary_color: tenant.primary_color,
secondary_color: tenant.secondary_color
}
end
render_as_css css
end
protected
# our renderer, could also add a custom one, but simple enough here
def render_as_css(css)
render text: css, content_type: 'text/css'
end
# looks for a template in views/stylesheets/_#{template}.css.erb
def render_css_for(template, params = {})
# load the template, parse ERB w params
scss = render_to_string partial: template, locals: { params: params }
load_paths = [Rails.root.join('app/assets/stylesheets')]
# parse the rendered template via Saas
Sass::Engine.new(scss, syntax: :scss, load_paths: load_paths).render
end
end
This way, you can link to /stylesheets/tenant/1.css
which will render the CSS for the tenant using the Sass engine.
In this case, in views/stylesheets/_tenant.css.erb, you'd have something like this (it's an ERB file but you can use Sass in there now):
@import "bootstrap-buttons";
<% if params[:primary_color].present? %>
$primary-color: <%= params[:primary_color] %>;
h1, h2, h3, h4, h5, h6 {
color: $primary-color;
}
<% end %>
<% if params[:secondary_color].present? %>
$secondary-color: <%= params[:secondary_color] %>;
a {
color: $secondary-color;
&:hover {
color: darken($secondary-color, 10%);
}
}
<% end %>
You'll note that I can now use @import
to import anything in your stylesheet path for the Sass engine (in this case, I can utilize some helpers from Bootstrap Sass lib).
You'll want to have some sort of cache cleaner to wipe the cache when your model backing the CSS is updated:
class Tenant < ActiveRecord::Base
after_update do
Rails.cache.delete("tenant_css_#{id}")
end
end
So that's the Rails side in a nutshell.
In Ember, my guess is you'll want to load the stylesheet based on an ID, so that stylesheet cannot be hard-coded into "index.html". Ember CSS Routes addon might serve you well, but I found that it just appends <link>
to the header, so if you need to swap CSS stylesheets at any time, this won't work. I got around this in a route like so:
afterModel(model, transition) {
// dynamically form the URL here
const url = "/stylesheets/tenant/1";
// build link object
const $link = $('<link>', { rel: 'stylesheet', href: url, id: 'tenant-styles' });
// resolve the promise once the stylesheet loads
const promise = new RSVP.Promise((resolve, reject) => {
$link.on('load', () => {
$link.appendTo('head');
resolve();
}).on('error', () => {
// resolve anyway, no stylesheet in this case
resolve();
});
});
return promise;
},
// remove the link when exiting
resetController(controller, isExiting, transition) {
this._super(...arguments);
if (isExiting) {
$('#tenant-styles').remove();
}
}
You could also add a blank element in the <head>
and then use Ember Wormhole to format a <link>
tag and render into the "wormhole".
Edit
You could also look into rendering Sass directly in the client application. For something as simple as two colors, this wouldn't have much of performance impact, especially if you used a service worker or similar to cache the results.