Lesson 6: Theming and Templating Against Future Tech Debt.

Post 6 in the 6 Things We Learned Revamping Ruby Central's Conference Websites series

Jonathan Greenberg
By Jonathan Greenberg
November 15, 2022

This is the sixth and final post in a series on things we explored while “Revamping the Ruby Central Conference Websites”.

The art of programming can be seen as a complex balancing act between managing technical debt from the past with immediate feature requests in the present while also juggling possible requirements for the future. When working on a new story it can be helpful to add a bit of flexibility and extensibility into the architecture of your code if it doesn’t cost too much in time, effort and complexity. If executed wisely you can hopefully save yourself some tech debt for the future though you have to be careful about getting stuck in a YAGNI situation and instead create more cruft and confusion for future developers to wrestle with.

One of the main feature requests from Ruby Central for the revamping of the conference website workflow was to streamline the process for creating a website so that they would not need to be starting from scratch every year. We decided that meant creating a default theme that could be used from year to year with a bit of customization for some variability in colors and fonts.

However, we were concerned that at some point a fresh look might be desired so we tried to build out a structure that would leave room for that option in the future. The result was adding a theme field on the Website model which for now is set to default and not even exposed in the UI. A set of folders were then added to match the theme:

app
├── assets
│   └── stylesheets
│       └── themes
│           └── default
│               ├── application.scss
│               ├── colors.scss
│               ├── ....
│   └── views
│       ├── layouts
│           ├── default
│               ├── _header.html.haml
│               ├── _footer.html.haml
│           └── default.html.haml
│       └── staff
│           └── pages
│               └── themes
│                   └── default
│                       ├── home.html.haml
│                       ├── splash.html.haml
│                       ├── ....

You get the idea. This helps explain something you might have noticed in the last post in this series inside the PagesController:

  def show
    @body = @page.published_body
    render layout: "themes/#{current_website.theme}"
  end

The /staff/pages/themes/default folder drives a helpful backend templating feature which allows a website organizer to quickly generate a new page from a template. In app/views/staff/pages/new.html.haml we provide a dropdown for seeding a page from a template:

.row
  .col-md-12
    .page-header.clearfix
      %h1
        New Website Page
        = link_to_docs("page-content-management")
      = simple_form_for(:page, url: new_event_staff_page_path, method: :get) do |f|
        = f.input(:template,
          label: "Start from scratch or choose a template below #{link_to_docs("page-templates")}"
.html_safe,
          required: false,
          collection: Page::TEMPLATES.keys,
          include_blank: true,
          input_html: { onchange: "this.form.submit()" })

and then in the Staff::PagesController we build the page:

class Staff::PagesController
  ...
  def build_page
    if template = params[:page] && page_params[:template].presence
      Page.from_template(
        template,
        unpublished_body: render_to_string(
          "staff/pages/themes/#{current_website.theme}/#{template}",
          layout: false
        ),
        website: current_website
      )
    else
      current_website.pages.build
    end
  end
end

This gives a nice little boost to initializing a new page that can still be modified before creation. The template injects some basic website details into a tailwind styled design. Here is the app/views/staff/pages/themes/default/splash/html.erb template:

<div class="flex relative h-screen" background-image-style-url>
  <div class="flex flex-col justify-between z-10 bg-black bg-opacity-80 text-white p-8 w-full h-full">
    <div class="flex justify-between flex-col sm:flex-row">
      <div class="flex flex-col mb-2">
        <p>Want to speak at <%= current_website.name %></p>
        <p>
          <%= link_to(
            "Call for Proposals",
            event_url(current_website.event),
            class: "underline"
          ) %>
          are open from now until <%= current_website.closes_at %>!
        </p>
      </div>
      <div class="flex flex-col sm:text-right">
        <p>Interested in sponsoring <%= current_website.name %>?</p>
        <p>
          Check out our
          <%= link_to(
            "Sponsorship Prospectus",
            current_website.prospectus_link,
            class: "underline"
          ) %>
        </p>
      </div>
    </div>
    <div class="flex flex-col items-start w-full my-4 sm:items-center">
      <logo-image width=175></logo-image>
      <div class="text-2xl font-semibold mt-4"><%= current_website.name %></div>
      <div class="text-xl font-semibold"><%= current_website.city %></div>
      <div class="text-2xl font-semibold mt-4 mb-2"><%= current_website.date_range %></div>
      <div class="text-left">
        <%= current_website.formatted_location %>
        <%= link_to("directions", current_website.directions, class: "mt-2 underline") %>
      </div>
    </div>
    <div class="flex justify-between flex-col sm:flex-row">
      <%= link_to(
        "@#{current_website.twitter_handle}",
        "https://twitter.com/#{current_website.twitter_handle}",
        class: "underline mb-2"
      ) %>
      <%= mail_to(
        current_website.contact_email,
        current_website.contact_email,
        class: "underline"
      ) %>
    </div>
  </div>
</div>

New Page From Template

The footer, headers and general layout are somewhat fixed with the theme but it is still possible to customize the colors with css variables and you can also upload fonts which can be then used as the primary or secondary fonts. We use a similar pattern for saving font files for the website using a nested attributes form as discussed in the “Tailwind Just In Time” blog post.

-# app/views/staff/websites/_form.html.haml
%div{"data-controller": "nested-form", class: "nested-fonts"}
  =legend_with_docs("Fonts")
  %template{"data-nested-form-target": "template"}
    = f.simple_fields_for :fonts,
      Website::Font.new,
      child_index: 'NEW_RECORD' do |font|
      = render 'font_fields', form: font
  = f.simple_fields_for :fonts do |font|
    = render 'font_fields', form: font
  %div{"data-nested-form-target": "links"}
    = link_to "Add Font", "#", class: 'btn btn-success',
      data: { action: "click->nested-form#add_association" }

 -# app/views/staff/websites/_font_fields.html.haml
= content_tag :div,
  class: 'inline-form nested-fields',
  data: { new_record: form.object.new_record? } do
  = form.input :name
  = form.input :file, label: font_file_label(form.object)
  .form-group
    = form.label 'Primary'
    %div
      = form.check_box :primary,
        class: 'primary-font',
        data: { action: "click->nested-form#radio_chosen" }
  .form-group
    = form.label 'Secondary'
    %div
      = form.check_box :secondary,
        class: 'secondary-font',
        data: { action: "click->nested-form#radio_chosen" }
  = link_to "Remove", "#", data: { action: "click->nested-form#remove_association" }
  = form.hidden_field :_destroy

Then in the layout they get included in the head:

-# app/views/layouts/themes/default.html.haml
:css
  #{current_website.font_faces_css}
  #{current_website.font_root_css}
class WebsiteDecorator < ApplicationDecorator
  ...
  def font_faces_css
    fonts.map do |font|
      <<~CSS
        @font-face {
          font-family: "#{font.name}";
          src: url('#{h.rails_storage_proxy_path(font.file)}');
        }
      CSS
    end.join("\n").html_safe
  end

  def font_root_css
    font_primary = fonts.primary.first
    font_secondary = fonts.secondary.first

    return "" unless font_primary || font_secondary
    <<~CSS.html_safe
      :root {
        #{"--sans-serif-font: '#{font_primary.name}' !important;" if font_primary}
        #{"--secondary-body-font: '#{font_secondary.name}' !important;" if font_secondary}
      }
    CSS
  end
end

Thanks to the magic of Tailwind arbitrary values a font can even make a cameo appearance anytime on a website page:

Custom Font

We hope that the new set of tools that we have provided Ruby Central will give event organizers just enough structure and freedom to create some great conference websites for many years to come. Since the whole cfp-app codebase is public we hope that you will fork it, check it out and maybe even try it out for the next conference you might be hosting. Issues, comments and pull requests are always welcome and perhaps you can also help make the next Ruby or Rails conference an even greater experience for yourself and the community.

If you’re looking for a team to help you discover the right thing to build and help you build it, get in touch.