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. But you never know, and if any do come, for whatever reason, I’d remove the 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…

Written on December 2, 2024