Configuring ActiveStorage for DigitalOcean Spaces
Joy of Rails uses ActiveStorage to manage image attachments, such as the screenshots for the snippets. Images are stored in the S3-compatible DigitalOcean Spaces service. Configuration is as simple as providing credentials in config/storage.yml
as shown here.
digitalocean:
service: S3
access_key_id: <%= Rails.application.credentials.dig(:digitalocean, :access_key) %>
secret_access_key: <%= Rails.application.credentials.dig(:digitalocean, :secret) %>
bucket: <%= Rails.application.credentials.dig(:digitalocean, :bucket) %>
endpoint: https://nyc3.digitaloceanspaces.com
region: us-east-1
Phlex is fun
I love Ruby so much that I write my HTML in Ruby. I’ve got nothing against ERb, but after a year of writing Phlex components, I’m not looking back.
render CodeBlock::Body.new(data: {controller: "snippet-editor"}) do
div(class: "grid-stack") do
render CodeBlock::Code.new(source, language: language, data: {snippet_editor_target: "source"}) do
label(class: "sr-only", for: "snippet[source]") { "Source" }
div(class: "code-editor autogrow-wrapper") do
textarea(
name: "snippet[source]",
data: {snippet_editor_target: "textarea"}
) { source }
end
end
end
end
Ruby’s backslash line continuation
It’s such a simple thing, but I love that Ruby treats the backslash character as a line continuation, similar to a shell.
I often use it to omit parens and group keyword arguments on new line.
def self.from(sitepress_resource)
Resource.new \
request_path: sitepress_resource.request_path,
body: sitepress_resource.body,
title: sitepress_resource.data.title,
description: sitepress_resource.data.description,
image: sitepress_resource.data.image,
# ...
Browsers are really good at parsing
I like organize my Tailwind classes by function with line breaks and tabs. I find it easier to read for excessively long class names.
But I find ERB formatting support in VS Code a bit lacking at the moment.
<div
id="<%= dom_id(question, :body) %>"
class="
question__body
flex items-start flex-col
space-col-4 grid-cols-12
md:items-center md:flex-row md:space-row-4
"><!-- -->
ActiveRecord::Relation #with and #excluding
The related pages feature on Joy of Rails queries on the pages
and page_embeddings
tables in the database. The query is backed two relatively new features in ActiveRecord: ActiveRecord::Relation#excluding
and the yet-to-be-documented (as far as I know) ActiveRecord::Relation#with
for representing Common Table Expressions (CTE).
select("pages.*", "similar_embeddings.distance")
.with(similar_embeddings: PageEmbedding.similar_to(page))
.excluding(page)
.order(distance: :asc)
A snippet() snippet
snippet()
is a SQLite function that returns a snippet of text containing the search term. It’s used to highlight search results. This example shows an ActiveRecord scope that enables selection additional fields title_snippet
and body_snippet
from an FTS5 virtual table (created separately).
https://www.sqlite.org/fts5.html#the_snippet_function
scope :with_snippets do
select("pages.*")
.select("snippet(pages_search_index, 0, '<mark>', '</mark>', '…', 32) AS title_snippet")
.select("snippet(pages_search_index, 1, '<mark>', '</mark>', '…', 32) AS body_snippet")
end
app/models/markdown.rb
This example demonstrates how you could parse a Commonmarker markdown document into an abstract syntax tree to customize the output. Each node
has a type
to switch on behavior and an each
method for iterating over its direct children.
require "commonmarker"
def visit(node)
return if node.nil?
case node.type
in :document
visit_children(node)
in :heading
header(node.header_level) { visit_children(node) }
in :paragraph
p { visit_children(node) }
# ...
end
end
def visit_children(node)
node.each { |c| visit(c) }
end
visit(Commonmarker.parse("# Hello World\n\nHappy Friday!"))
config/initializers/vapid.rb
The X-Factor of Rails configuration
Use the Rails config.x
object to add organize custom configuration into namespaces.
Rails.application.configure do
config.x.vapid.public_key = credentials.dig(:vapid, :public_key)
config.x.vapid.private_key = credentials.dig(:vapid, :private_key)
end
footer.css
CSS :has() Example
Hey Tailwind enthusiasts, don‘t sleep on CSS. It‘s getting really good. Modern browsers now support features like custom variables, new pseudo selectors (e.g. :has(), :is()), and CSS nesting. You can do some sweet styling without need of a preprocessor like SASS. Here‘s a snippet from the footer nav on https://www.joyofrails.com.
nav li:has(+ li) {
&:after {
margin: 0 var(--space-xs);
content: ' / ';
}
}
Markdown ASTs with Commonmarker
For converting Markdown, Rubyists may default to Redcarpet or Kramdown. I suggest taking a look at Commonmarker, a Ruby gem built on top of comrak, a Rust of implementation the GitHub-flavored version of the CommonMark spec.
Why? One reason is you can parse Markdown into an abstract syntax tree (AST) which enables powerful customization of what you can do with the markdown source.
require 'commonmarker'
Commonmarker.to_html('"Hi *there*"', options: {
parse: { smart: true }
})
# => <p>“Hi <em>there</em>”</p>\n
doc = Commonmarker.parse("*Hello* world", options: {
parse: { smart: true }
})
doc.walk do |node|
puts node.type # => [:document, :paragraph, :emph, :text, :text]
end
app/views/users/newsletter_subscriptions/_banner.html.erb
Phlex and ERB play nice together!
If you’re curious about Phlex but unsure of whether you want to go "all in", you can start small. You can render a Phlex component in ERB and you can yield back to the ERB template from Phlex. You can adopt Phlex gradually.
This code snippet displays an ERB template that renders a Phlex component called Users::NewsletterSubscriptions::Banner
. The first render call yields to a block that in turns renders another ERB partial called "users/newsletter_subscriptions/form"
. These features help demonstrate Phlex’s compatibility with ERB.
<%= render Users::NewsletterSubscriptions::Banner.new do %>
<%= render "users/newsletter_subscriptions/form" %>
<% end %>
Vite on Rails
An open secret about Rails and JavaScript: lots of apps are using Vite to bundle frontend assets via the excellent Rails integration provided by Vite Ruby. You may want to give it a try. #rails #vite
bundle add vite_rails
bundle exec vite install
config/database.yml
I‘ve mentioned previously that Joy of Rails uses SQLite along with Rails support for multiple databases. Here's a snapshot of what the Joy of Rails config/database.yml
file looks like in development
. I’ve separated the database for each of the Rails "backends" running on SQLite, including ActiveRecord (primary), Rails cache, Solid Queue job queue, and Action Cable. Each simply maps to a different file on disk.
default: &default
adapter: sqlite3
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 3 } %>
timeout: 5000
development:
primary:
<<: *default
database: storage/development/data.sqlite3
cache:
<<: *default
database: storage/development/cache.sqlite3
migrations_paths: db/migrate_cache
queue:
<<: *default
database: storage/development/queue.sqlite3
migrations_paths: db/migrate_queue
cable:
<<: *default
database: storage/development/cable.sqlite3
migrations_paths: db/migrate_cable
ApplicationController.render(
inline: article.body,
type: :"mdrb-atom",
layout: false,
content_type: "application/atom+xml",
assigns: {
format: :atom
}
)
app/javascript/controllers/screenshot.js
import { Controller } from '@hotwired/stimulus';
import * as htmlToImage from 'html-to-image';
export default class extends Controller {
connect() {
this.element.addEventListener(
'turbo:before-fetch-request',
this.prepareScreenshot,
);
}
prepareScreenshot = async (event) => {
event.preventDefault();
if (event.detail.fetchOptions.body instanceof URLSearchParams) {
const data = await htmlToImage.toPng(this.snippetTarget);
event.detail.fetchOptions.body.append('screenshot', data);
}
event.detail.resume();
};
}