NOTE: this is based on my first round of getting a multi-tenant app to use themes. It's not the cleanest way, but it works for now

Assuming you're using Lucky and you send multiple domains to that same app, you may need to style each site slightly different. To do this, you'd want to theme each site. In these examples, I have a Site model that has a theme column.

The first thing we will do is create a mixin for our actions. In src/actions/mixins/ create a new file themable.cr .



module Themable macro included expose current_theme end enum Themes Default Dark end # if it returns nil or a non-listed theme # just return the default def current_theme : String # `current_site` is exposed from a different bit t = current_site . theme Themes . from_value? ( t ) ? Themes . from_value? ( t ). to_s : "Default" end end

Now that we have access to a current_theme method, we just need to include this mixin. In your src/actions/browser_action.cr



abstract class BrowserAction < Lucky :: Action include Themable #... macro theme_page if current_theme == "Default" {{ @type }} Page else case Themes . parse ( current_theme ) when . dark? Dark :: {{ @type }} Page end end end end

In this BrowserAction, I added a macro to help determine which page we are going to render. This will make a bit more sense in the actions.



class Dashboards::Index < BrowserAction get "/" do render ( theme_page ) end end

In this root action here, we now render either Dashboards::IndexPage or Dark::Dashboards::IndexPage . This is depending on which current_site has been loaded. As long as you're keeping up with traditional naming conventions, then this just all works.

Now it's time to setup the views portion. In our src/pages/main_layout.cr file, we'll need to update some stuff.



abstract class MainLayout include Lucky :: HTMLPage needs current_theme : String abstract def content def render html_doctype html lang: "en" do head do #... other stuff css_link ( dynamic_asset ( " #{ @current_theme . downcase } .css" )) end body do content end end end end

For this MainLayout, we are calling either default.css or dark.css . There's a ton of different ways you could handle this. Maybe you have a base.css , and then just inherit and override, or maybe you include a secondary css file? What ever you decide to do with that, just make sure your webpack mix file is updated with all these theme styles.

Another issue you may run in to here is, maybe you have completely different header or footers depending on the theme. You may need to do some additional logic here like:



body do if @current_theme == "Dark" render_dark_header end content end

This of course could be abstracted out to a module in src/components/header_component.cr



module HeaderComponent private def render_header ( theme ) case Themable :: Themes . parse ( theme ) when . dark? _dark_header end end private def _dark_header div ( class: "dark" ) do h1 ( "Dark Header" ) end end end abstract class MainLayout #... include HeaderComponent def render #... body do render_header ( @current_theme ) content end end end

Lastly, we just need the main view part! For this, we're going to use a special directory structure. We currently have src/pages/dashboards/index_page.cr , and we will just add src/pages/dashboards/dark/index_page.cr . This sort of breaks the hierarchy standard, but for me personally it makes more sense. (for now at least, I'm sure I'll hate it in 3 months lol)



class Dashboards::IndexPage < MainLayout def content #... home page content here end end class Dark::Dashboards::IndexPage < :: Dashboards :: IndexPage def content #... dark theme home page content. # AND... Bonus! previous_def # add additional stuff that's on dark theme and not default end end

Really, that's basically it. Everything else is just going to be your personal preference, or additional things you may need.

If you're reading this, and you can think of some cleaner ways, please comment below!