Lessons Learned from Writebook

37 Signals (37s) have generously released the source code to Writebook, their latest offering under the Once suite of products. There is no Github repository, but the source code is readily available. It is accepted with thanks and gratitude.

I’m going to post snippets of the code (without permission) sharing my “lessons learned”. I doubt anyone would object, seeing that 37s does similar with closed source code. Secondly, emailing and asking for permission? It would be too much overhead for them to answer, seeing that they are releasing this product without being remunerated in American paper money. But you never know, and if any objections do come (for whatever reason), I’m obliged to remove this post.

Immediately Adopt What is Useful

If you see something that is worthy of copying - then do so, immediately.

How to read their code base?

Here’s where to start:

  1. Gemfile
  2. Routes: what is the root_path?
  3. Then go straight to the tests.

Gemfile

  • Minimise dependencies: that is an extra layer of difficulty.

No factory-bot, and no rspec

The greatest regret of my life? Falling into the factory-bot paradigm. Simple and clean fixtures are all that are required. I never understood detractors cavilling at “fixtures” being brittle. How are they any more brittle than the factory methods in factory-bot?

title and open graphs

Every page has a title. Don’t forget it!

They use open graphs. I didn’t even know what this was, but as soon as I saw it, I (basically) cut and pasted, and added it in my repositories:

<!-- application.html.erb -->

  <head>    
  	...
    <%= content_for(:open_graph) %>   
    ...
  </head>

And then in the relevant page:

<% content_for :open_graph do %>
  <%= tag.meta property: "og:title", content: "Project: #{quote.id}" %>
  <%= tag.meta property: "og:description", content: truncate("#{quote.id} - #{quote.project_name} - #{quote.aasm_state} - #{quote.created_at}", :length => 80) %>
  <!-- fix the image to a client specific logo -->
  <%= tag.meta property: "og:image", content: asset_url("tek1.svg") %>
  <%= tag.meta property: "og:image:url", content: asset_url("tek1.svg") %>
  <%= tag.meta property: "og:image:secure_url", content: asset_url("tek1.svg") %>  
  <%= tag.meta property: "og:image:type", content: "image/svg+xml" %>
  <%= tag.meta property: "og:image:width", content: "396" %>
  <%= tag.meta property: "og:image:height", content: "182" %>
  <%= tag.meta property: "og:image:alt", content: "firm logo" %>
  <%= tag.meta property: "og:url", content: quote_url(quote) %>
  <%= tag.meta property: "twitter:title", content: "Project: #{quote.id}" %>
  <%= tag.meta property: "twitter:description", content: truncate("#{quote.id} - #{quote.project_name}", :length => 80) %>
  <%= tag.meta property: "twitter:image", content: asset_url("tek1.svg") %>
  <%= tag.meta property: "twitter:card", content: "summary_large_image" %>
<% end %>

For some reason, certain applications do not display svg as well as they do png files.

So I have immediately incorporated these lessons.

Implicit Concepts and knowledge

They seem to rely heavily on industry-based concepts. For example, in the publishing industry, “leaf”, “leaves” and “press” are terms of art. 37 Signal relies on these concepts when naming models. This means, once you understand those concept, it’s much easier to understand the code-base.

I do similar with my code-base. For example, if you know anything about rock bands, all I need to mention is: “Queen”, “albums” and “songs”, and “News of the World” or “Bohemian Rhapsody” and you will know exactly what I’m talking about. What if I mentioned freddie. You simply “know”: (i) what band he is in, and (ii) possibly what songs he has written, as well as his albums. I don’t have to explain anything new to you.

This aids immensely in writing and using fixtures.

New Methods

This is what 37 Signals does:

<!-- show.html.erb -->
<% content_for :header do %>
  <%= render "leaves/header", book: @book, leaf: @leaf %>
<% end %>

I had typically done the following:

<!-- show.html.erb -->
<%= render "leaves/header", book: @book, leaf: @leaf %>

<!-- leaves/header.html.erb -->
<% content_for :header do %>
  <%= render "leaves/header", book: @book, leaf: @leaf %>
<% end %>

In other words, I would render the partial, and keep content_for within the partial rather than outside of it.

Having looked at someone else’s source code, I think I prefer it such that the partial does not have the content_for method within it, because I want to see it in the show.html.erb page.

Psuedo Currying of Code

Direct from the code base:

### first_run.rb
User.create!(user_params.merge(role: :administrator)).tap do |user|
      DemoContent.create_manual(user)
end

But this is how I would typically do it:

user = User.create!(user_params.merge(role: :administrator))
DemoContent.create_manual(user) 

It’s barbaric, but it still works: it’s not immediately obvious the two lines of code are “curried” (i.e. directly connected - with the output of the first is the input to the second).

Can you improve upon the expressiveness of 37s code?

# some other options
DemoContent.create_admin_user(user_params).then do |user|
  create_manual(user)  # I don't want to call DemoContent here.
end

# but why should you reach for DemoContent, when you want to create an admin user?
# psuedo code - does this work?
User.create_admin(user_params)
|> DemoContent.create_manual


# or this?
DemoContent.create_user(:admin, user_params).then do |user| # mixin in create_user into demo content?
  create_manual(user)
end

# or using the 'then' keyword?
DemoContent.create_admin(user_params).then do |user|
  create_manual(user)
end


User.create_admin(user_params).then do |user|
  DemoContent.create_manual(user)
end

How should I choose? Choose what tastes right to you.

The use of helper methods and helper classes

The following is the first_run controller. What does it do?

  • This is the first page you see when you deploy the app.
  • The entire concept is self-explanatory, but there is no “first_runs” database table. All this controller does is create the first user, and creates the first write_book with that user as the admin.
  • Also note - that if an bad actor wanted to take control and hoist themselves as admin before you do - that is possibile. 37s have rationalised that it is not worth securing everything, and the chances of that happening are so remote, it is not worth the extra overhead to manage.
class FirstRunsController < ApplicationController
  allow_unauthenticated_access

  before_action :prevent_running_after_setup

  def show
    @user = User.new
  end

  def create
    user = FirstRun.create!(user_params)
    start_new_session_for user

    redirect_to root_url
  end

  private
    def prevent_running_after_setup
      redirect_to root_url if User.any?
    end

    def user_params
      params.require(:user).permit(:name, :email_address, :password)
    end
end


# first_run.rb
class FirstRun
  ACCOUNT_NAME = "Writebook"

  def self.create!(user_params)
    account = Account.create!(name: ACCOUNT_NAME)

    User.create!(user_params.merge(role: :administrator)).tap do |user|
      DemoContent.create_manual(user)
    end
  end
end

If you wanted to be garrulous, you could write the above like this:

class FirstRunsController < ApplicationController
  # .... etc.

  def create

    ## the following 3 lines are extracted into first_runs
    account = Account.create!(name: ACCOUNT_NAME)
    user = User.create!(user_params.merge(role: :administrator)).tap do |user|
      DemoContent.create_manual(user)
    end

    start_new_session_for user

    redirect_to root_url
  end  
end

37s seem to want to simplify concepts at a higher level, and put everything in organised drawers.

I think this works well. Immensely well, especially when you have a large code-base.

Nesting Controllers

Suppose you have:

  • pet owners, and
  • pets
# pet_ower.rb
has_many :pets


# pet.rb
belongs_to :owner

Ordinarily, where would you put the pets_controller.rb?

app/controllers/pets_controller.rb  # dump it here?

No! Don’t do that. If you nest it, then you automatically know, by convention that a pet is defined by its owner.

37s nests it:

app/controllers/owners/pets_controller.rb 

Does it make more sense to you?

# routes.rb
 resources :books, except: %i[ index show ] do
    resource :publication, controller: "books/publications", only: %i[ show edit update ]
    resource :bookmark, controller: "books/bookmarks", only: :show
end

Notice how publications and bookmarks are nested in a particular folder structure? Moreover, with zeitwerk, they will be scoped under the Books ‘namespace’.

class Books::BookmarksController < ApplicationController
  allow_unauthenticated_access

  include BookScoped

  def show
    @leaf = @book.leaves.active.find_by(id: last_read_leaf_id) if last_read_leaf_id.present?
  end

  private
    def last_read_leaf_id
      cookies["reading_progress_#{@book.id}"]
    end
end

Heavy use of Concerns

(a) Common Before Actions

How is the @book instance variable set?

# /controllers/books/bookmarks_controller.rb
class Books::BookmarksController < ApplicationController  
  # etc ...

  include BookScoped

  def show
    # how is @book set?
    @leaf = @book.leaves.active.find_by(id: last_read_leaf_id) if last_read_leaf_id.present?
  end

  # etc.
end

Look into the BookScoped concern:


module BookScoped extend ActiveSupport::Concern
  included do
    before_action :set_book
  end

  private
    def set_book
      @book = Book.accessable_or_published.find(params[:book_id])
    end    
end

How would I be doing it?

class Books::BookmarksController < ApplicationController  
  # etc ...

  before_action :set_book

  def show
    # how is @book set?
    @leaf = @book.leaves.active.find_by(id: last_read_leaf_id) if last_read_leaf_id.present?
  end

  private
    def set_book
      @book = Book.accessable_or_published.find(params[:book_id])
    end  
end

They are both doing the same thing, but one places common code away in a drawer - in a concern.

I might try out this technique and see if I like it.

… to be continued…

CSS

How does 37s manage css?

  • No tailwind css. This came as a surprise, since DHH seemed to be praising the tailwind approach in one of his key-notes.
  • No scss. Just vanilla css.
  • bem notation.

There are a variety of choices you can make when managing css: OOCSS, SMACSS, SUITCSS, Atomic, and lastly BEM. 37s seem to be using a combination of utility classes (i.e. similar to tailwind), while allowing for the changing of themes (via css variables), and using BEM notation where relevant.

This is a verbatim cut and paste from the first_runs/show.html.erb

Let us look at the css: panel shadow center margin-block-double.

This almost looks like bootstrap. What on earth is panel?

.panel {
  background-color: var(--color-bg);
  border: 1px solid var(--panel-border-color, var(--color-subtle));
  color: var(--color-ink);
  border-radius: var(--panel-border-radius, 1em);
  inline-size: var(--panel-size, 40ch);
  max-inline-size: calc(100% - var(--inline-space) * 2);
  padding: calc(var(--block-space) * 2);
  position: relative;

  @media (prefers-color-scheme: dark) {
    --panel-border-color: var(--color-subtle-dark);
  }
}

What on earth is var? It’s a CSS variable: this means you can define the --color-bg somewhere, once, and then make numerous references to it. The advantage of doing so, is that if you wish to change your --color-bg you only need make that change in one place as opposed to many. Also they make use of calc in css. They’ve completed discarded scss and are using vanilla css.

 /* colors.css */ 
--color-bg: oklch(var(--lch-white));

But then what on earth is oklch? I had never heard of such a thing before looking into the 37s source code:

(a) OKLCH is more human readable

e.g. suppose I have a shade of blue, and I need it to remain the same color, except slightly darker: how would you do this with hex, or rgb?

background:   #6ea3db; /* blue*/

/*what do I need to do to the above, in order to change it to light blue?*/

background:   #what-should-this-be?; /* light blue */

This is difficult to reason with: because hex and RGB do not allow you to simply change JUST the lightness. But with oklch it’s a piece of cake: if you want a lighter blue, simply change the “lightness value.”

background:   oklch(0.7 0.1 250);  /*blue */

/*let us change the lightness value (i.e. the 0.7 value above to higher number:  0.8)*/

background:   oklch(0.8 0.1 250);  /*this will be a lighter blue  */


oklch(from blue calc(l + 0.1) c h / 0.5); /*this will be a lighter blue, but it is calculated relatively.  */

i.e. it’s more intuitive.

(b) OKLCH supports more colors

The RGB “gamut” has x number of colors in its subset. If you want to display a color outside that “gamut” - then you will be unable to describe it if you use RBG “encoding”. Modern day displays (Apple’s devices) allow people to display colors which cannot be described if you use the RGB format, but which are possible with oklch.1

(c) HSL

Produces is another way of encoding colors, kinda similar to oklch but it produces unexpected results.

(d) The CSS Workflow is unknown

I would be very interested to understand the process by which styles are created (and organised). There are a million colors / styles to choose from, for a billion components: you could spend an entire day, determining upon the minutiae, plus the names as well. Is it derived from a template or a palette of sorts? It looks very sophisticated, compiled by a dedicated professional, with an immense amount of conceptual complexity decision-making. I cannot see it being put together by a one-man shop. I’m not sure what the answer is, but if high level styling can be made possible by one man, who doesn’t know a lot of css (like myself), then that would be a victory for the web. i.e. Active Record has enabled me to write queries, without knowing (much sql) - perhaps there needs to be the css equivalent of that.

  • (a) Reset all Styles: They reset all CSS styles with the modern-css-reset library.

  • (b) Their “core libraries”

  • colors.css
  • utilities.css
  • base.css
  • layout.css
  • Element based styles: input.css, text.css
  • and the rest of the styles seem to be focused around a particular view: e.g. books.css, or the qr_code.css.

Unfortunately, we don’t have their git commits, so we cannot discern the process by which it is all put together.

<% content_for(:title) { "Set up Writebook" } %>

<div class="panel shadow center margin-block-double <%="shake" if flash[:alert] %>">
  <%= image_tag "writebook-icon.svg", class: "product__logo center colorize--black", size: 130 %>
  <h1 class="margin-none-block-start margin-block-end-double">Writebook</h1>

  <%= form_with model: @user, url: first_run_path, class: "flex flex-column gap" do |form| %>
    <div class="flex align-center gap">
      <%= translation_button(:user_name) %>
      <label class="flex align-center gap input input--actor txt-large">
        <%= form.text_field :name, class: "input", autocomplete: "name", placeholder: "Name", autofocus: true, required: true, data: { "1p-ignore": true } %>
        <%= image_tag "person.svg", aria: { hidden: "true" }, size: 30, class: "colorize--black" %>
      </label>
    </div>

    <div class="flex align-center gap">
      <%= translation_button(:email_address) %>
      <label class="flex align-center gap input input--actor txt-large">
        <%= form.email_field :email_address, class: "input", autocomplete: "username", placeholder: "Email address", required: true %>
        <%= image_tag "email.svg", aria: { hidden: "true" }, size: 30, class: "colorize--black" %>
      </label>
    </div>

    <div class="flex align-center gap">
      <%= translation_button(:password) %>
      <label class="flex align-center gap input input--actor txt-large">
        <%= form.password_field :password, class: "input", autocomplete: "new-password", placeholder: "Password", required: true, maxlength: 72 %>
        <%= image_tag "password.svg", aria: { hidden: "true" }, size: 30, class: "colorize--black" %>
      </label>
    </div>
    <button type="submit" id="log_in" class="btn btn--reversed center">
      <%= image_tag "arrow-right.svg", aria: { hidden: true }, size: 24 %>
      <span class="for-screen-reader">Create your account</span>
    </button>
  <% end %>
</div>

… to be continued

BEM notation

Consider this:

 <span class="btn btn--placeholder placeholder-start" aria-hidden="true"></span>

   ...

<span class="btn btn--placeholder placeholder-end" aria-hidden="true"></span>

BEM - refers to block, elements and modifier notation.

  • btn is a class for a standard button. (i.e. this is the block)
  • btn--placeholder? This doesn’t sound like an “element” of a “block”, because buttons don’t really have placeholders. It probably sounds like a “modifier” e.g. like btn--danger for example, to signify a dangerous red button, which would destroy resources. Except in th is case, except it is used to specify a ‘placeholder’.
  • product__logo.

1 Take the above with a grain of salt, because I’m probably butchering the nomenclature.

first_run. They’ve marked it as a show action, but if you’re following Rails convention, it should be new action, since a new resource is being created - I have no idea why they’ve defied standard convention like this.

Written on December 2, 2024