Today I would like to talk to you about theming a front-end for white-labeling purposes. Recently this topic crossed my path as we were refreshing the layouts and back-ends of all our applications which include our Editor, Dashboard (login required) and website.
We always offered a branding option to our business customers and this also has to work in the refreshed layouts of the Editor and Dashboard. Before I started implementing this in the dashboard however, we had a solution already built into our Editor which used JS and CORS-enabled stylesheets to modify the CSS after it was sent to the client.
I ended up duplicating this approach in the dashboard due to a 3-day deadline and the fact that our back-enders didn’t have the time to set-up a more robust approach with me at the moment since they were already working hard to migrate our users from an old payment system to a new one.
While I was implementing this I kept thinking it was just wrong to do this kind of manipulation on the client side, we’d have to actually block rendering for this JS to complete in order to prevent flashing of colors. The JS could fail to execute and break the CSS entirely and to finish it off, it also required CORS to be enabled on the CSS.
Despite all this, support for browsers seemed decent enough for us to use this method and it worked. It worked remarkably well for the amount of code it took to actually change values in a stylesheet from one to another:
Then came the catch, of-CORS-e…
This theming branch got merged, deployed and met the deadline. It worked fine for that specific customer, but we started seeing some of our users reporting issues where they sometimes saw this instead of our Editor:
Naturally, we rushed to reproduce but this proved difficult. After some fiddling in different browsers, double checking with back-enders if all CORS settings were correct, and checking additional user reports coming from support, we learned that this had to do with a WebKit exclusive caching bug that is marked as wontfix.
The bug causes a file cached by the browser that was initially sent without CORS headers, to not be retrieved (cache busted) again once CORS headers are present in subsequent requests.
This resulted in CSS without CORS being served from cache into this theming environment that required CSS to be CORS enabled. When this wasn’t the case, some properties would not be present such as “cssRules” on the stylesheet we were trying to modify. In turn this caused JS to error out and render the CSS useless.
Problem identified, time to fix it!
So with the fact that this caching issue will not be fixed in mind, we set out to get rid of the CORS solution. There was one simple solution but it required some back-end changes that we did not want to mix into their current workload.
This solution was to build a version of our stylesheet using the customers’ company colors. Then, before serving the page to a user, check if (s)he is part of this company and serve a different stylesheet based on that. Simple, but very effective and robust.
Nothing can really break since there are no additional “moving parts” as opposed to the JS+CORS solution. The checking gets moved to the back-end instead of the front-end so that browser-caching is not an issue and CORS isn’t necessary for this either since the right stylesheet will be sent from the start.
The only issue I can think of is that we would have to create a separate variables.sass file for each new customer and keep them all synced to prevent compile-errors from missing / outdated settings, this also gets magnified by the fact that we have multiple products that need to work with such a system.
But due to the constraints we decided to do something even better. That required no additional stylesheets to be generated but rather, inject some variable overrides in server-rendered HTML templates using CSS variables!
This solution only required us to change places we used SASS variables to use var(–varname) statements and add fall backs using the regular SASS variables above these lines. Since CSS fails on a line-by-line basis if a browser does not understand the var(–varname) syntax, it will just be ignored and the fallback will be used instead.
This actually allows us to do some pretty neat things with regards to UI customization. When offering a UI to our customers for example, we could live-update the variables using some JS to show instant-previews of how it would look in different colors. Even without a UI this can be done directly in the DevTools:
The only downside to this approach is that it is not supported in any version of IE which we would have been able to support with the back-end stylesheet generating solution (aside from live-updating, that is). Perhaps that is something we will still look into but IE is dying and Edge will switch to Blink. The only downside to having no support in IE11 is that our default brand colors will be used instead since they are the defaults.
Fixed! Or, maybe not?
So here we are with our CSS variables in place, deployed to production and of course, the issue did not disappear… For a little while we were stumped since we did not rely on CORS anymore, we checked request headers and our CDN settings to see if they had any useful information for us.
Nothing seemed out of the ordinary, well, nothing except one small little attribute called crossorigin. I had set this on our CSS tags as well as our JS script tags during testing and forgot to remove them afterwards. This resulted in the following error for some users that weren’t migrated to our latest Dashboard yet. It looks like this in the console:
Of course, the caching rule is essential here. Since these assets were already cached long before we wanted to use CORS, and these users would not get a newly-versioned CSS file, they would be stuck in this loop of serving a CORS-disabled file from cache while CORS actually has to be enabled due to the crossorigin attribute. Which in turn causes the dashboard to just explode since now, this JS file that is responsible for serving the rest of the JS will not be executed due to essentially non-existent permission issues.
We definitely did learn a lot, we had to dig far and deep to pinpoint the issues and had basically 0-days to fix the issue since users were already affected. In the end it comes down to the following: If you don’t have to use CORS, then don’t use it.
In our case it was a mistake on my part to leave these attributes in there, even if accidental. One thing to note though, is that this issue wouldn’t have arisen if all of our users were already migrated and were using the latest Dashboard instead.
A simple deploy with an incremented version number would cause all files to be cache-busted and re-fetched from the server correctly, with proper CORS headers sent. Not using CORS however, is always a safer option and preferable over ones that do use CORS.
This concludes my post on theming a front-end. It was a blast and I would definitely love to take another stab at it to see if we can improve our setup even more. Either by making it backwards compatible with older browsers or just doing awesome stuff with it.
I hope you enjoyed this post and learned something new.
Have a good one!