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:
- Gemfile
- Routes: what is the root_path?
- 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 theqr_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 theblock
)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. likebtn--danger
for example, to signify a dangerousred
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.