# Andrew Mason
> Andrew Mason is a Ruby on Rails engineer at Podia and co-host of the Remote Ruby podcast, writing about Rails, Ruby tooling, CI, and open source.
Ruby on Rails engineer, podcaster, and Remote Ruby co-host based in Phoenix, AZ.
## Explore
- [Writing](https://andrewm.codes/posts/)
- [Projects](https://andrewm.codes/projects/)
- [Speaking](https://andrewm.codes/speaking/)
- [Uses](https://andrewm.codes/uses.md)
- [About](https://andrewm.codes/about.md)
## Selected writing
- [Automating Ruby Gem Releases with GitHub Actions](https://andrewm.codes/p/automating-ruby-gem-releases-with-github-actions.md): Automate Ruby gem releases with GitHub Actions and Release Please: conventional-commit versioning, changelog generation, and publishing to RubyGems.
- [Instantly speed up your Rails application by self-hosting your fonts](https://andrewm.codes/p/instantly-speed-up-your-rails-application-by-self-hosting-your-fonts.md): Improve Rails page speed by self-hosting web fonts instead of relying on third-party font CDNs.
- [Rails Coverage Tools: Coverband](https://andrewm.codes/p/rails-coverage-tools-coverband.md): Add Coverband to a Rails application to measure production code usage and find unused Ruby code, gems, and views.
- [A11Y in Rails: Automated Linting with AccessLintπ](https://andrewm.codes/p/a11y-in-rails-automated-linting-with-accesslint.md): Add automated accessibility (a11y) linting to a Ruby on Rails app with AccessLint, which flags issues on every pull request before they ship.
- [CI for Ruby on Rails: CircleCI](https://andrewm.codes/p/ci-for-ruby-on-rails-circleci.md): Part two of a CI for Ruby on Rails series: build a complete CircleCI pipeline with Docker images, caching, database setup, tests, and linters.
---
# Adding Webmentions to a Bridgetown Site
> How I added webmentions to my static Bridgetown site using webmention.io, Bridgy, and a nightly GitHub Actions job, so replies and likes from Bluesky show up right under my posts.
[Andrea Fomera](https://afomera.dev) replied to [one of my posts](https://bsky.app/profile/afomera.dev/post/3mvbzefpqzs2t) recently, and it showed up right underneath it:

> good to see you blerging again
No comment system. No third-party embed. No JavaScript widget loading in the corner of the page. Andrea wrote that reply somewhere else entirely, and it landed on my site as plain text that now lives in [my Git repo](https://github.com/andrewmcodes/andrewm.codes).
That's a webmention, and I finally set them up on this site.
Here's how it all fits together.
## What even is a webmention?
A [webmention](https://www.w3.org/TR/webmention/) is a small notification. When someone links to one of your pages from their own site, their site sends a tiny POST to yours with two URLs: the page doing the mentioning (`source`) and the page being mentioned (`target`). In effect: "hey, I mentioned you over here." Your site gets to decide what to do with that: ignore it, count it, or render it as a reply.
It's a [W3C Recommendation](https://www.w3.org/TR/webmention/) and a core piece of the [IndieWeb](https://indieweb.org/Webmention). Think of it as the open, decentralized version of "3 people liked this," except the likes and replies come from all over the web instead of from within the walled garden of a single social media site.
Great in theory. There are two problems for a site like mine.
First, my site is completely static. It's a [Bridgetown](https://www.bridgetownrb.com) build deployed to Cloudflare as static files, so there's no server sitting there ready to receive a POST.
Second, almost nobody writes replies on their own blog anymore. They reply on Bluesky. They like the post on Mastodon. The conversation happens on social, not on personal sites.
Both come down to the same fix: put hosted services at the boundary and keep my own site completely static.
## The moving parts
Before the step-by-step, here's the shape of the whole thing, because it only clicked for me once I could see all the pieces at once:
- **[webmention.io](https://webmention.io)** receives mentions on my behalf, since my static site can't.
- **[Bridgy](https://brid.gy)** watches my social accounts and turns likes, reposts, and replies into webmentions.
- **A [GitHub Actions job](https://github.com/andrewmcodes/andrewm.codes/blob/main/.github/workflows/webmentions.yml)** pulls the collected mentions into a JSON file in my repo once a day.
- **A [Bridgetown component](https://www.bridgetownrb.com/docs/components/ruby)** reads that JSON and renders it under each post at build time.
Nothing runs on a server of mine, because I don't have one. Every part is either a hosted service or a build-time step. That constraint shaped every decision below.
## Step 1: Tell the world where to send mentions
First, point people at an endpoint that can actually receive a webmention. [webmention.io](https://webmention.io), built by [Aaron Parecki](https://aaronparecki.com), is a hosted endpoint that does exactly this.
Add the discovery link tags to the `
` of your layout. Mine live in [`src/_layouts/default.erb`](https://github.com/andrewmcodes/andrewm.codes/blob/main/src/_layouts/default.erb):
```erb
```
Any sender that finds these tags now knows where to deliver mentions for my domain.
You sign in to webmention.io with your own domain using [IndieAuth](https://indieweb.org/IndieAuth). For that to work, your site needs to expose `rel="me"` links pointing at the profiles that make up your identity. So while we're in the ``, add those too:
```erb
```
## Step 2: Backfeed from social with Bridgy
An endpoint that receives webmentions is only useful if anyone is sending them. That's where [Ryan Barrett's](https://snarfed.org) [Bridgy](https://brid.gy) comes in.
Bridgy watches the social posts tied to your site and turns replies, likes, and reposts on them into webmentions sent back to your endpoint. This is called backfeed, and it's the bridge between "the conversation happens on Bluesky" and "the conversation shows up on my site."
Connect your accounts in Bridgy, and it takes care of the polling. That Bluesky reply from the screenshot at the top? Bridgy saw it and forwarded it to webmention.io, which is how it reached me at all.
Note: which networks Bridgy supports changes over time. It works well with Bluesky and the fediverse, and it used to cover Twitter, but that stopped once Twitter became X and locked down its API. Check [Bridgy](https://brid.gy) for what's currently supported before you count on a given account.
## Step 3: Pull the mentions into the repo
At this point webmention.io is collecting mentions, but my static build has no idea they exist. I need to pull them in.
webmention.io exposes a per-domain [JF2](https://www.w3.org/TR/jf2/) feed. This script pages through the feed, collects every entry, and writes them back out as a JF2 feed of its own in `src/_data/webmentions.json`, which Bridgetown then exposes as `site.data.webmentions`:
```ruby
# scripts/fetch-webmentions.rb
require "json"
require "net/http"
require "uri"
DOMAIN = "andrewm.codes"
PER_PAGE = 200
OUT = File.expand_path("../src/_data/webmentions.json", __dir__)
token = ENV["WEBMENTION_IO_TOKEN"]
if token.nil? || token.empty?
warn "WEBMENTION_IO_TOKEN not set; skipping fetch, leaving src/_data/webmentions.json unchanged."
exit 0
end
# Page through the JF2 feed until a short page signals the end.
def fetch_all(token)
children = []
page = 0
loop do
uri = URI("https://webmention.io/api/mentions.jf2")
uri.query = URI.encode_www_form(
"domain" => DOMAIN,
"token" => token,
"per-page" => PER_PAGE,
"page" => page
)
response = Net::HTTP.get_response(uri)
unless response.is_a?(Net::HTTPSuccess)
raise "webmention.io responded #{response.code} #{response.message}"
end
batch = JSON.parse(response.body)["children"] || []
children.concat(batch)
break if batch.length < PER_PAGE
page += 1
end
children
end
children = fetch_all(token)
# Newest first so the component can take the most recent without re-sorting.
children.sort_by! { |wm| wm["wm-received"].to_s }
children.reverse!
feed = {"type" => "feed", "name" => "Webmentions", "children" => children}
File.write(OUT, JSON.pretty_generate(feed) + "\n")
puts "Wrote #{children.length} webmentions to #{OUT}"
```
That `{ type, name, children }` shape is why the component reads `site.data.webmentions.children` later on. The full file, minus the header comment I trimmed here, is [`scripts/fetch-webmentions.rb`](https://github.com/andrewmcodes/andrewm.codes/blob/main/scripts/fetch-webmentions.rb). It leans entirely on Ruby's standard library, so there are no gems to install.
A single post's mentions can be fetched publicly, but pulling every mention for the whole domain needs the account API token, which I keep out of the repo. Locally it comes from my Keychain via [`fnox`](https://fnox.jdx.dev), and in CI it's a repo secret.
`fnox` is from [Jeff Dickey](https://jdx.dev), the same person behind [`mise`](https://mise.jdx.dev), which I use to manage tool versions and tasks all over this repo.
> BTW, we [interviewed Jeff on Remote Ruby](https://www.remoteruby.com/2260490/episodes/18785026-jeff-dickey-on-mise-precompiled-rubies-and-much-more) if you want to hear more about his projects like mise and fnox.
Note: if the token is missing, the script logs a warning and exits `0` without touching the committed file. That was deliberate. I never want a missing secret to break a build. A build with slightly stale webmentions is fine. A build that fails is not.
I run it locally with a `mise` task:
```sh
mise run webmentions
```
There's a deliberate choice hiding in this step. I could fetch webmention.io during every Bridgetown build instead, but I put Git in the middle on purpose. Once a mention is fetched, it's committed data in the repo, versioned right alongside the post it belongs to. Builds stay reproducible, I get a history of exactly what changed and when, and webmention.io doesn't have to be reachable for me to deploy. This is the step where the durable copy of a mention stops living on someone else's server and starts living in mine.
## Step 4: Refresh daily with GitHub Actions
I don't want to remember to run that script, so a scheduled GitHub Action does it for me once a day:
```yaml
# .github/workflows/webmentions.yml
name: Sync webmentions
on:
schedule: [{ cron: "0 5 * * *" }]
workflow_dispatch:
permissions:
contents: write
jobs:
sync:
runs-on: ubuntu-latest
outputs:
changed: ${{ steps.commit.outputs.changed }}
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/setup
with:
node: false
- name: Fetch webmentions from webmention.io
env:
WEBMENTION_IO_TOKEN: ${{ secrets.WEBMENTION_IO_TOKEN }}
run: ruby scripts/fetch-webmentions.rb
- uses: ./.github/actions/commit-data
id: commit
with:
paths: src/_data/webmentions.json
message: "chore(data): refresh webmentions"
```
It fetches, then commits `webmentions.json` only if it actually changed. The rest of [the workflow](https://github.com/andrewmcodes/andrewm.codes/blob/main/.github/workflows/webmentions.yml), which I've left out of the snippet, gates its deploy job on that same `changed` output, so the site only rebuilds when there's something new to show.
Note: the `workflow_dispatch` trigger is there so I can click "Run workflow" and pull mentions on demand without waiting for the next scheduled run. Handy right after someone tells you they replied to something.
## Step 5: Render them under the post
Now for the part that actually shows up on the page. I built a [`Webmentions` component](https://github.com/andrewmcodes/andrewm.codes/blob/main/src/_components/webmentions.rb), a [`Bridgetown::Component`](https://www.bridgetownrb.com/docs/components/ruby), that reads the data file and filters it down to the mentions for the current post.
The matching is simple. Every entry in the feed has a `wm-target`, and I keep the ones whose target is this post's absolute URL:
```ruby
def target
"#{@site.metadata.url}#{url}"
end
def mentions
@mentions ||= begin
children = @site.data.webmentions&.children
Array(children).select { |wm| wm["wm-target"] == target }
end
end
```
From there, the component groups mentions by their `wm-property`. Likes, reposts, and bookmarks become a tally in the header, and reply-type entries become the thread you see below it.
One decision I want to call out, because it matters more than it looks. When I render a reply, I use the plain-text content, never the HTML:
```ruby
def content_text(wm)
text = wm.dig("content", "text")
return nil if text.nil? || text.strip.empty?
(text.length > 280) ? "#{text[0, 279].rstrip}β¦" : text
end
```
This content comes from strangers on the internet. I could sanitize the HTML webmention.io hands me, but I don't want any of its formatting in the first place, so the simplest safe move is to not trust it at all. I render the plain-text version instead.
The 280-character cap in there is my own display choice, not something the protocol requires. I just want imported replies to stay lightweight underneath the post.
Wiring it into the [post layout](https://github.com/andrewmcodes/andrewm.codes/blob/main/src/_layouts/post.erb) is one line, passing the post's URL and its `syndication` front matter:
```erb
<%= render Webmentions.new(url: resource.relative_url, syndication: resource.data.syndication) %>
```
That `syndication:` bit is a small IndieWeb nicety. It's an array of the places I cross-posted the article to, and the component renders a "syndicated to" row of links (with the RSS feed always added at the end):
```yaml
syndication:
- https://bsky.app/profile/andrewm.codes/post/abc123
```
## The result
Put it all together and you get the block from the top of this post: a "webmentions" heading, a tally when there are likes or reposts, the replies themselves, and a small form for anyone who wants to send a webmention by hand.
The replies read like comments. They just don't live in a comment system. They live in a JSON file in my repo, gathered from wherever the conversation actually happened.
## Final Thoughts
I like this setup more than any comment system I've run before, and the reason is ownership.
**Every reply on my site is plain JSON in my Git repo now, not a row in someone else's database.** If webmention.io or Bridgy disappeared tomorrow, the pipeline would stop collecting anything new, but nothing I've already gathered would vanish. My site would keep building with exactly the data it has today. That's the whole IndieWeb pitch in one sentence, and it's the same reason I keep reaching for static sites in the first place.
Owning the data has a quieter benefit too: moderation. I haven't had anyone reply with something heinous yet, and I'd rather not dare a reader to be the first, but if it happened I get the final say over what renders. I can filter or skip any mention I don't want on the page, and I can delete it outright from the [webmention.io dashboard](https://webmention.io/dashboard).
It's also not much code. A fetch script, a scheduled Action, and one component. Most of the heavy lifting is done by two hosted services maintained by people who have been doing this far longer than I have, so real credit to Aaron Parecki and Ryan Barrett for webmention.io and Bridgy.
If you run a static site and you've been missing having a conversation on it, give webmentions a try. I'd love it if the first one you get is a reply to this post.
Happy coding!
---
# Exporting my Pieces code snippets to Obsidian
> After 358 code snippets, I exported everything out of Pieces into plain Markdown I own in Obsidian. Here's the Ruby script, and why owning your data matters.
I opened Pieces today and was greeted with this message:
> After Sunday, August 16, 2026 at 11:59:59 PM UTC, creating new memories, generating workstream summaries, and using Agentic Chats will require a paid plan. You can still read, browse, and query your existing memories and chats in Pieces and through MCP. You do not need a paid plan for that access. As of August 10, 2026, your Pieces history includes 200K+ memories across 20 months. Thank you for making Pieces part of your work. We don't take that trust, or this change, lightly. Long-term context and Agentic Chats have ongoing infrastructure costs. Paid plans let us keep those services reliable and continue improving Pieces. We understand that paying for capabilities that were free may be disappointing.
I've been using Pieces since they first released it, and I've really enjoyed having it in my workflow. Over the years, they've added a ton of great features, but I never really changed how I used it: for me, Pieces was always primarily a place to save code snippets I wanted to reference later.
I completely understand their move to a paid model, and I hope it works out well for them. If you've never tried it, you should. I just don't use enough of its newer functionality to justify another AI subscription.
There was just one problem.
I had **358 code snippets** saved in Pieces, some dating back almost three years, and I didn't want uninstalling the app to mean losing them.
Since I already keep the rest of my notes in Obsidian, Markdown seemed like the obvious destination.
## Finding the Data
My first thought was to use the Pieces CLI.
Unfortunately, while the CLI can list materials, it doesn't currently provide a convenient machine-readable bulk export command.
PiecesOS was already running locally on my Mac, though, and in my case it was listening on port `39300`.
It turns out I could grab all of my saved assets directly:
```bash
curl -fsS http://localhost:39300/assets -o ./tmp/pieces-assets.json
```
That gave me a single JSON snapshot containing all **358** of my saved materials.
At this point I technically had a backup, which was the most important part. Even if the rest of the migration went terribly wrong, I now had the original data outside of Pieces.
## Understanding the Export
The JSON was a little more complicated than I expected.
Most snippets stored their original contents here:
```text
original.reference.fragment.string.raw
```
But one of my 358 snippets stored its contents as an array of bytes instead:
```text
original.reference.file.bytes.raw
```
So the migration script needed to support both formats.
Before writing anything to my Obsidian vault, I spent some time validating the export rather than assuming the API response was consistent.
A few things I found:
- All 358 assets had unique Pieces IDs.
- All 358 snippets were recoverable.
- All snippet contents were valid UTF-8.
- 357 snippets stored their contents as strings.
- 1 snippet stored its contents as bytes.
- 65 snippets ended with a newline.
- 293 did not.
- 40 snippets contained triple backticks.
- Several snippets shared the same title.
- Some snippets had completely identical contents but different metadata.
I decided **not to deduplicate anything**.
If Pieces thought I had 358 saved materials, I wanted 358 Markdown files when the migration finished.
## Pieces Metadata Was... Interesting
Pieces stores quite a bit more than the snippet itself.
There were descriptions, tags, websites, classifications, commit messages, timestamps, and other generated metadata attached to the assets.
Initially I thought I would preserve most of that in the resulting Markdown files.
After actually inspecting it, I changed my mind.
For example, Pieces had generated descriptions like:
> The code snippet below calculates the factorial of a given number using recursion.
For a snippet named `Database Query Optimization Strategies`.
Other descriptions were truncated, inaccurate, or clearly generated from unrelated context.
Tags had similar problems. There were over 6,000 tag relationships, but almost all of them had been generated automatically.
Some examples included things like:
```text
Framework: Sorry, but I can't generate a response based on the given code snippet
```
and:
```text
Pages related to {{value:Link}}...
```
That is not metadata I particularly want permanently attached to my Obsidian notes.
So I ended up being fairly conservative about what made it into the human-readable files.
## What I Kept
Each generated note contains a small amount of frontmatter:
```yaml
---
type: snippet
source: "[[Pieces.app]]"
language: shell
created_on: 2023-11-21
updated_on: 2023-11-21
---
```
If I had manually added tags in Pieces, those are included too:
```yaml
---
type: snippet
source: "[[Pieces.app]]"
language: javascript
created_on: 2024-09-19
updated_on: 2024-09-19
tags:
- obsidian
- quickadd
---
```
Only 7 tags across 5 snippets had actually been added manually.
I also kept a `source_url` when Pieces had a trustworthy original source URL.
There were quite a few website relationships in the export, but most were automatically generated "Relevant Site" links. Some were Google searches, GitHub tag pages, temporary Raindrop cache URLs, or even JavaScript chunks from websites.
After filtering those out, only **20 of the 358 snippets** had a source URL that I felt was worth including.
## What I Didn't Keep
I deliberately left these out of the Markdown files:
- Pieces IDs
- generated descriptions
- generated tags
- generated website associations
- generated Git commit messages
- other Pieces-specific enrichment
I still preserve all of this information in the raw JSON export and a normalized JSONL archive.
That means nothing is actually lost, but my Obsidian notes don't inherit years of questionable generated metadata.
There was one exception.
One snippet had an explanation I had manually written inside Pieces:
> The picker template when run will check if the note you've created was created in the default location...
That was clearly useful context I had authored myself, so the exporter keeps manual explanations in an optional `## Explanation` section.
## Fixing Language Detection
Pieces also wasn't always correct about programming languages.
For example, this:
```bash
mdfind kMDItemAppStoreHasReceipt=1
```
was classified as `BatchFile`.
That might technically fall into Pieces' idea of command-line code, but it definitely isn't a Windows batch file.
Since I want Obsidian's syntax highlighting to work correctly, the migration script includes a small correction layer.
If Pieces reports `BatchFile` but the contents contain obvious Unix shell commands or syntax such as `brew`, `git`, `curl`, `grep`, `mdfind`, environment variables, pipes, or shell conditionals, I treat it as shell instead. I did the same for GraphQL, AppleScript, and a few other languages.
There were also 13 particularly strange classifications that were easier to correct explicitly after inspecting them.
## Handling Markdown Code Fences
This was another edge case I almost missed.
Forty of my snippets contained triple backticks themselves.
If I blindly generated this:
````markdown
```markdown
some markdown containing ```
```
````
I could accidentally terminate the outer code block.
Instead, the exporter determines the longest consecutive run of backticks inside each snippet and uses a fence at least one character longer.
So a snippet containing:
````markdown
Some text.
```markdown
# Example
```
````
would be wrapped in:
`````markdown
````
Some text.
```markdown
# Example
```
````
`````
It is a small detail, but exactly the kind of thing that can silently corrupt a bulk Markdown migration.
## Duplicate Titles
Pieces allows multiple snippets to have the same name.
I had 16 groups of duplicate names, including:
- `Rails routes fuzzy finder`
- `Git file search and run Ruby test`
- `Generate Obsidian Plugin Metadata Files`
- `Remove processed tag from active Markdown file`
For unique titles, I use the title directly:
```text
Find All Apps Installed from App Store.md
```
If multiple snippets resolve to the same filename, **all** members of the collision group get part of their Pieces ID appended:
```text
Rails routes fuzzy finder--fa3b8c22.md
Rails routes fuzzy finder--7db5e524.md
```
I didn't want the filename chosen for one duplicate to depend on whichever asset happened to appear first in the JSON.
The filenames should be deterministic every time the exporter runs.
## The Result
A generated note looks roughly like this:
````bash
---
type: snippet
source: "[[Pieces.app]]"
language: shell
created_on: 2023-11-21
updated_on: 2023-11-21
---
## Code
```bash
mdfind kMDItemAppStoreHasReceipt=1
```
````
Pretty boring.
That's exactly what I wanted.
My snippets are now plain Markdown files that I can search, edit, link, version with Git, process with Ruby, query from Obsidian, or move somewhere else entirely later.
There is no special application required to read them.
## Keeping the Original Export
I didn't want the conversion to Markdown to become the new source of truth immediately, so the exporter produces a few different artifacts:
```text
pieces-export/
βββ raw/
β βββ assets.json
βββ normalized/
β βββ assets.jsonl
βββ markdown/
β βββ ...
βββ manifest.json
```
`raw/assets.json` is the untouched response from PiecesOS.
`normalized/assets.jsonl` contains a more convenient representation of the data, including metadata that I intentionally left out of Obsidian.
`manifest.json` maps every Pieces asset to its Markdown filename and includes hashes of the original contents.
That gives me something I can use to verify the migration instead of looking at a folder with 358 files and hoping everything worked.
## Why I Went This Route
I could have just copied the visible snippet contents out of Pieces and called it a day.
But bulk migrations are exactly where small inconsistencies become annoying later.
I wanted to know that:
- every Pieces asset produced a Markdown file
- every original snippet was preserved exactly
- duplicate titles didn't overwrite each other
- metadata filtering was intentional
- rerunning the exporter produced the same filenames
- I still had the untouched source data if I discovered a bug
The most important part of the entire process was probably the first command:
```bash
curl -fsS http://localhost:39300/assets -o ./tmp/pieces-assets.json
```
Once I had that file, Pieces was no longer the only place containing my snippets.
Everything after that was just transformation.
## Finished Script
```ruby
#!/usr/bin/env ruby
# frozen_string_literal: true
require "date"
require "digest"
require "fileutils"
require "json"
require "optparse"
require "securerandom"
require "time"
EXPECTED_ASSET_COUNT = 358
REAL_LUA_ASSET_ID = "eb6ef70c-fd33-47a1-967e-7c6e246384f6"
# NOTE: This constant is very specific to my own Pieces setup
# It maps my specific Pieces asset IDs to the programming language I want to use in Obsidian.
LANGUAGE_OVERRIDES = {
"c379d282-ca8b-493e-a4df-fa010ebc3797" => "javascript",
"c8ecd0c3-693e-4026-8698-8185e20c4d0b" => "bash",
"f943f133-cb73-4b47-a5c3-0078712906bf" => "json",
"93cf8228-2aae-4691-8525-f6e37cecd189" => "yaml",
"34d06144-5932-47b0-b00d-3e55b9ea8d19" => "markdown",
"e46af71d-21f2-49b5-bc02-4b5021fd0681" => "zsh",
"1f8a754f-e5ec-418b-b4b2-92bd24aa8798" => "shell",
"0af3ccde-f9c3-409b-8171-2f01db9b869e" => "shell",
"cddeb072-f13d-4b7b-94a9-1a1e8c480775" => "bash",
"51c77565-8545-4ce7-86b9-476c962a7376" => "bash",
"64cb2e46-129e-44b5-af47-deb67bc4870b" => "json",
"12f44507-75ae-48a1-aada-77c75e9b4814" => "graphql",
"d16236f4-5d05-4fc5-8465-4df119a7d0b3" => "graphql"
}.freeze
ANALYSIS_LANGUAGE_MAP = {
"ASP" => "asp",
"BatchFile" => "batch",
"CSS" => "css",
"CoffeeScript" => "coffeescript",
"Elixir" => "elixir",
"Groovy" => "groovy",
"HTML" => "html",
"JSON" => "json",
"JavaScript" => "javascript",
"Lua" => "lua",
"Markdown" => "markdown",
"Perl" => "perl",
"Python" => "python",
"R" => "r",
"Ruby" => "ruby",
"SQL" => "sql",
"Shell" => "shell",
"TEX" => "tex",
"TOML" => "toml",
"TypeScript" => "typescript",
"YAML" => "yaml"
}.freeze
CLASSIFICATION_LANGUAGE_MAP = {
"asp" => "asp",
"bat" => "batch",
"bash" => "bash",
"coffee" => "coffeescript",
"css" => "css",
"graphql" => "graphql",
"groovy" => "groovy",
"html" => "html",
"js" => "javascript",
"json" => "json",
"lua" => "lua",
"md" => "markdown",
"pl" => "perl",
"py" => "python",
"r" => "r",
"rb" => "ruby",
"sh" => "shell",
"sql" => "sql",
"tex" => "tex",
"toml" => "toml",
"ts" => "typescript",
"yaml" => "yaml",
"yml" => "yaml",
"zsh" => "zsh"
}.freeze
FENCE_LANGUAGE_MAP = {
"applescript" => "applescript",
"asp" => "asp",
"bash" => "bash",
"batch" => "batch",
"coffeescript" => "coffeescript",
"css" => "css",
"elixir" => "elixir",
"graphql" => "graphql",
"groovy" => "groovy",
"html" => "html",
"javascript" => "javascript",
"json" => "json",
"lua" => "lua",
"markdown" => "markdown",
"perl" => "perl",
"python" => "python",
"r" => "r",
"ruby" => "ruby",
"shell" => "bash",
"sql" => "sql",
"tex" => "tex",
"toml" => "toml",
"typescript" => "typescript",
"yaml" => "yaml",
"zsh" => "zsh"
}.freeze
KNOWN_CONTROL_CHARACTERS = {
"c379d282-ca8b-493e-a4df-fa010ebc3797" => [0x07, 0x1B]
}.freeze
options = {
input: "./tmp/pieces-assets.json",
output: "./snippets",
force: false
}
OptionParser.new do |parser|
parser.banner = "Usage: ruby migrate_pieces_to_obsidian.rb [options]"
parser.on("-i", "--input PATH", "Pieces assets.json snapshot") do |path|
options[:input] = path
end
parser.on("-o", "--output PATH", "Output directory") do |path|
options[:output] = path
end
parser.on("-f", "--force", "Replace an existing output directory") do
options[:force] = true
end
end.parse!
input_path = File.expand_path(options[:input])
output_path = File.expand_path(options[:output])
abort "Input does not exist: #{input_path}" unless File.file?(input_path)
if File.exist?(output_path) && !options[:force]
abort <<~MESSAGE
Output already exists: #{output_path}
Re-run with --force to replace it.
MESSAGE
end
def extract_body(asset)
raw = asset.dig("original", "reference", "fragment", "string", "raw")
if raw.is_a?(String)
return [raw.dup.force_encoding(Encoding::UTF_8), "string"]
end
bytes = asset.dig("original", "reference", "file", "bytes", "raw")
if bytes.is_a?(Array)
unless bytes.all? { |byte| byte.is_a?(Integer) && byte.between?(0, 255) }
raise "Invalid byte array for asset #{asset.fetch("id")}"
end
return [
bytes.pack("C*").force_encoding(Encoding::UTF_8),
"file_bytes"
]
end
raise "Unsupported body storage for asset #{asset.fetch("id")}"
end
def validate_body!(asset, body)
id = asset.fetch("id")
raise "Invalid UTF-8 in asset #{id}" unless body.valid_encoding?
if body.bytes.first(3) == [0xEF, 0xBB, 0xBF]
raise "Unexpected UTF-8 BOM in asset #{id}"
end
controls = body.codepoints.select do |codepoint|
(codepoint < 0x20 && ![0x09, 0x0A, 0x0D].include?(codepoint)) ||
codepoint == 0x7F
end
allowed = KNOWN_CONTROL_CHARACTERS.fetch(id, [])
unexpected = controls.uniq - allowed
return if unexpected.empty?
formatted = unexpected.map { |cp| "U+%04X" % cp }.join(", ")
raise "Unexpected control characters in asset #{id}: #{formatted}"
end
def source_timestamp(asset, key)
value = asset.dig(key, "value")
unless value.is_a?(String)
raise "Missing #{key}.value for asset #{asset.fetch("id")}"
end
Time.iso8601(value)
value
end
def analysis_language(asset)
asset.dig("original", "reference", "analysis", "code", "language") ||
asset.dig("original", "analysis", "code", "language") ||
asset.dig("analysis", "code", "language")
end
def classification_language(asset)
asset.dig("original", "reference", "classification", "specific") ||
asset.dig("original", "classification", "specific") ||
asset.dig("classification", "specific")
end
def shell_like?(body)
patterns = [
/\A\s*#!/,
/\b(?:mdfind|mdls|defaults|osascript|plutil|brew|git|gh|curl|wget|grep|sed|awk|find|fd|fzf|xargs|open|launchctl|pg_ctl)\b/,
/\b(?:if|then|elif|fi|for|while|until|do|done|case|esac|function)\b/,
/\$\{?[A-Za-z_][A-Za-z0-9_]*\}?/,
/(?:&&|\|\|)/,
/\|\s*[A-Za-z_][A-Za-z0-9_-]*/
]
patterns.any? { |pattern| body.match?(pattern) }
end
def normalize_language(asset, language)
case language
when "asp"
"markdown"
when "groovy"
"graphql"
when "tex"
"shell"
when "lua"
asset.fetch("id") == REAL_LUA_ASSET_ID ? "lua" : "applescript"
else
language
end
end
def resolve_language(asset, body)
id = asset.fetch("id")
if LANGUAGE_OVERRIDES.key?(id)
return LANGUAGE_OVERRIDES.fetch(id)
end
analysis = analysis_language(asset)
specific = classification_language(asset)
detected =
if analysis.is_a?(String) &&
!analysis.empty? &&
!%w[Unknown UNKNOWN TEXT text].include?(analysis)
ANALYSIS_LANGUAGE_MAP[analysis]
end
if detected.nil? &&
specific.is_a?(String) &&
!specific.empty? &&
!%w[Unknown UNKNOWN TEXT text].include?(specific)
detected = CLASSIFICATION_LANGUAGE_MAP[specific.downcase]
end
# Pieces sometimes identifies Unix shell snippets as Windows BatchFile.
if detected == "batch" && shell_like?(body)
detected = "shell"
end
normalize_language(asset, detected)
end
def manual_tags(asset)
id = asset.fetch("id")
Array(asset.dig("tags", "iterable"))
.select do |tag|
text = tag["text"]
text.is_a?(String) &&
!text.strip.empty? &&
tag.dig("mechanisms", id) == "MANUAL"
end
.map { |tag| tag.fetch("text").strip }
.uniq
end
def manual_explanation(asset)
annotations = Array(asset.dig("annotations", "iterable"))
candidates = annotations.select do |annotation|
annotation["type"] == "EXPLANATION" &&
annotation["mechanism"] == "MANUAL" &&
annotation["text"].is_a?(String) &&
!annotation["text"].strip.empty?
end
candidates.max_by do |annotation|
annotation.dig("updated", "value") ||
annotation.dig("created", "value") ||
""
end&.fetch("text")&.strip
end
def accepted_source_url(asset)
id = asset.fetch("id")
Array(asset.dig("websites", "iterable"))
.find do |website|
url = website["url"].to_s
website.dig("mechanisms", id) == "MANUAL" &&
website["name"] == "Origin Website" &&
!url.empty? &&
!url.include?("cache.raindrop.io") &&
!url.include?("X-Amz-Signature=") &&
!url.include?("X-Amz-Expires=") &&
!url.include?("/_next/static/chunks/")
end
&.fetch("url")
end
def sanitize_filename(name)
value = name.to_s.unicode_normalize(:nfc)
value = value
# macOS / path separators plus characters Obsidian disallows.
.gsub(/[\/:\[\]#^|]+/, " - ")
.gsub(/[\u0000-\u001F\u007F]/, " ")
.gsub(/\s+-\s+-\s+/, " - ")
.gsub(/\s+/, " ")
.strip
.sub(/\A[.\s]+/, "")
.sub(/[.\s]+\z/, "")
value = "Untitled" if value.empty?
value
end
def collision_key(filename)
filename
.unicode_normalize(:nfc)
.downcase
end
def safe_fence(body)
max_run = body.scan(/`+/).map(&:length).max || 0
"`" * [3, max_run + 1].max
end
def yaml_string(value)
JSON.generate(value)
end
def frontmatter(
language:,
tags:,
source_url:,
created_at:,
updated_at:
)
output = +"---\n"
output << "type: snippet\n"
output << %(source: "[[Pieces.app]]"\n)
output << "language: #{yaml_string(language)}\n" if language
output << "created_on: #{Date.parse(created_at)}\n"
output << "updated_on: #{Date.parse(updated_at)}\n"
unless tags.empty?
output << "tags:\n"
tags.each do |tag|
output << " - #{yaml_string(tag)}\n"
end
end
if source_url
output << "source_url: #{yaml_string(source_url)}\n"
end
output << "---\n"
output
end
def markdown_for(
body:,
language:,
tags:,
source_url:,
explanation:,
created_at:,
updated_at:
)
markdown = +""
markdown << frontmatter(
language: language,
tags: tags,
source_url: source_url,
created_at: created_at,
updated_at: updated_at
)
if explanation
markdown << "\n## Explanation\n\n"
markdown << explanation
markdown << "\n"
end
markdown << "\n## Code\n\n"
fence = safe_fence(body)
fence_language = FENCE_LANGUAGE_MAP.fetch(language, language).to_s
markdown << fence
markdown << fence_language unless fence_language.empty?
markdown << "\n"
markdown << body
# The closing Markdown fence must begin on its own line. The manifest
# retains whether the original Pieces body actually had a trailing newline.
markdown << "\n" unless body.end_with?("\n")
markdown << fence
markdown << "\n"
markdown
end
raw_json = File.binread(input_path)
raw_sha256 = Digest::SHA256.hexdigest(raw_json)
data = JSON.parse(raw_json)
assets = data.fetch("iterable")
unless assets.length == EXPECTED_ASSET_COUNT
abort(
"Expected #{EXPECTED_ASSET_COUNT} assets from the audited snapshot, " \
"but found #{assets.length}. Re-audit before exporting."
)
end
asset_ids = assets.map { |asset| asset.fetch("id") }
unless asset_ids.uniq.length == asset_ids.length
abort "Duplicate Pieces asset IDs detected."
end
original_ids = assets.map { |asset| asset.dig("original", "id") }
unless original_ids.compact.uniq.length == original_ids.compact.length
abort "Duplicate Pieces original IDs detected."
end
prepared = assets.map do |asset|
body, storage = extract_body(asset)
validate_body!(asset, body)
created_at = source_timestamp(asset, "created")
updated_at = source_timestamp(asset, "updated")
if Time.iso8601(updated_at) < Time.iso8601(created_at)
raise "updated < created for asset #{asset.fetch("id")}"
end
{
asset: asset,
id: asset.fetch("id"),
original_id: asset.dig("original", "id"),
name: asset.fetch("name"),
basename: sanitize_filename(asset.fetch("name")),
body: body,
body_storage: storage,
body_sha256: Digest::SHA256.hexdigest(body.b),
body_bytes: body.bytesize,
trailing_newline: body.end_with?("\n"),
language: resolve_language(asset, body),
raw_analysis_language: analysis_language(asset),
raw_classification_language: classification_language(asset),
created_at: created_at,
updated_at: updated_at,
tags: manual_tags(asset),
explanation: manual_explanation(asset),
source_url: accepted_source_url(asset)
}
end
groups = prepared.group_by do |record|
collision_key(record.fetch(:basename))
end
prepared.each do |record|
colliding =
groups.fetch(
collision_key(record.fetch(:basename))
).length > 1
record[:filename] =
if colliding
"#{record.fetch(:basename)}--#{record.fetch(:id)[0, 8]}.md"
else
"#{record.fetch(:basename)}.md"
end
end
filenames = prepared.map do |record|
collision_key(record.fetch(:filename))
end
unless filenames.uniq.length == filenames.length
abort "Filename collision remains after deterministic ID suffixing."
end
temporary_path =
"#{output_path}.tmp-#{Process.pid}-#{SecureRandom.hex(4)}"
begin
FileUtils.rm_rf(temporary_path)
raw_dir = File.join(temporary_path, "raw")
normalized_dir = File.join(temporary_path, "normalized")
markdown_dir = File.join(temporary_path, "markdown")
FileUtils.mkdir_p(raw_dir)
FileUtils.mkdir_p(normalized_dir)
FileUtils.mkdir_p(markdown_dir)
raw_output = File.join(raw_dir, "assets.json")
File.binwrite(raw_output, raw_json)
File.chmod(0o600, raw_output)
normalized_path =
File.join(normalized_dir, "assets.jsonl")
File.open(normalized_path, "wb") do |file|
prepared.sort_by { |record| record.fetch(:id) }.each do |record|
asset = record.fetch(:asset)
normalized = {
id: record.fetch(:id),
original_id: record[:original_id],
name: record.fetch(:name),
filename: record.fetch(:filename),
body: record.fetch(:body),
body_storage: record.fetch(:body_storage),
body_sha256: record.fetch(:body_sha256),
body_bytes: record.fetch(:body_bytes),
trailing_newline: record.fetch(:trailing_newline),
language: {
resolved: record[:language],
pieces_analysis: record[:raw_analysis_language],
pieces_classification: record[:raw_classification_language]
},
timestamps: {
created: record.fetch(:created_at),
updated: record.fetch(:updated_at)
},
markdown_metadata: {
tags: record.fetch(:tags),
source_url: record[:source_url],
explanation: record[:explanation]
},
annotations: Array(asset.dig("annotations", "iterable")),
tags: Array(asset.dig("tags", "iterable")),
websites: Array(asset.dig("websites", "iterable"))
}
file.write(JSON.generate(normalized))
file.write("\n")
end
end
manifest_assets = []
prepared
.sort_by { |record| record.fetch(:filename).downcase }
.each do |record|
markdown = markdown_for(
body: record.fetch(:body),
language: record[:language],
tags: record.fetch(:tags),
source_url: record[:source_url],
explanation: record[:explanation],
created_at: record.fetch(:created_at),
updated_at: record.fetch(:updated_at)
)
markdown_path =
File.join(markdown_dir, record.fetch(:filename))
File.binwrite(markdown_path, markdown)
manifest_assets << {
id: record.fetch(:id),
original_id: record[:original_id],
name: record.fetch(:name),
filename: record.fetch(:filename),
body_storage: record.fetch(:body_storage),
body_sha256: record.fetch(:body_sha256),
body_bytes: record.fetch(:body_bytes),
trailing_newline: record.fetch(:trailing_newline),
markdown_sha256:
Digest::SHA256.hexdigest(markdown.b),
language: record[:language],
pieces_analysis_language:
record[:raw_analysis_language],
pieces_classification_language:
record[:raw_classification_language],
created: record.fetch(:created_at),
updated: record.fetch(:updated_at),
manual_tags: record.fetch(:tags),
explanation: record[:explanation],
source_url: record[:source_url]
}
end
markdown_count =
Dir.glob(File.join(markdown_dir, "*.md")).length
unless markdown_count == assets.length
raise(
"Expected #{assets.length} Markdown files, " \
"created #{markdown_count}"
)
end
language_counts =
prepared
.group_by { |record| record[:language] || "unknown" }
.transform_values(&:length)
.sort
.to_h
manifest = {
schema_version: 2,
generated_at: Time.now.utc.iso8601,
source: {
path: input_path,
sha256: raw_sha256,
asset_count: assets.length
},
output: {
markdown_count: markdown_count,
assets_with_manual_tags:
prepared.count { |record| record.fetch(:tags).any? },
total_manual_tags:
prepared.sum { |record| record.fetch(:tags).length },
assets_with_explanation:
prepared.count { |record| record[:explanation] },
assets_with_source_url:
prepared.count { |record| record[:source_url] },
languages: language_counts
},
assets: manifest_assets
}
File.write(
File.join(temporary_path, "manifest.json"),
JSON.pretty_generate(manifest) + "\n"
)
FileUtils.rm_rf(output_path) if File.exist?(output_path)
FileUtils.mv(temporary_path, output_path)
puts
puts "Pieces export complete"
puts "----------------------"
puts "Assets: #{assets.length}"
puts "Markdown files: #{markdown_count}"
puts "Manual tags: #{manifest.dig(:output, :total_manual_tags)}"
puts "Explanations: #{manifest.dig(:output, :assets_with_explanation)}"
puts "Source URLs: #{manifest.dig(:output, :assets_with_source_url)}"
puts "Raw SHA-256: #{raw_sha256}"
puts "Output: #{output_path}"
puts
puts "Languages:"
language_counts.each do |language, count|
puts format(" %-16s %d", language, count)
end
rescue
FileUtils.rm_rf(temporary_path)
raise
end
```
## Final Thoughts
I don't really have a problem with Pieces moving functionality behind a paid plan. Software costs money to build and services cost money to run.
It just changed the equation for me.
I primarily wanted a convenient place to save code snippets, and at this point I would rather own those snippets as Markdown than continue storing them in another application-specific system. I had to make a few changes after running the migration script, but that's fine.
This is also a good reminder of something I keep relearning with personal tooling: **the easier it is to get your data out of a tool, the more comfortable I am putting data into it.**
Pieces gave me enough local access to recover everything I cared about, and now my snippets are sitting alongside the rest of my notes in Obsidian.
With the migration verified, I can finally uninstall Pieces without wondering what I left behind.
---
# We Are the Ruby Community
> My accepted keynote proposal for Blastoff Rails 2026 on why everyone already belongs in the Ruby community.
## Abstract
Many developers feel like observers of the Ruby community rather than participants in it. This is a people-first talk about converting the folks who haven't experienced, or don't see themselves as part of, the Ruby community into realizing they already belong. It reframes what community actually means, breaks down the invisible barriers that make people feel like they don't belong, and gives attendees practical ways to participate right away, especially those who feel like observers rather than contributors. The goal is to show that the Ruby community isn't something you join, it's something you help create.
## Details
### Outline
- Introduction
- What we picture when we hear "the Ruby community," and why that picture keeps people out
- The gap between feeling like an observer and feeling like a participant
- The invisible barriers
- "I'm not experienced enough" and other stories we tell ourselves
- Where these barriers actually come from, and why they're mostly imagined
- Reframing what community means
- Community as something you help create, not a club you're admitted to
- Small, low-stakes contributions that count more than people think
- Practical ways to participate right away
- Concrete first steps for people who feel like they're on the outside
- How the rest of us can lower the barrier for them
- Conclusion
- You already belong
### Desired Outcomes
Attendees who feel like outsiders leave understanding that they already belong and knowing exactly how to take a first step. Attendees who already feel established leave with a clearer sense of the invisible barriers others face and practical ways to lower them.
### Intended Audience
Anyone who feels like an observer of the Ruby community rather than a participant, and the established community members who want to help them in.
## Pitch
The Ruby community's warmth is one of its best features, and yet plenty of people stand at the edge of it convinced they haven't earned a way in. I've spent years hosting podcasts and meeting Rubyists at every stage of their careers, and the same quiet story comes up again and again: "I'm not really part of this." This keynote is my attempt to dismantle that story on stage, to make the case that community is something we build together rather than a status we're granted, and to send people home with something they can actually do on Monday. It's a hopeful, people-first talk, and it's the one I most want to give right now.
[Slides](https://speakerdeck.com/andrewmcodes/we-are-the-ruby-community) Β· [Video](https://youtu.be/ZBrBMC_sH2A)
---
# Kill Process Running on a Specific Port
> I often have to kill processes that weren't stopped correctly on different ports and can never remember the command.
I often have to kill processes that weren't stopped correctly on different ports and can never remember the command.
## Snippet
```sh
kill -9 $(lsof -ti tcp:4000)
```
## Usage
1. Replace `4000` in the script above with the port you want to kill the process on.
2. Run the script in your terminal.
## Extending
We can extract this into a function to make it easier to use.
```sh
terminate() {
local port=$1
local pid=$(lsof -ti tcp:$port)
if [ -n "$pid" ]; then
kill $pid
echo "Killed process $pid on port $port"
else
echo "No process found on port $port"
fi
}
```
Then we can use it like this:
```sh
terminate 4000
```
---
# Living with ADHD: The Benefits of Openness and Vulnerability
> Adventures in adjusting to life with ADHD - Join me on my journey as I talk about working with, and understanding, my ADHD diagnosis.
I have Combined Type ADHD and **it's a big part of what makes meβ¦me.** It isn't my [entire personality](/personality/), but it is constantly affecting me to the point I feel **it's important to know about when you meet or interact with me.**
I didn't know I had it until late in college and I often think about how different my life would have been had I known, and gotten help for it, earlier.
Because of this and because it is such a big part of my life, **I talk very openly about my ADHD** on [social media](/about/), on [podcasts](/podcasts/), and to hiring managers.
Since I became very vocal about it, I have met so many other ADHD developers, some of whom were afraid to admit they had it previously because of the stigma. Even more importantly, **several folks have told me they realized they had ADHD** after hearing me talk about the symptoms and have gotten help.
There are companies out there who wouldn't consider hiring me because of my ADHD, but every time I hear from someone who benefitted from me discussing it openly, **I don't care.**
Medication completely changed my life and I currently take Vyvanse to help manage my ADHD.
**If you want to talk about ADHD**, hit me up on [Twitter](https://twitter.com/andrewmcodes).
> ADHD is being a perfectionist without the capability of motivating yourself to achieve said perfection. \- [@avresco](https://twitter.com/avresco/status/1286441624361287681)
---
# INTP: My Personality Type
> Notes on my INTP personality type, how I relate to the description, and why it helps explain how I think and work.
I find it very hard to describe my personality.
INTP. I think the description below is pretty spot-on.
> INTPs are philosophical innovators, fascinated by logical analysis, systems, and design. They are preoccupied with theory, and search for the universal law behind everything they see. They want to understand the unifying themes of life, in all their complexity.
> INTPs are detached, analytical observers who can seem oblivious to the world around them because they are so deeply absorbed in thought. They spend much of their time in their own heads: exploring concepts, making connections, and seeking understanding of how things work. To the Architect, life is an ongoing inquiry into the mysteries of the universe. 1
To really understand me, you have to understand that I have [Combined Type ADHD](/adhd/), which means I struggle a lot with my hyperactivity and attention span.
[View this post](/adhd/) for more details and why I talk openly about it.
Recently I have increasingly more aware of my inate perfectionism and how it negatively affects me.
---
# How to Add a Progress Bar Around Your Twitter Avatar
> Add a personalized progress bar to your Twitter avatar with the help of BlackMagic.so's Profile Progress Bar Tool. Follow me on Twitter to see it in motion!
My [Twitter avatar](https://twitter.com/andrewmcodes) is outlined by a blue, previously green, progress bar that I get asked about often.
The progress bar is updated in real-time every time I get a new follower. When I reach 10 new followers, the progress bar resets and the loop begins again.
To achieve this effect, I use [BlackMagic.so](https://blackmagic.so/), a suite of tools for influencers to enhance [Twitter](https://twitter.com/andrewmcodes) from [Tony Dinh.](https://twitter.com/tdinh_me) Their main product is an analytics tools, but they have a free tool named [Profile Progress Bar](https://blackmagic.so/profile-progress-bar), which you can use to add a progress bar to your Twitter avatar.
<%= render(Figure.new) do |c| %>
<% c.slot :image do %>
<%= render Image.new(variant: :figure, iid: "posts/twitter-avatar/blackmagic-so-profile-progress-bar-twitter-tool", alt: "BlackMagic.so Profile Progress Bar Twitter Tool") %>
<% end %>
<% c.slot :caption do %>
The BlackMagic.so Profile Progress Bar Twitter Tool
<% end %>
<% end %>
Connect your [Twitter account](https://twitter.com/andrewmcodes) to BlackMagic.so to add a progress bar to your Twitter avatar. You can customize the colors and progress value to your liking and start the real-time updates.
BlackMagic.so has several other free, and paid tools, but this is currently the only one I use.
There is no real reason other than it's fun.
Want to see it increment? [Follow me on Twitter! π](https://twitter.com/andrewmcodes)
---
# Minimalist Habit Tracking Template for Obsidian
> A short tutorial on how to build a minimalist habit tracker template for Obsidian using the Dataview plugin.
In [Obsidian](https://obsidian.md), there are usually many different ways to implement a feature that you would like to have in your vault. Habit tracking is a way to help you track and implement habits in your daily life.
## Daily Notes
If you are already creating daily notes in Obsidian, via the [Daily Notes core plugin](https://help.obsidian.md/Plugins/Daily+notes) or the [Periodic Notes community plugin](https://github.com/liamcain/obsidian-periodic-notes), it makes sense to track your habits here as well. In order to accomplish this, we can leverage the power of [Dataview](https://github.com/blacksmithgu/obsidian-dataview) and templates to create a minimal dashboard for tracking our habit's progress for the week.
Moving forward, I will assume you have [Periodic Notes](https://github.com/liamcain/obsidian-periodic-notes) or [Daily Notes](https://help.obsidian.md/Plugins/Daily+notes) plugin enabled and configured with a template. You do not have to use the [Templater community plugin](https://github.com/SilentVoid13/Templater), the [Templates core plugin](https://help.obsidian.md/Plugins/Templates) will work as well. I will be using Templater in the examples below, but you can sub out any of the special syntax for [Obsidian Templates](https://help.obsidian.md/Plugins/Templates) equivalent.
## Daily Note Template
Here is an example daily note template with Templater, which includes the habits we want to track:
```md
# [[<%% tp.file.title %>|<%% moment(tp.file.title).format("MMMM Do, YYYY") %>]]
[[<%% tp.date.yesterday("YYYY-MM-DD") %>]] | [[<%% tp.date.tomorrow("YYYY-MM-DD") %>]]
## Habits
**Reading**:: **Sleep**:: 0 **Exercise**:: 0 **Highlights**:: 0 **Mindfulness**:: 0
## Notes
```
We will use this template for our daily notes. Whenever we create a new daily note, we can fill in the values for our habits. An example of a daily note with the habits filled in:
## Dashboard
Now we can create a minimal dashboard to track our habits throughout the week. Make sure [Dataview](https://github.com/blacksmithgu/obsidian-dataview) is enabled and create a new note for your dashboard with the following:
```dataview
TABLE WITHOUT ID
file.link as Date,
choice(exercise > 30, "β ", "β") as Exercise,
choice(sleep > 6, "β ", "β") as Sleep,
choice(highlights >= 3, "β ", "β") as Highlights,
choice(mindfulness > 10, "β ", "β") as Mindfulness,
reading as Reading
FROM "daily"
WHERE file.day <= date(now) AND file.day >= date(now) - dur(7days)
SORT file.day ASC
```
<%= render Note.new do %> `FROM "daily"` determines which pages are collected and displayed. In this case, we are only collecting notes in the `daily` folder. You can select based on other sources like tags and links. Refer to the [Dataview FROM documentation](https://blacksmithgu.github.io/obsidian-dataview/query/queries/#from) for more information. <% end %>
After we accumulate more daily notes, our dashboard will look something like:
## Conclusion
This is my preferred method for creating a habit tracking dashboard, but as I said in the beginning, there are multiple ways of accomplishing this behavior.
You can extend this pattern to create dashboards for other features, such as: mood tracking, reading list, journaling, etc.
Let me know what you come up with on [Twitter!](https://twitter.com/andrewmcodes)
---
# Create Repository from Current Directory with the GitHub CLI
> Use gh to create a repo using your current directory as the source and push to GitHub without having to set your upstream.
<%= render(Command.new) { "gh repo create your-repo --public --source=. --push" } %>
## Usage
1. Make sure you have the GitHub CLI installed and you are signed in
2. Run the command above to create a public repo from the current directory and push
## Resources
- [GitHub CLI Documentation](https://cli.github.com/)
---
# How to Deploy Your Bridgetown Site to Github Pages
> It has never been easier to deploy your Bridgetown site to GitHub Pages thanks to a new bundled configuration in Bridgetown v1.0
With the release of [Bridgetown version 1.0](https://www.bridgetownrb.com/release/reaching-1.0-next-generation-progressive-site-generator/) came several new [bundled configurations](https://www.bridgetownrb.com/docs/bundled-configurations) for deployment, in addition to other powerful new features.
I was personally able to introduce a bundled configuration for deploying to Vercel in [bridgetownrb/bridgetown#483.](https://github.com/bridgetownrb/bridgetown/pull/483) After the final v1 release, I was pouring over the changes and, to my delight, I discovered that a bundled configuration for GitHub Pages was added in [bridgetown/bridgetownrb#503](https://github.com/bridgetownrb/bridgetown/pull/503).
Before I explain why I was overjoyed with this addition from [Ayush](https://twitter.com/ayushn21), let's deploy a Bridgetown site to GitHub Pages using this new configuration to see how it works!
## Requirements
Make sure [Bridgetown v1.0 or higher is installed](https://www.bridgetownrb.com/docs), otherwise the `gh-pages` bundled configuration will not be available.
β οΈ This tutorial assumes you are creating a ["Project Site"](https://docs.github.com/en/pages/getting-started-with-github-pages/about-github-pages#types-of-github-pages-sites). If you are trying to create a user or organization site, you may not need to make the configuration changes below.
## Getting Started
If you have an existing Bridgetown site you want to deploy to GitHub Pages, run `bin/bridgetown configure gh-pages` from the root of your project to add the necessary files and skip the following step.
If you do not have an existing site, let's create a new one with the `gh-pages` configuration.
```sh
bridgetown new bridgetown-gh-pages-demo -t erb -c gh-pages && cd bridgetown-gh-pages-demo
```
If done correctly, you will see the following output in your terminal, accompanied by further instructions: `π A GitHub action to deploy your site to GitHub pages has been configured!`
## Updates for GitHub Pages
We need to make two updates to our site to make it work correctly on GitHub Pages as detailed in the configuration output.
The URL of our deployed site will be `https://.github.io//`, where `` acts as a base path for the site. For me, that URL was `https://andrewmcodes.github.io/bridgetown-gh-pages-demo/`.
First, we need to update the `base_path` in our Bridgetown config file `bridgetown.config.yml`:
```diff
- base_path: ""
+ base_path: "/bridgetown-gh-pages-demo"
```
Secondly, [esbuild](https://esbuild.github.io) is now the default build tool in Bridgetown v1.0, and we need to tell it that our `publicPath` is now prefixed with `/bridgetown-gh-pages-demo` in `esbuild.config.js`:
```diff
- const esbuildOptions = {}
+ const esbuildOptions = { publicPath: "/bridgetown-gh-pages-demo/_bridgetown/static" }
```
That should be all the setup we need to do!
β οΈ One thing you may need to check for on an existing site is that you are using `resource.relative_url` instead of passing the location directly to any anchor tags for example. `#relative_url` will correctly add the base path we configured in `bridgetown.config.yml` to the links that you will otherwise have to add manually.
## Pushing Our Work
Let's add and commit this code:
```sh
git add . && git commit -m "chore: initial commit"
```
Next, create your repository using your preferred method. I find myself reaching for the [GitHub CLI](https://cli.github.com/) these days:
```sh
gh repo create bridgetown-gh-pages-demo --public --source=. --push
```
If you set up the repository from the GitHub UI, don't forget to add the remote origin and push, which the CLI command above handles for us:
```sh
git remote add origin https://github.com/USERNAME/bridgetown-gh-pages-demo.git
git push -u origin main
```
## Configure GitHub Pages and Deploy
You may have noticed an action started automatically when you pushed your code, which kicked off a GitHub Pages deploy. Once the action completes successfully, we should have a branch named `gh-pages` with our static site contents.
The last thing we need to do is tell GitHub Pages to use the `gh-pages` branch as it's source in `Settings -> Pages`.
Navigate to the settings page and set the source from `None` to `gh-pages` and click save.
Once the automatic deployment completes, your site URL should be displayed! Navigate to it in your browser and verify everything works correctly.
## Success!
I want to give another big shoutout to [Ayush](https://twitter.com/ayushn21) for adding this bundled configuration to Bridgetown v1.0!
Why? Because now the former art that I had been (lazily) maintaining can be archived and taken off my plate. π
## Farewell Old Code
In the summer of 2020, I created [bridgetown-gh-pages-action](https://github.com/andrewmcodes/bridgetown-gh-pages-action), which attempted to solve the same problem of making deploying your Bridgetown site to GitHub Pages automatic and seamless.
Unfortunately, as I discovered with many of the GitHub Actions I created earlier on, maintenance was a pain, it was not flexible, and the issues were specific to the users environment and almost impossible to debug.
With the introduction of the new `gh-pages` bundled configuration, I will be archiving this action and it will not receive any future updates.
## Final Thoughts
I am interested to see whether this configuration will cause similar issues and churn that I went through on this and several other actions now that it is in Bridgetown. The reality is that these actions are great at the golden path, but tend to fall over if the user has left that path.
By the way, if you are in this situation, you should be creating your own custom action with the library versions you need, not reaching for something off the shelf.
For now, I will be keeping my eyes on the issues to see if I can pitch in when the inevitable "bug" report comes in.
---
# Enable Repeating Keys in VS Code on macOS
> If you want to use Vim in VS Code, you have to enable repeating keys, which can be frustrating if you are new to Vim.
```sh
# VS Code
defaults write com.microsoft.VSCode ApplePressAndHoldEnabled -bool false
# VS Code - Insiders
defaults write com.microsoft.VSCodeInsiders ApplePressAndHoldEnabled -bool false
```
## Usage
1. Run the first command above to enable repeating keys in VS Code
2. If you use VS Code Insiders, run the second command as well
3. You may need to restart VS Code, but repeating keys should now be enabled
## Resources
- [Visual Studio Code Insiders Documentation](https://code.visualstudio.com/insiders/)
---
# Install Brew on an Intel Mac
> The one-liner that installs Homebrew on an Intel Mac running macOS Monterey, the PATH export it needs in ~/.zshrc, and how to verify with brew doctor.
```sh
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
echo 'export PATH="/usr/local/sbin:$PATH"' >> ~/.zshrc
source ~/.zshrc
```
## Usage
1. Run the command above in your terminal application of choice
2. If you have not installed the Xcode Command Line Tools, brew will automatically install them for you
3. After the installation completes, run `brew doctor`
4. If the output of `brew doctor` is `Your system is ready to brew.`, you are done
5. If `brew doctor` returns issues, resolve them according to the provided instructions
6. You may want to set up Homebrew's shell-completion at this time
## Resources
- [Homebrew Documentation](https://docs.brew.sh)
- [Homebrew Documentation: Configuring Completions in zsh](https://docs.brew.sh/Shell-Completion#configuring-completions-in-zsh)
---
# Install Brew on a M1 Mac
> The one-liner that installs Homebrew on an M1 Mac running macOS Monterey, the /opt/homebrew shellenv line for ~/.zprofile, and how to verify it.
```sh
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"
```
## Usage
1. Run `uname -m` in the Terminal of your choice and verify it outputs `arm64`
2. Run the command above
3. If you have not installed the Xcode Command Line Tools, brew will automatically install them for you
4. After the installation completes, run `brew doctor`
5. If the output of `brew doctor` is `Your system is ready to brew.`, you are done
6. If `brew doctor` returns issues, resolve them according to the provided instructions
7. You may want to set up Homebrew's shell-completion at this time
## Resources
- If you are not using the M1 chip, [use this snippet instead](/snippets/brew-install-intel-mac/)
- [Homebrew Documentation](https://docs.brew.sh)
- [Homebrew Documentation: Configuring Completions in zsh](https://docs.brew.sh/Shell-Completion#configuring-completions-in-zsh)
---
# Stop Hoarding Notes
> My ideas on why you should become just as comfortable deleting notes as you do code
Every time you encounter an old note, or any note, it is a good time ask yourself:
> Is this note bringing me explicit value or can it be deleted?
**Don't hoard notes.** If they are not serving their purpose they are useless. **Get rid of them**.
Note taking is similar to coding in this aspect in my brain.
Time for a story:
Early in my career, my mentor told me not to get "married to my code". He explained further and I wish I had known at the time what an impact this statement would have on me.
Over the years I have created my own understanding of this statement. Throughout your time as a software engineer, you will write lots of code, and you will delete lots of code.
**Don't be sad that you or someone else has to delete your code.**
You will write more.
Don't let your pride get in your way and convince you that the removal of the code means anything more than it no longer serves its purpose in the codebase.
**It's 1's and 0's vibrating on metal.**
The second you decide that it represents your worth you discount all the amazing code you have yet to write. There is a good bet that if you had to rewrite it, you'd do it differently.
The code is not important, the lessons you learned while building it are though and they aren't going anywhere. The code's purpose has been served. It was once needed but now it no longer is.
> By acknowledging their contribution and letting them go with gratitude, you will be able to truly put the things you own, and your life, in order. \- Marie Kondo.
Much like code, your notes should be created with a particular goal in mind and when they no longer serve that goal, it is time to thank them and delete them.
Chances are if you start traversing through your notes, you will find lots of things you forgot you saved. That is likely a sign that you haven't been back to it since it was created. The information is in the note, you found it before when you needed it, and you will find it again.
If you are finding lots of information that would have been helpful had it been accessible, it's time to find ways to make sure it resurfaces. Your brain is not a hard drive, it is not good at storage. What it is good at is making connections. Find ways to connect this note to others so there is a clear entry path or put new notes in a box (folder) that you review once a week. After potentially not seeing, or needing, it for an entire week, you may feel differently about its plans to take up space in your system. Regardless of implementation, the result is the same.
Less low quality notes, more notes that you bring you value.
Don't hoard your notes. Like physical possessions, they do nothing but hide the beauty underneath.
⌫
---
# Getting Started with Obsidian
> A beginners guide to setting up Obsidian, an advanced markdown note taking app, for the first time.
## Quick Start
TL;DR I built a [template vault](https://github.com/andrewmcodes/obsidian-beginner-vault-template) based on the settings and instructions in this post.
[Create your own vault from the template](https://github.com/andrewmcodes/obsidian-beginner-vault-template/generate) and then follow the instructions on the README to get started. Continue reading for the breakdown of what I think is a good default setup in Obsidian without any third-party plugins.
## Install
Obsidian can be installed on mobile for [iOS](https://apps.apple.com/us/app/obsidian-connected-notes/id1557175442) and [Android](https://play.google.com/store/apps/details?id=md.obsidian) and on desktop by [visiting their downloads page.](https://obsidian.md/download)
Alternatively, if you are on macOS you can use [Homebrew](https://brew.sh):
```bash
brew install --cask obsidian
```
## Startup
Once installed, opening Obsidian for the first time will land you on the [vault creator](https://help.obsidian.md/User+interface/Vault+switcher#Create+new+vaults) where you can create a new vault, open a folder in Obsidian as a vault, or open the [help vault where you can find the documentation.](https://help.obsidian.md/Obsidian/Index)

Create a new vault and then enter in the name and choose a location. 
When your vault opens for the first time, you may be prompted to [turn on Live Preview](https://help.obsidian.md/Live+preview+update) which I suggest doing.

Your new vault should now be ready for notes to be added.

## Setup
The first thing I do after creating a new Obsidian vault is to create the following folder structure:
```
.
βββ _assets
β βββ attachments
β βββ templates
βββ daily
```
- `attachments` - this is where we will [store files](https://help.obsidian.md/How+to/Manage+attachments) like [images.](https://help.obsidian.md/How+to/Format+your+notes#Images)
- `templates` - This where we will store our [templates.](https://help.obsidian.md/Plugins/Templates)
- `daily` - This is where we will store our [daily notes.](https://help.obsidian.md/Plugins/Daily+notes)
## Settings
### Editor
| Setting | Value |
| ---------------- | ------- |
| Show frontmatter | Enabled |
| Fold heading | Enabled |
| Fold indent | Enabled |
| Use tabs | Enabled |
| Tab size | 2 |
### Files & Links
| Setting | Value |
| ------------------------------------ | ----------------------------- |
| Confirm file deletion | Disabled |
| Automatically update internal links | Enabled |
| Detect all file extensions | Enabled |
| Default location for new attachments | In the folder specified below |
| Attachment folder path | `_assets/attachments` |
### Appearance
| Setting | Value |
| ------------------ | ------- |
| Translucent window | Enabled |
| Font size | 15 |
### Hotkeys
I have modified some of the important hotkeys to better match VS Code's defaults since that is the text editor that I am most familiar with. Click the `Restore default` icon if you'd like to revert a specific hotkey to Obsidian's default hotkeys.
| Description | Shortcut |
| ------------------------------------- | ----------- |
| Command palette: Open command palette | ββ§P |
| Delete current file | ββ§Backspace |
| Delete paragraph | ββ§D |
| Edit file title | ββ₯T |
| Focus on editor | β1 |
| Quick switcher: Open quick switcher | βP |
| Split horizontally | β\ |
| Split vertically | ββ§\ |
| Swap line down | β§β₯β |
| Swap line up | β§β₯β |
### Core plugins
| Plugin | Value |
| -------------------------------------------------------------------------------------- | -------- |
| [Outgoing Links](https://help.obsidian.md/Plugins/Outgoing+links) | Enabled |
| [Tag pane](https://help.obsidian.md/Plugins/Tag+pane) | Enabled |
| [Daily notes](https://help.obsidian.md/Plugins/Daily+notes) | Enabled |
| [Templates](https://help.obsidian.md/Plugins/Templates) | Enabled |
| Slash commands | Enabled |
| [Starred](https://help.obsidian.md/Plugins/Starred+notes) | Enabled |
| [Markdown format importer](https://help.obsidian.md/Plugins/Markdown+format+converter) | Disabled |
| [Outline](https://help.obsidian.md/Plugins/Outline) | Enabled |
| [Audio recorder](https://help.obsidian.md/Plugins/Audio+recorder) | Enabled |
| [Workspaces](https://help.obsidian.md/Plugins/Workspaces) | Enabled |
### Community Plugins
| Setting | Value |
| --------- | -------- |
| Safe mode | Disabled |
## Core Plugin Options
### Templates
| Setting | Value |
| ------------------------ | ------------------- |
| Template folder location | `_assets/templates` |
### Daily notes
#### Daily Note Template
Create a new template titled `t_daily` under `_assets/templates`. Refer to the [Templates documentation](https://help.obsidian.md/Plugins/Templates) for more information about variables.
```md
# {{date}}
## Notes
---
#daily
```
#### Settings
| Setting | Value |
| -------------------------- | --------------------------- |
| New file location | daily |
| Template file location | `_assets/templates/t_daily` |
| Open daily note on startup | Enabled |
#### Hotkeys
| Description | Shortcut |
| -------------------------- | -------- |
| Templates: Insert template | ββ₯I |
## Important Documentation
I would suggest starting with these documentation pages:
- [Creating Notes Documentation](https://help.obsidian.md/How+to/Create+notes)
- [Embedding Files Documentation](https://help.obsidian.md/How+to/Embed+files)
- [Block Level Linking Documentation](https://help.obsidian.md/How+to/Link+to+blocks)
- [YAML Front Matter Documentation](https://help.obsidian.md/Advanced+topics/YAML+front+matter)
## Wrap Up
This post is still growing so expect some changes. Also, don't forget to check out the [GitHub repo for the template!](https://github.com/andrewmcodes/obsidian-beginner-vault-template)
---
# Alfred Custom Terminal Snippet
> An AppleScript that wires Alfred's terminal integration to a custom terminal like Warp or Archipelago, plus the Alfred settings that enable it.
```applescript
on alfred_script(q)
do shell script "open -a Warp ~" -- [tl! highlight]
set appOpen to false
set nbrOfTry to 0
delay 0.5
repeat
try
tell application "System Events"
if exists (window 1 of process "Warp") then
set appOpen to true
exit repeat
end if
end tell
end try
set nbrOfTry to nbrOfTry + 1
if nbrOfTry = 20 then exit repeat
delay 0.5
end repeat
if appOpen then tell application "System Events" to keystroke q & return
end alfred_script
```
## Usage
1. Replace `Warp` in the script above with the name of your terminal app, e.g., Archipelago, Fig, Warp
2. Open Alfred's preferences and navigate to the Terminal preferences under "Features"
3. Set `Application` to `Custom`
4. In the text box that appears, paste the script
## Resources
- [Accessing Alfred Preferences Documentation](https://www.alfredapp.com/help/kb/access-preferences/)
- [Alfred Terminal Documentation](https://www.alfredapp.com/help/features/terminal/)
---
# How to Unhide Desktop Icons on macOS
> If your desktop icons disappear, you may need to toggle the desktop back on via the command line.
One day I realized all of the desktop icons on my 2019 MacBook Pro were missing, but still visible in Finder. I thought maybe it was a bug in Big Sur 11.4 but I eventually found the solution.
There is a command to hide the icons on the desktop:
```bash
defaults write com.apple.finder CreateDesktop FALSE; killall Finder
```
I don't remember running it, but by setting `CreateDesktop` to `TRUE` and restarting Finder, all my icons showed back up!
```bash
defaults write com.apple.finder CreateDesktop TRUE; killall Finder
```
To make this easier in the future, I added a function to my shell:
```bash
# Pass TRUE or FALSE - toggles desktop icons on and off.
function toggle_desktop_icons()
{
if [ -z "$1" ]
then
echo "You must pass 'TRUE' or 'FALSE' to this function!"
echo "Try 'toggle_desktop_icons true' or 'toggle_desktop_icons false'"
echo ""
echo "Aborting!"
return 0
fi
defaults write com.apple.finder CreateDesktop $1; killall Finder
}
```
## References
- [The Easiest Way To Hide Desktop Icons On Mac β Setapp](https://setapp.com/how-to/hide-icons-on-mac)
---
# How to install Ruby on Rails 6.1 with asdf on macOS Big Sur
> Setup Ruby, Node, and PostgreSQL with asdf to quickly get up and running with Rails
Ruby and Ruby on Rails have an outdated reputation of being difficult to set up, and some jump on this point to push their full stack JavaScript fantasies. In 2021, however, this doesnβt have to be an issue with the correct tool.
In this tutorial, we will set up Ruby on Rails 6.1 with Ruby 3, a PostgreSQL database, and Webpacker via Node on a clean install of macOS Big Sur without any pain thanks to [asdf](https://asdf-vm.com/#/).
## Step 1 - Install Homebrew
> The Missing Package Manager for macOS (or Linux)
Install [Homebrew](https://brew.sh/) and update your `$PATH`. Take a look at the [official documentation](https://docs.brew.sh/) if you run into issues.
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
echo 'export PATH="/usr/local/sbin:$PATH"' >> ~/.zshrc
source ~/.zshrc # If you see weird behavior, restart your terminal
brew doctor
```
If everything is set up correctly, `brew doctor` will return `Your system is ready to brew.`. Address issues if there are any as they may cause issues down the road. You may also want to set up [Homebrew's shell-completion](https://docs.brew.sh/Shell-Completion#configuring-completions-in-zsh) at this time.
## Step 2 - Install asdf
> Manage multiple runtime versions with a single CLI tool
Install [asdf](https://asdf-vm.com/) as well as some necessary dependencies and update your `~/.zshrc`. If you'd like to install asdf through a different method, check out [the official documentation](https://asdf-vm.com/#/core-manage-asdf).
We are also going to create a `.asdfrc` file and [enable `legacy_version_file`](https://asdf-vm.com/#/core-configuration?id=homeasdfrc), which will allow us to get version info from files like `.ruby-version`.
```bash
brew install coreutils curl git gpg gawk zsh yarn asdf
echo -e "\n. $(brew --prefix asdf)/asdf.sh" >> ~/.zshrc
echo 'legacy_version_file = yes' >> ~/.asdfrc
```
Restart your terminal.
## Step 3 - Install Ruby
```bash
asdf plugin add ruby
asdf install ruby 3.0.0
asdf global ruby 3.0.0
```
[Documentation](https://github.com/asdf-vm/asdf-ruby)
## Step 4 - Install Node
```bash
asdf plugin add nodejs
bash -c '`${ASDF_DATA_DIR:=$HOME/`.asdf}/plugins/nodejs/bin/import-release-team-keyring'
asdf install nodejs 14.16.0
asdf global nodejs 14.16.0
```
[Documentation](https://github.com/asdf-vm/asdf-nodejs)
## Step 5 - Install Postgres
```bash
asdf plugin add postgres
asdf install postgres 13.2
asdf global postgres 13.2
$HOME/.asdf/installs/postgres/13.2/bin/pg_ctl -D $HOME/.asdf/installs/postgres/13.2/data -l logfile start
```
[Documentation](https://github.com/smashedtoatoms/asdf-postgres)
π₯ You can [add this handy function to your `~/.zshrc`](https://gist.github.com/jbranchaud/3cda6be6e1dc69c6f55435a387018dac "3cda6be6e1dc69c6f55435a387018dac") from [Josh Branchaud](https://twitter.com/jbrancha) to make switching postgres versions easier.
## Step 6 - Create Ruby on Rails app
To make sure everything's correct, let's create a new Rails app with a postgres database:
```bash
gem install bundler rails
rails new asdf_demo -d postgresql
cd asdf_demo
bin/rails db:prepare
bin/rails s
```
Open `localhost:3000` in your browser and you should see the Rails welcome screen. π₯³
## Summary
In the end, this was all we needed to get a Rails app running on a clean install of macOS Big Sur:
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
echo 'export PATH="/usr/local/sbin:$PATH"' >> ~/.zshrc
source ~/.zshrc
brew doctor
brew install coreutils curl git gpg gawk zsh yarn asdf
echo -e "\n. $(brew --prefix asdf)/asdf.sh" >> ~/.zshrc
echo 'legacy_version_file = yes' >> ~/.asdfrc
# β οΈ Restart your terminal β οΈ
asdf plugin add ruby
asdf install ruby 3.0.0
asdf global ruby 3.0.0
asdf plugin add nodejs
bash -c '`${ASDF_DATA_DIR:=$HOME/`.asdf}/plugins/nodejs/bin/import-release-team-keyring'
asdf install nodejs 14.16.0
asdf global nodejs 14.16.0
asdf plugin add postgres
asdf install postgres 13.2
asdf global postgres 13.2
$HOME/.asdf/installs/postgres/13.2/bin/pg_ctl -D $HOME/.asdf/installs/postgres/13.2/data -l logfile start
# β οΈ Restart your terminal β οΈ
gem install bundler rails
rails new asdf_demo -d postgresql
cd asdf_demo
bin/rails db:prepare
bin/rails s
```
> I am sure there are ways to improve this and I would love to hear any optimizations you come up with!
It doesn't have to end here though! Checkout [asdf's plugin list](https://asdf-vm.com/#/plugins-all?id=plugin-list) for all available plugins. [Redis](https://github.com/smashedtoatoms/asdf-redis), [Elasticsearch](https://github.com/asdf-community/asdf-elasticsearch), and [ImageMagick](https://github.com/mangalakader/asdf-imagemagick) can all be managed through asdf.
Hopefully this tutorial will help you get up and running with a Ruby on Rails 6.1 development environment lightning fast and pain free. π
Happy coding!
---
# Automating Ruby Gem Releases with GitHub Actions
> Automate Ruby gem releases with GitHub Actions and Release Please: conventional-commit versioning, changelog generation, and publishing to RubyGems.
Whether you are a gem maintaining machine or new to the world of authoring gems, this tutorial is for you. Adhereing to SemVer and keeping an updated changelog are both important components in well maintained open source, but they are also a pain at times. This tutorial will walk you through a simple way to create a release process that automates the small, but important, parts of maintaining a Ruby gem.
## Release Please
[Release Please Action](https://github.com/googleapis/release-please-action) is a GitHub action created by Google to automate releases with [Conventional Commit Messages](https://www.conventionalcommits.org/en/v1.0.0/). As you merge PR's into your main branch, the action will create/update a new release branch that automatically adds your commits to a changelog and bumps the version according to your commits. When you're ready to release your changes, merging the PR will cause a new GitHub release to be created and released. We can even automate publishing to package registries like [RubyGems](https://rubygems.org)!
## Conventional Commits
This article will assume you are familiar with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). Here is a brief overview of the important prefixes, pulled from [the action's README](https://github.com/googleapis/release-please-action#whats-a-release-pr)
The most important prefixes you should have in mind are:
- `fix`: which represents bug fixes, and correlates to a SemVer patch.
- `feat`: which represents a new feature, and correlates to a SemVer minor.
- `feat!`:, or `fix!:`, `refactor!:`, etc., which represent a breaking change (indicated by the !) and will result in a SemVer major.
I've considered doing a longer article about how I use conventional commit messages in my workflow, so let me know if you'd be interested in that.
## Testing it out
I'm going to create a new gem to demo this action's functionality:
```bash
bundle gem release-please-demo --test=rspec --ci=github
cd release-please-demo
bundle install
```
> Skip to the bottom if you'd just like to see the result!
Next we will need to update our gemspec if we want to publish the gem. I'm not going to go over this right now, but if you're curious to learn more about how to setup a Ruby gem specification, I suggest [checking out this great article by Piotr Murach](https://piotrmurach.com/articles/writing-a-ruby-gem-specification/).
This is what my `release-please-demo.gemspec` looks like after filling in the `TODO` placeholders Bundler leaves for the summary and description, removing the `allowed_push_host` line (that placeholder only matters for a private gem server, so leaving it out publishes to public RubyGems), and uncommenting the `rubygems_mfa_required` line (recommended):
```ruby
# frozen_string_literal: true
require_relative "lib/release/please/demo/version"
Gem::Specification.new do |spec|
spec.name = "release-please-demo"
spec.version = Release::Please::Demo::VERSION
spec.authors = ["Andrew Mason"]
spec.email = ["REDACTED@andrewm.codes"]
spec.summary = "Demo of release-please."
spec.description = "A demo gem showing how to use release-please to automatically version gems."
spec.homepage = "https://github.com/andrewmcodes/release-please-demo"
spec.license = "MIT"
spec.required_ruby_version = ">= 3.2.0"
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "https://github.com/andrewmcodes/release-please-demo"
spec.metadata["changelog_uri"] = "https://github.com/andrewmcodes/release-please-demo/blob/main/CHANGELOG.md"
spec.metadata["rubygems_mfa_required"] = "true"
# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
gemspec = File.basename(__FILE__)
spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
ls.readlines("\x0", chomp: true).reject do |f|
(f == gemspec) ||
f.start_with?(*%w[bin/ Gemfile .gitignore .rspec spec/ .github/ .standard.yml])
end
end
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
end
```
Since the purpose of this article is to focus on the release cycle, we are just going to use the gem that Bundler scaffolded without any code changes. If you were building you own gem, this is the part where you would add functionality to the gem.
## Setting up the action
Let's build our release action:
```sh
touch .github/workflows/release.yml
```
Open this in your code editor of choice.
First we are going to set the name of the action, and when it should run. We only want this action to run when something is merged into the default branch, or a release branch depending on your workflow. I name my default branch main, so every time code gems pushed to main, we will run this action.
```yaml
# .github/workflows/release.yml
name: release
on:
push:
branches:
- main
```
Next we need to setup a job for the [Release Please Action](https://github.com/googleapis/release-please-action). As of `v4`, configuration moved out of the workflow's `with:` block and into two files that live at the root of your repo: a `release-please-config.json` that describes each package, and a `.release-please-manifest.json` that tracks the current version. Please [view the official configuration documentation](https://github.com/googleapis/release-please/blob/main/docs/manifest-releaser.md) to learn more.
Here is `release-please-config.json` for our gem. Options are annotated with comments (real JSON can't have comments, so strip these before committing):
```jsonc
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"packages": {
// "." means the package lives at the repo root
".": {
// The release type
"release-type": "ruby",
// The name of our gem
"package-name": "release-please-demo",
// Path to the version file to increment
"version-file": "lib/release/please/demo/version.rb",
// Where the changelog is written
"changelog-path": "CHANGELOG.md",
// Should breaking changes before 1.0.0 produce minor bumps?
"bump-minor-pre-major": true,
// Tag releases as v1.2.3 rather than 1.2.3
"include-v-in-tag": true
}
}
}
```
And `.release-please-manifest.json`, seeded with your current version:
```json
{
".": "0.1.0"
}
```
Now the workflow itself. It needs `contents: write` and `pull-requests: write` permissions so the action can push the release branch and open the PR, and we pass the built-in `GITHUB_TOKEN`:
```yaml
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
token: ${{ secrets.GITHUB_TOKEN }}
```
We are going to do some more cool things in a second, but let's go ahead and see what this produces. Before the first push, two bits of housekeeping:
- **Remove the failing sample spec.** Bundler scaffolds a test that asserts `expect(false).to eq(true)`, so the `main.yml` CI it adds with `--ci=github` fails until you delete it.
- **π¨ Deal with `Gemfile.lock`.** Release Please's Ruby updater bumps the version in `version.rb` and the lock's `specs:` section, but not Bundler's newer `CHECKSUMS` section ([release-please#2720](https://github.com/googleapis/release-please/issues/2720)). Since [lockfile checksums](https://blog.rubygems.org/2024/12/19/bundler-v2-6.html) are on by default in Bundler 2.6+ (and Bundler 4 / Ruby 4.0), a committed lock ends up internally inconsistent after each bump, and the release job's frozen `bundle install` fails with exit code 16. Two ways to avoid it:
- **Don't commit the lock:** add `/Gemfile.lock` to `.gitignore`. This is the conventional choice for a library gem anyway.
- **Keep the lock but drop its checksums:** run `bundle config set --local lockfile_checksums false` and regenerate the lock with `bundle install`. With no `CHECKSUMS` section there's nothing for the version bump to leave stale. (`--local` writes `.bundle/config`, which Bundler gitignores by default, so commit the checksum-free `Gemfile.lock` it produces.)
Now create the repo and push it with the [GitHub CLI](https://cli.github.com/):
```bash
git add -A
git commit -m "chore: initial commit"
gh repo create release-please-demo --public --source=. --remote=origin --push
```
One manual setting is required before Release Please can open a release PR: in the new repo, go to **Settings β Actions β General β Workflow permissions** and enable **Allow GitHub Actions to create and approve pull requests**. Without it, the action fails when it tries to open the PR.
The release action will run once you push your changes to the main branch. On this first run there's nothing to release yet: the only commits are chores and build changes, so Release Please finds no user-facing commits and doesn't open a release PR.
Just for reference - here's `git log --oneline` after the initial push:
```bash
c793bca (HEAD -> main, origin/main) chore: initial commit
```
There are no `feat:` or `fix:` commits, so the action did not create a release PR.
## Creating a release
I'm going to cheat and add an empty commit for a feature:
```bash
git commit --allow-empty -m "feat: add a feature"
git push -u origin main
```
Our release action should run and this time find a user facing commit and open a new release PR. The PR will increment the version number and create a new, or edit an existing, Changelog.

## Publish to RubyGems
Our current setup is great if we just want to automate changelog creation and versioning, but we would still have to publish the gem ourselves after the release was created. Fortunately, we can hook into our existing workflow to automate publishing as well!
You may have noticed we gave our first step an id of `release`. By doing this, we can check the output of that step in other steps and act accordingly.
Rather than juggle a long-lived RubyGems API token, we'll publish with [Trusted Publishing](https://guides.rubygems.org/trusted-publishing/). It uses OpenID Connect (OIDC) so GitHub Actions authenticates to RubyGems.org with a short-lived token minted at publish time. There's no secret to create, rotate, or leak. The [`rubygems/release-gem`](https://github.com/rubygems/release-gem) action handles the build-and-push for us.
### One-time RubyGems setup
Trusted publishing needs a one-time configuration on RubyGems.org. From the gem's **Trusted publishers** page, click **Create** and provide the repository owner, repository name, and the workflow filename (`release.yml`). For a gem that hasn't been published yet, add a [pending trusted publisher](https://guides.rubygems.org/trusted-publishing/) from your profile instead, and the first successful publish claims the name. There's no secret to add to GitHub afterward.
### Setup Steps
`release-gem` assumes your workflow has already checked out the repo and set up Ruby with Bundler, and that your gem has Bundler's release tasks configured (the default `Rakefile` from `bundler gem` does). Guarded on `release_created`, we check out the code and set up Ruby. Per [the action's README](https://github.com/rubygems/release-gem/blob/v1/README.md), the checkout uses `persist-credentials: false`:
```yaml
# Checkout code if release was created
- uses: actions/checkout@v7
with:
persist-credentials: false
if: ${{ steps.release.outputs.release_created }}
# Setup ruby if a release was created
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
ruby-version: ruby
if: ${{ steps.release.outputs.release_created }}
```
`ruby-version: ruby` tells `setup-ruby` to use the latest stable Ruby. `bundler gem` doesn't scaffold a `.ruby-version` file, so without this input the step has no version to resolve and fails.
### Publish Step
If a release was created, `release-gem` builds the gem and pushes it to RubyGems over trusted publishing:
```yaml
- uses: rubygems/release-gem@v1
if: ${{ steps.release.outputs.release_created }}
```
For this to work the job needs two permissions: `id-token: write` (mandatory for trusted publishing) and `contents: write` (so the release tasks can push the tag). As of `v1.1.0`, `release-gem` also generates a build provenance [attestation](https://github.com/rubygems/release-gem#attestations) by default.
## Release and Publish
Our final action, with the added `id-token: write` permission:
```yaml
# .github/workflows/release.yml
name: release
on:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
id-token: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
token: ${{ secrets.GITHUB_TOKEN }}
# Checkout code if release was created
- uses: actions/checkout@v7
with:
persist-credentials: false
if: ${{ steps.release.outputs.release_created }}
# Setup ruby if a release was created
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
ruby-version: ruby
if: ${{ steps.release.outputs.release_created }}
# Build and push to RubyGems via trusted publishing
- uses: rubygems/release-gem@v1
if: ${{ steps.release.outputs.release_created }}
```
Commit, push this code, and wait for your release PR to be updated by our action bot. Once the release PR has been updated, merge the PR into your main branch.
Once our release action runs, assuming it succeeds, you should see a new release in GitHub! One great feature of this action is that it will build the release notes from our changelog entries. π

If we check RubyGems, we should see our new gem has been published and is ready to share!
## Final Thoughts
If you followed the tutorial and don't intend to use your new gem, you should consider yanking it to allow others to use the name in the future.
```bash
gem yank release-please-demo -v 0.2.0
```
One great aspect of the action is that you can use it with other languages or a `.txt` file, allowing you to create consistent pattern across all of your open source. You could enhance the workflow by adding in checks to run the tests before releases and also adding a linter to ensure conventional commits are used. With this workflow, you'll be able to make new releases without pulling down the code and never have to try and remember how you release a project again.
Give it a try and tell me what you think!
---
# Redesigning my website
> Why and how I rebuilt andrewm.codes with Bridgetown to put content first, and the stack behind it: ERB, Tailwind CSS, and Strapi.
My personal website - [andrewm.codes](https://andrewm.codes/) has gone through many iterations over the years. Some may view my constant tinkering as a waste of time, and it probably is, but I've come to realize that I really enjoy using it as a playground to test new technologies and new ideas. The problem with this approach though is that it continually became an excuse for not writing actual content. With this iteration of the site, I decided to put content creation first and tinkering second.
## History
My site has existed on multiple domains using various technologies since 2016. I won't bore you with all of the details but here is a short list of _some_ of the tech I've used to build other versions:
1. Plain HTML & CSS
2. [Nuxt](https://nuxtjs.org/)
3. [Gatsby](https://www.gatsbyjs.com)
4. [Stackbit](https://www.stackbit.com)
5. [Bridgetown](https://www.bridgetownrb.com)
## Planning
One of the reasons most of my other sites ended up being unmaintainable or not having certain features that I wanted was simply due to a lack of planning. This time I wanted things to be different.
### Choosing a Framework
As a Rubyist, I had always struggled to wrap my brain around JavaScript static site generators because of what I viewed as unnecessary complexity and dependency hell; however, I continued to use them over something like [Jekyll](https://jekyllrb.com) because I wanted to use more modern tools like [Tailwind CSS](https://tailwindcss.com) and build my views with components.
Enter [Bridgetown](https://www.bridgetownrb.com).
I'm not going to go over what Bridgetown is right now, but the key features it had that I wanted were Ruby, markdown, components, ability to use modern JS tooling easily, API for content generation, and easily customizable. I definitely encourage you to give it a look if you're considering a site redesign.
### Setting Priorities
I sat down and chose a few things I really cared about when it came to the content and architecture of the site:
- Owning my content
- Ability to publish content remotely
- Great developer experience
- Scriptable
While I didn't follow this 100% of the time, my general goal was to only work on things in service to these main goals.
Data design is something I will discuss below but I wanted to call out the fact I should have spent more time settling on the shape of the data and exactly what content I wanted before diving straight into building with fake data. What I failed to recognize at first is that the data structure for your static site is just as important as for your webapp, and careful upfront design can save lots of time and headache.
## The Stack
- Framework: [Bridgetown](https://www.bridgetownrb.com)
- Template Language: [ERB](https://www.bridgetownrb.com/docs/erb-and-beyond)
- Frontend Build Tool: [Snowpack](https://www.snowpack.dev)
- CSS: [Tailwind CSS](https://tailwindcss.com) w/ Darkmode!
- JavaScript Sprinkles: [Stimulus](https://stimulus.hotwire.dev)
- Performance: [Turbo](https://turbo.hotwire.dev)
- Image Management: [Cloudinary](https://cloudinary.com/)
- Hosting: [Vercel](https://vercel.com)
- Analytics: [Plausible](https://plausible.io)
## Content Creation Workflow
Bridgetown gives you access to [powerful ways to dynamically build pages and date](https://www.bridgetownrb.com/docs/plugins#http-requests-and-the-document-builder) so regardless of where the data comes from, implementation is pretty straight forward especially since you can leverage gems of your choosing.
> Warning: overkill solution ahead
All of the site data, except for blog articles, are saved in a [Strapi headless CMS](https://strapi.io/) self-hosted on [Heroku](https://heroku.com) backed by a PostgreSQL database. A bit of work to get up and running but it offers me:
- Robust REST Api with automically generated Swagger docs
- GraphQL endpoint with API methods
- Media file management with automatic Cloudinary uploads
Ultimately this is one piece I may replace in the future but its an easy way to get up and running fast with a headless CMS and allow you to own the data. This was the only option I found that gave me all the features I wanted out of the box. I tried several data sources including Forestry, DatoCMS, Sanity, Netlify CMS, Airtable, Notion, and Google Sheets, but ultimately the fact Strapi can use Postgres as a datastore is what sold it. This gives me infinite power of the data including from my via [SQLPro Studio](https://www.sqlprostudio.com).
With Strapi in place, I wrote a little Ruby (that needs to be refactored) to dump the records from specified API endpoints into datafiles. When the site builds, I reference these datafiles to build the collection views and dynamically generate content pages (like [about](https://andrewm.codes/about/)).
As for blog articles, they can be stored in Strapi as well, but I elected to just use [DEV](https://dev.to/andrewmcodes) as the primary source for articles, dumped to a datafile using the [DEV API](https://docs.dev.to/api/) and a little Ruby. When the site builds, Bridgetown uses that datafile to generate the posts.
Notes were one thing that I originally built into the site but I decided to pull them all out after discovering [Gistpad](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.gistfs). I plan to do a more detailed post about my workflow with this incredible extension in the future but the simple version is I use Gistpad's wiki feature to easily author notes in VS Code that sync to a GitHub repo. When I'm on my phone, I use [GitJournal](https://gitjournal.io/) to by hooking it up to my repo.
## What's Next
There are a few more features I want to add that didn't stop me from shipping:
- Update some of my old posts to work better with the new layout
- WebMentions integration
- Dynamic social share images
- More automations via iPhone Shortcuts and GitHub Actions
## Takeaways
If I had to do it over again I would spend the time upfront to carefully choose the underlying data structure instead of jumping in head first. I consider myself to be a tool builder but an incredible amount of time was wasted by trying to lean solely on third-party services. Once I leaned back into my natrual tendency to build tools, I was able to solve most of my issues with a few lines of Ruby.
At the end of the day though, I'm thrilled with the way it's turned out on the front and backend. More importantly tweaking my site will no longer get in the way of publishing content.
[Check it out and leave me your feedback!](https://andrewm.codes)
---
# Webpacker 6: Image Asset Guide
> How to serve images and SVGs from a Webpacker 6 Rails app using require.context and asset_pack_path. Part of the archived Webpacker 6 guide.
In order to use your images and SVG files with Webpacker 6, you need to put them in the correct place and import them into your context.
## Install
We should be good here.
## Usage
### Add Assets
```sh
mkdir -p app/javascript/media/images
```
### Require Context
```diff
// app/javascript/packs/application.js
+
+ function importAll(r) {
+ r.keys().forEach(r);
+ }
+ // Add relevant file extensions as needed below.
+ // I'm sure there is a better way :shrug:
+ importAll(require.context('../media/images/', true, /\.(svg|jpg)$/));
```
## Verify
> Note: Restart the dev server for good luck!
Add an SVG and PNG into `app/javascript/media/images`
In one of your views, add two image tags:
```erb
```
Reload your browser and you should see your images.
Note that `<%%= asset_pack_path 'media/images/icon.svg' %>` only returns a string, so if you would rather inline your SVG files you will need to refer to the [Webpack Asset Modules documentation][1] and merge your changes into your Webpack context, as explained in [these Webpacker docs][2].
[1]: https://webpack.js.org/guides/asset-modules/#inlining-assets
[2]: https://github.com/rails/webpacker#webpack-configuration
---
# Webpacker 6: Troubleshooting Guide
> Tools and techniques for debugging Webpacker 6 and Webpack build errors in Rails. Archived, since Webpacker is no longer maintained.
A (growing) collection of tools and techniques for debugging your Webpack(er) setup.
I highly recommend reading the [SurviveJS - Webpack 5 ebook for a real deep dive!][2].
## Locating the Issue
Use the network tab in your browser to view the CSS thatβs being sent to the browser for clues.
## Cached Styles
If you update your styles but your UI does not change and there are no errors, itβs likely browser caching.
Hard reload after clearing your cache and you should see your changes.
To avoid these errors, import your styles into your JS pack vs. using `style-loader`.
## Tools
### Dashboards
- [Webpack-Dashboard][1]
[1]: https://github.com/FormidableLabs/webpack-dashboard "Webpack-Dashboard"
[2]: https://survivejs.com/webpack/foreword/ "SurviveJS - Webpack 5 ebook for a real deep dive!"
---
# Webpacker 6: SCSS/Sass Loaders
> How to compile SCSS and Sass in a Webpacker 6 Rails app with sass-loader and sass. Part of the archived Webpacker 6 upgrade guide.
In order to process `.scss` and `.sass` files with Webpacker 6, you need to add [sass-loader and sass][1].
> Note: This section builds on the [CSS section](https://andrewm.codes/blog/webpacker-6-css-loaders/)
## Install
```bash
yarn add sass-loader sass
```
## Usage
**You should be able to use the same pack tag that you added for CSS.**
Make sure you restart `bin/webpack-dev-server` after installing new loaders.
### Style Loader Example
```diff
<%%# app/views/layouts/application.html.erb %>
+ <%%= stylesheet_packs_with_chunks_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%%= javascript_packs_with_chunks_tag 'application', 'data-turbolinks-track': 'reload' %>
```
### Extract Example
```diff
<%%# app/views/layouts/application.html.erb %>
<%%= javascript_packs_with_chunks_tag 'application', 'data-turbolinks-track': 'reload' %>
+ <%%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
```
```diff
// app/javascript/packs/application.js
+ import "./application.scss"
```
## Verify
> Note: Make sure you restart the dev server!
Letβs create a new SCSS file:
```bash
touch app/javascript/packs/application.scss
```
Next, add some SCSS:
```css
/* app/javascript/packs/application.scss */
$body-background: #fafafa;
$body-color: #444;
body {
background: $body-background;
color: $body-color;
font-family: sans-serif;
}
h1,
nav,
footer {
text-align: center;
}
main {
margin: 4rem auto;
max-width: 60rem;
}
```
Reload your browser and your styles should be applied now, and the Webpacker loader error should be gone.
[1]: https://webpack.js.org/loaders/sass-loader/
---
# Webpacker 6: PostCSS Loaders
> How to process .pcss files in Webpacker 6 with postcss-loader and PostCSS 8. Part of the archived Webpacker 6 upgrade guide.
In order to process `.pcss` files with Webpacker 6, you need to add [postcss-loader][1]. I am also going to add PostCSS 8 support.
> Note: This section builds on the CSS section
## Install
```bash
yarn add postcss-loader postcss@latest autoprefixer@latest postcss-import@latest
```
### Add PostCSS Config File
```bash
touch postcss.config.js
```
```bash
// postcss.config.js
module.exports = {
plugins: [
require('postcss-import'),
require('autoprefixer')
]
}
```
## Usage
**You should be able to use the same pack tag that you added for CSS.**
Make sure you restart `bin/webpack-dev-server` after installing new loaders.
### Style Loader Example
```diff
<%%# app/views/layouts/application.html.erb %>
+ <%%= stylesheet_packs_with_chunks_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%%= javascript_packs_with_chunks_tag 'application', 'data-turbolinks-track': 'reload' %>
```
### Extract Example
```diff
<%%# app/views/layouts/application.html.erb %>
<%%= javascript_packs_with_chunks_tag 'application', 'data-turbolinks-track': 'reload' %>
+ <%%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
```
```diff
// app/javascript/packs/application.js
+ import "./application.css"
```
## Verify
> Note: Make sure you restart the dev server!
Letβs create a new PostCSS file:
```bash
mkdir app/javascript/stylesheets
touch app/javascript/stylesheets/base.pcss
```
Next, add some CSS:
```css
/* app/javascript/stylesheets/base.pcss */
h1 {
font-size: 2.2em;
color: #2563eb;
}
p {
font-size: 1.2em;
}
```
Lastly, update `application.css`:
```css
/* app/javascript/packs/application.css */
@import "../stylesheets/base.pcss";
```
Reload your browser and your styles should be applied now, and the Webpacker loader error should be gone.
[1]: https://webpack.js.org/loaders/postcss-loader/ "postcss-loader"
---
# Webpacker 6: CSS Loaders
> How to process CSS in Webpacker 6 with css-loader, style-loader, and mini-css-extract-plugin. Part of the archived Webpacker 6 upgrade guide.
> **This page has changed since first posted**, refer to the changelog at the bottom.
In order to process `.css` files with Webpacker 6, you need to add [css-loader][1], [style-loader][2], and [mini-css-extract-plugin][3].
## Install
```bash
yarn add css-loader style-loader mini-css-extract-plugin
```
## Usage
Add a `stylesheet_packs_with_chunks_tag` or `stylesheet_pack_tag` to the document head.
Make sure you restart `bin/webpack-dev-server` after installing new loaders.
### Style Loader Example
```diff
<%%# app/views/layouts/application.html.erb %>
+ <%%= stylesheet_packs_with_chunks_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%%= javascript_packs_with_chunks_tag 'application', 'data-turbolinks-track': 'reload' %>
```
### Extract Example
```diff
<%%# app/views/layouts/application.html.erb %>
<%%= javascript_packs_with_chunks_tag 'application', 'data-turbolinks-track': 'reload' %>
+ <%%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
```
```diff
// app/javascript/packs/application.js
+ import "./application.css"
```
## Verify
> Note: Make sure you restart the dev server!
Letβs create a new file for our CSS:
```diff
touch app/javascript/packs/application.css
```
Next, add some CSS:
```css
/* app/javascript/packs/application.css */
h1 {
font-size: 2.2em;
color: #2563eb;
}
p {
font-size: 1.2em;
}
```
Reload your browser and your styles should be applied now, and the Webpacker loader error should be gone.
## Changelog
- [chore: add css-minimizer-webpack-plugin](https://github.com/andrewmcodes/andrewm-codes-website/pull/12/commits/6b50b3e1a08236a09cd836c97066ddd4e3b76eed)
[1]: https://webpack.js.org/loaders/css-loader/
[2]: https://webpack.js.org/loaders/style-loader/ "style-loader"
[3]: https://github.com/webpack-contrib/mini-css-extract-plugin "mini-css-extract-plugin"
[4]: https://webpack.js.org/plugins/css-minimizer-webpack-plugin "css-minimizer-webpack-plugin"
---
# Webpacker 6: Tailwind CSS 2.0 Integration
> How to add Tailwind CSS 2.0 to a Rails 6 app running Webpacker 6 with PostCSS. Archived, since Webpacker is no longer maintained.
In order to add Tailwind CSS 2.0 to your Rails 6 + Webpacker 6 application, you need PostCSS set up, plus a few additional steps.
Tailwind CSS has [detailed documentation on preprocessor usage][1] so refer to that for further setup.
> Note: This section builds on the PostCSS section
## Install
```bash
yarn add tailwindcss
```
### Add Tailwind CSS Config File
```bash
yarn tailwind init
```
### Update PostCSS Config
```diff
// postcss.config.js
module.exports = {
plugins: [
require('postcss-import'),
+ require('tailwindcss'),
require('autoprefixer')
]
}
```
## Usage
**You should be able to use the same pack tag that you added for CSS.**
Make sure you restart `bin/webpack-dev-server` after installing new loaders.
### Style Loader Example
```diff
<%%# app/views/layouts/application.html.erb %>
+ <%%= stylesheet_packs_with_chunks_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%%= javascript_packs_with_chunks_tag 'application', 'data-turbolinks-track': 'reload' %>
```
### Extract Example
```diff
<%%# app/views/layouts/application.html.erb %>
<%%= javascript_packs_with_chunks_tag 'application', 'data-turbolinks-track': 'reload' %>
+ <%%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
```
```diff
// app/javascript/packs/application.js
+ import "./application.css"
```
## Verify
> Note: Make sure you restart the dev server!
Letβs create a new PostCSS file:
```bash
touch app/javascript/stylesheets/base.css
echo "@import 'tailwindcss/base';" >> app/javascript/stylesheets/base.css
touch app/javascript/stylesheets/utilities.css
echo "@import 'tailwindcss/utilities';" >> app/javascript/stylesheets/utilities.css
touch app/javascript/stylesheets/components.css
echo "@import 'tailwindcss/components';" >> app/javascript/stylesheets/components.css
```
Next, add some CSS:
```diff
/* app/javascript/stylesheets/base.css */
@import "tailwindcss/base";
+ h1 {
+ font-size: 2.2em;
+ color: #2563eb;
+ }
+
+ p {
+ font-size: 1.2em;
+ }
```
Lastly, update `application.css`:
```css
/* app/javascript/packs/application.css */
@import "../stylesheets/base.css";
@import "../stylesheets/components.css";
@import "../stylesheets/utilities.css";
```
Reload your browser and your styles should be applied now with Tailwind CSS, and the Webpacker loader error should be gone.
[1]: https://tailwindcss.com/docs/using-with-preprocessors
---
# Webpacker 6: Upgrade Guide
> Upgrade a Rails app from Webpacker 5 to 6: the Gemfile bump, the install task, and the new pack tags. Part of the archived Webpacker 6 guide.
> **This page has changed since first posted**, refer to the changelog at the bottom.
In this article, we will walkthrough how to upgrade to the latest version of [Webpacker][1], which is, at the time of writing, `6.0.0.beta.2`.
Our upgrade process will begin with updating the Webpacker libraries, then we will update our configs and templates, and end up verifying that our new setup is working.
Letβs get started!
## Updating our `Gemfile`
Update the gem in your `Gemfile`:
```diff
# Gemfile
- gem 'webpacker', '~> 5.0'
+ gem 'webpacker', '~> 6.0.0.beta.2'
```
Next, run `bundle install` to install the new gem version. If all goes well, you should see `Using webpacker 6.0.0.beta.2 (was 5.2.1)` in the install output.
## Installing in our Application
Run the installation command, `bin/rails webpacker:install`, to generate the required configuration files, as well as update our `package.json`
## Update Document Head
Lastly, let's update `app/views/layouts/application.html.erb`. The docs for Webpacker v6 recommend using the `javascript_packs_with_chunks_tag` tag.
```diff
<%%# app/views/layouts/application.html.erb %>
- <%%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
- <%%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
+ <%%= javascript_packs_with_chunks_tag 'application', 'data-turbolinks-track': 'reload' %>
```
## Verify Installation
Run the Rails server (`bin/rails s`) and the Webpack Dev Server (`bin/webpack-dev-server`) via your preferred method. Two terminal tabs will work or create a [Procfile][2] and run via [overmind][3] or [foreman][4]. The Rails server will also compile your assets if the dev server is not running, but this is much slower vs running separate processes and not recommended.
Visit `http://localhost:3000` in your browser. If all's well, you should see the contents of `app/views/pages/home.html.erb`.
We can verify our JavaScript is getting loaded by adding the following to `app/javascript/packs/application.js`:
```js
// app/javascript/packs/application.js
console.log("Hello from Webpacker!");
```
Open the browser console and reload the page and you should see the message we added:
```js
[Log] Hello from Webpacker! (application-7fbebc85af7886af0a64.js, line 62)
```
## Summary
Congrats! Youβre up and running with Webpacker 6!
Unfortunately you will quickly realize that your upgrade is not finished if you begin developing like you usually would with Webpacker.
Webpacker 6 requires you to add the [appropriate Webpack loaders][5] yourself, which is a breaking change from previous versions.
We will tackle that [in the next article!](/p/webpacker-6-css-loaders/)
## Changelog
- [feat: updates for webpacker v6.0.0.beta.2](https://github.com/andrewmcodes/andrewm-codes-website/pull/11)
[1]: https://github.com/rails/webpacker/releases "Webpacker"
[2]: https://devcenter.heroku.com/articles/procfile
[3]: https://github.com/DarthSim/overmind
[4]: https://github.com/ddollar/foreman
[5]: https://webpack.js.org/loaders/
---
# Webpacker 6: Tutorial Setup
> Set up a demo Rails 6.1 app to follow along with the archived Webpacker 6 upgrade series, from new app to root route.
Before we start the upgrade process for Webpacker 6, we are going to create a small demo application for us to work on.
If you are upgrading an existing app or not using this series as a tutorial, you can skip this step! We will begin the formal upgrade process in the next article.
## Generate a new Rails app
First we will generate new Ruby on Rails app:
```sh
rails new webpacker_6 --skip-sprockets --skip-spring --skip-webpack-install --skip-bundle
cd webpacker_6
```
- `--skip-sprockets`: Skip Sprockets files
- `--skip-spring`: Don't install Spring application preloader
- `--skip-bundle`: Don't run bundle install
- `--skip-webpack-install`: Don't run Webpack install
### Setup the Database
```sh
bin/rails db:prepare
```
### Turn off asset scaffolding
Prevent Rails from creating asset files when running the generators and scaffolds:
```diff
# config/application.rb
# ...
module Webpacker6
class Application < Rails::Application
config.load_defaults 6.1
+ config.generators do |g|
+ g.assets false
+ end
end
end
```
## Add Pages Controller
Generate pages controller with a home action:
```sh
bin/rails g controller pages home
```
## Add Root Route
Set `pages#home` as the root route:
```diff
# config/routes.rb
Rails.application.routes.draw do
get 'pages/home'
+ root to: 'pages#home'
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end
```
> Note: Because we skipped the Webpacker install task, you will get an error if you try to start the application as is. We will fix that in the next article.
---
# Webpacker 6
> An archived guide to setting up and upgrading Webpacker 6 in Rails applications, including loaders, assets, and verification steps.
Webpacker was officially retired before the official v6.0.0 was released following [the release of Rails 7.](https://rubyonrails.org/2021/12/15/Rails-7-fulfilling-a-vision) [Shakapacker](https://github.com/shakacode/shakapacker) is the official, actively maintained successor to Webpacker now.
Since the final version was never released, this post has been archived and will not receive further updates. The original upgrade guide was in multiple steps, but have been combined here for convenience.
---
Before we start the upgrade process for Webpacker 6, we are going to create a small demo application for us to work on.
If you are upgrading an existing app or not using this series as a tutorial, you can skip this step! We will begin the formal upgrade process in the next article.
## Generate a new Rails app
First we will generate new Ruby on Rails app:
```sh
rails new webpacker_6 --skip-sprockets --skip-spring --skip-webpack-install --skip-bundle
cd webpacker_6
```
- `--skip-sprockets`: Skip Sprockets files
- `--skip-spring`: Don't install Spring application preloader
- `--skip-bundle`: Don't run bundle install
- `--skip-webpack-install`: Don't run Webpack install
### Setup the Database
```sh
bin/rails db:prepare
```
### Turn off asset scaffolding
Prevent Rails from creating asset files when running the generators and scaffolds:
```diff
# config/application.rb
# ...
module Webpacker6
class Application < Rails::Application
config.load_defaults 6.1
+ config.generators do |g|
+ g.assets false
+ end
end
end
```
## Add Pages Controller
Generate pages controller with a home action:
```sh
bin/rails g controller pages home
```
## Add Root Route
Set `pages#home` as the root route:
```diff
# config/routes.rb
Rails.application.routes.draw do
get 'pages/home'
+ root to: 'pages#home'
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end
```
> Note: Because we skipped the Webpacker install task, you will get an error if you try to start the application as is. We will fix that in the next article.
---
## Updating our `Gemfile`
Update the gem in your `Gemfile`:
```diff
# Gemfile
- gem 'webpacker', '~> 5.0'
+ gem 'webpacker', '~> 6.0.0.beta.2'
```
Next, run `bundle install` to install the new gem version. If all goes well, you should see `Using webpacker 6.0.0.beta.2 (was 5.2.1)` in the install output.
## Installing in our Application
Run the installation command, `bin/rails webpacker:install`, to generate the required configuration files, as well as update our `package.json`
## Update Document Head
Lastly, let's update `app/views/layouts/application.html.erb`. The docs for Webpacker v6 recommend using the `javascript_packs_with_chunks_tag` tag.
```diff
<%%# app/views/layouts/application.html.erb %>
- <%%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
- <%%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
+ <%%= javascript_packs_with_chunks_tag 'application', 'data-turbolinks-track': 'reload' %>
```
## Verify Installation
Run the Rails server (`bin/rails s`) and the Webpack Dev Server (`bin/webpack-dev-server`) via your preferred method. Two terminal tabs will work or create a [Procfile][2] and run via [overmind][3] or [foreman][4]. The Rails server will also compile your assets if the dev server is not running, but this is much slower vs running separate processes and not recommended.
Visit `http://localhost:3000` in your browser. If all's well, you should see the contents of `app/views/pages/home.html.erb`.
We can verify our JavaScript is getting loaded by adding the following to `app/javascript/packs/application.js`:
```js
// app/javascript/packs/application.js
console.log("Hello from Webpacker!");
```
Open the browser console and reload the page and you should see the message we added:
```js
[Log] Hello from Webpacker! (application-7fbebc85af7886af0a64.js, line 62)
```
---
In order to process `.css` files with Webpacker 6, you need to add [css-loader][6], [style-loader][7], and [mini-css-extract-plugin][8].
## Install
```bash
yarn add css-loader style-loader mini-css-extract-plugin
```
## Usage
Add a `stylesheet_packs_with_chunks_tag` or `stylesheet_pack_tag` to the document head.
Make sure you restart `bin/webpack-dev-server` after installing new loaders.
### Style Loader Example
```diff
<%%# app/views/layouts/application.html.erb %>
+ <%%= stylesheet_packs_with_chunks_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%%= javascript_packs_with_chunks_tag 'application', 'data-turbolinks-track': 'reload' %>
```
### Extract Example
```diff
<%%# app/views/layouts/application.html.erb %>
<%%= javascript_packs_with_chunks_tag 'application', 'data-turbolinks-track': 'reload' %>
+ <%%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
```
```diff
// app/javascript/packs/application.js
+ import "./application.css"
```
## Verify
> Note: Make sure you restart the dev server!
Letβs create a new file for our CSS:
```diff
touch app/javascript/packs/application.css
```
Next, add some CSS:
```css
/* app/javascript/packs/application.css */
h1 {
font-size: 2.2em;
color: #2563eb;
}
p {
font-size: 1.2em;
}
```
Reload your browser and your styles should be applied now, and the Webpacker loader error should be gone.
---
In order to process `.pcss` files with Webpacker 6, you need to add [postcss-loader][10]. I am also going to add PostCSS 8 support.
## Install
```bash
yarn add postcss-loader postcss@latest autoprefixer@latest postcss-import@latest
```
### Add PostCSS Config File
```bash
touch postcss.config.js
```
```bash
// postcss.config.js
module.exports = {
plugins: [
require('postcss-import'),
require('autoprefixer')
]
}
```
## Usage
**You should be able to use the same pack tag that you added for CSS.**
Make sure you restart `bin/webpack-dev-server` after installing new loaders.
### Style Loader Example
```diff
<%%# app/views/layouts/application.html.erb %>
+ <%%= stylesheet_packs_with_chunks_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%%= javascript_packs_with_chunks_tag 'application', 'data-turbolinks-track': 'reload' %>
```
### Extract Example
```diff
<%%# app/views/layouts/application.html.erb %>
<%%= javascript_packs_with_chunks_tag 'application', 'data-turbolinks-track': 'reload' %>
+ <%%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
```
```diff
// app/javascript/packs/application.js
+ import "./application.css"
```
## Verify
> Note: Make sure you restart the dev server!
Letβs create a new PostCSS file:
```bash
mkdir app/javascript/stylesheets
touch app/javascript/stylesheets/base.pcss
```
Next, add some CSS:
```css
/* app/javascript/stylesheets/base.pcss */
h1 {
font-size: 2.2em;
color: #2563eb;
}
p {
font-size: 1.2em;
}
```
Lastly, update `application.css`:
```css
/* app/javascript/packs/application.css */
@import "../stylesheets/base.pcss";
```
Reload your browser and your styles should be applied now, and the Webpacker loader error should be gone.
---
In order to process `.scss` and `.sass` files with Webpacker 6, you need to add [sass-loader and sass][11].
> Note: This section builds on the [CSS section](https://andrewm.codes/blog/webpacker-6-css-loaders/)
## Install
```bash
yarn add sass-loader sass
```
## Usage
**You should be able to use the same pack tag that you added for CSS.**
Make sure you restart `bin/webpack-dev-server` after installing new loaders.
### Style Loader Example
```diff
<%%# app/views/layouts/application.html.erb %>
+ <%%= stylesheet_packs_with_chunks_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%%= javascript_packs_with_chunks_tag 'application', 'data-turbolinks-track': 'reload' %>
```
### Extract Example
```diff
<%%# app/views/layouts/application.html.erb %>
<%%= javascript_packs_with_chunks_tag 'application', 'data-turbolinks-track': 'reload' %>
+ <%%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
```
```diff
// app/javascript/packs/application.js
+ import "./application.scss"
```
## Verify
> Note: Make sure you restart the dev server!
Letβs create a new SCSS file:
```bash
touch app/javascript/packs/application.scss
```
Next, add some SCSS:
```css
/* app/javascript/packs/application.scss */
$body-background: #fafafa;
$body-color: #444;
body {
background: $body-background;
color: $body-color;
font-family: sans-serif;
}
h1,
nav,
footer {
text-align: center;
}
main {
margin: 4rem auto;
max-width: 60rem;
}
```
Reload your browser and your styles should be applied now, and the Webpacker loader error should be gone.
---
In order to use your images and SVG files with Webpacker 6, you need to put them in the correct place and import them into your context.
## Install
We should be good here.
## Usage
### Add Assets
```sh
mkdir -p app/javascript/media/images
```
### Require Context
```diff
// app/javascript/packs/application.js
+
+ function importAll(r) {
+ r.keys().forEach(r);
+ }
+ // Add relevant file extensions as needed below.
+ // I'm sure there is a better way :shrug:
+ importAll(require.context('../media/images/', true, /\.(svg|jpg)$/));
```
## Verify
> Note: Restart the dev server for good luck!
Add an SVG and PNG into `app/javascript/media/images`
In one of your views, add two image tags:
```erb
```
Reload your browser and you should see your images.
Note that `<%%= asset_pack_path 'media/images/icon.svg' %>` only returns a string, so if you would rather inline your SVG files you will need to refer to the [Webpack Asset Modules documentation][12] and merge your changes into your Webpack context, as explained in [these Webpacker docs][13].
[2]: https://devcenter.heroku.com/articles/procfile
[3]: https://github.com/DarthSim/overmind
[4]: https://github.com/ddollar/foreman
[6]: https://webpack.js.org/loaders/css-loader/
[7]: https://webpack.js.org/loaders/style-loader/ "style-loader"
[8]: https://github.com/webpack-contrib/mini-css-extract-plugin "mini-css-extract-plugin"
[10]: https://webpack.js.org/loaders/postcss-loader/ "postcss-loader"
[11]: https://webpack.js.org/loaders/sass-loader/
[12]: https://webpack.js.org/guides/asset-modules/#inlining-assets
[13]: https://github.com/rails/webpacker#webpack-configuration
---
# gem install mysql2
> How to fix the mysql2 gem's 'library not found for -lssl' native extension error on macOS using cmake and OpenSSL build flags.
I've come across this error several times throughout my development career so I figured it was finally time to write it down.
## Scenario
Whenever I try to install certain versions of the `mysql2` gem in a Ruby on Rails application, I get the following error:
```
Gem::Ext::BuildError: ERROR: Failed to build gem native extension
...
make "DESTDIR="
compiling client.c
compiling infile.c
compiling mysql2_ext.c
compiling result.c
compiling statement.c
linking shared-object mysql2/mysql2.bundle
ld: library not found for -lssl
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [mysql2.bundle] Error 1
make failed, exit code 2
An error occurred while installing mysql2 (0.5.2), and Bundler cannot continue.
Make sure that `gem install mysql2 -v '0.5.2'` succeeds before bundling.
```
## Solution
In order to fix this issue on macOS, first make sure that you have `cmake` installed.
```sh
brew install cmake
```
Then you can install the gem via the following command:
```sh
gem install mysql2 -v '0.5.2' -- --with-ldflags=-L/usr/local/opt/openssl/lib --with-cppflags=-I/usr/local/opt/openssl/include
```
Hope this helps save someone some time!
---
# Ruby's Shovel Method: Digging Deeper
> A short, fun look at Ruby's shovel operator (<<): what it does on arrays and strings, and why you can chain it even though you probably shouldn't.
> Heads up! This is not _actually_ a deep dive π¬
With everything going on in the world, I almost forgot how fun it can be to code in Ruby! I am not being sarcastic, its _actually_ really fun!
## What kind of fun?
Earlier, I was reviewing some code in a PR to [Bridgetown](https://github.com/bridgetownrb/bridgetown), and I came across this change:
```diff
- static_template_files << "/Gemfile"
+ static_template_files.push "/Gemfile", "/package.json"
```
Whenever I see small method changes during code review, I find it a helpful mind exercise to consider whether the method actually _needed_ to be changed since there's almost always _a way_ in Ruby. More often than not, this leads me to my favorite type of Ruby: quirky, fun, and a bit magical.
You could even call it _blursed_...π€
And if we are really being honest, blursed Ruby is my favorite type of Ruby.
## `Array#<<`
Commonly referred to as the "shovel operator", `<<` is a method in Ruby that is commonly used to push an object onto an array, but you can shovel into strings as well.
For example:
```ruby
%w[foo bar] << "baz"
#=> ["foo", "bar", "baz"]
```
> Speaking of blursed Ruby, [check out the example code in the docs for `str << int`](https://ruby-doc.org/core-2.6/String.html#method-i-3C-3C).
## TIL!
Today I `/re(learned|membered)/` you can chain shovels! Not that you _should_....but just in case, know that you _can_.
The following is a blursed example of just that:

[View gist](https://gist.github.com/andrewmcodes/2e4ba1d60016e065155f0509d3814234)
I realized I was grinning from ear to ear while writing this code. This code will run just fine if you paste it into IRB or Pry, emojis and all! A fun example of what you _can_ do with Ruby, if you wanted to.
## Back to code review
Just because you _can_ do it doesn't mean you should! The readability of swapping the shovel operator with the `Array#push` method is the right call in my opinion. The change got a green check of approval from me.
But, I couldn't resist sharing my blursed example. Hopefully it makes you grin as well.
As I said at the beginning: it's fun to write Ruby!
Happy coding!
---
# How I Use VSCode
> A snapshot of my Visual Studio Code setup: the settings and extensions I use day to day, shared via How I VSCode.
Iβm about to overhaul my settings and plugins in VSCode so this is my βbeforeβ shot.
I recently updated and shared my [Visual Studio Code settings](https://howivscode.com/andrewmcodes) on [How I VSCode](https://howivscode.com).
Check them out and share yours as well!
Really loving this tool because now I can always point people towards tools I like without having to copy it from a gist somewhere or be on my computer.
What plugins are you really enjoying? Any new ones? Which ones do you no longer need as the software matures?
Happy (VS) Coding!
---
# 15 Resources I Learned Something From This Weekend
> A weekend roundup of 15 things worth your time: five Ruby on Rails blog posts, five open source projects, and five podcast episodes.
I wanted to get a lot of writing done this weekend but unfortunately I had to take care of a cranky app instead.
It's been awhile since I have posted though, and since I know how my brain works, there was some inner worry that I'd keep slipping out of my (once regular) habit of writing.
For my own mental health, I promised myself I would post something, but I needed to do it quickly. After digging through my browser history for something, it dawned on me that there was a lot of great content in there that some of y'all may find interesting as well!
Without further adieu, here are 5 blog posts, 5 projects, and 5 podcasts that I read/listened to this weekend:
## Blog Posts
- [Rails Performance: When is Caching the Right Choice? - Honeybadger Developer Blog](https://www.honeybadger.io/blog/rails-caching-alternatives/)
- [Churn vs. Complexity vs. Code Coverage - FastRuby.io | Rails Upgrade Service](https://www.fastruby.io/blog/code-quality/churn-vs-complexity-vs-coverage.html)
- [A 1588x Render Speed Increase With Solr Caching on Rails to Solve All Your Performance Issues - NRoweGT: Atlanta Ruby on Rails Consultancy](http://blog.nrowegt.com/use-and-abuse-solr-caching-on-rails-to-solve-all-performance-your-issues/)
- [Code Audit: How to Ensure the Quality of Your Ruby on Rails Application](https://rubygarage.org/blog/how-to-do-code-audit-for-ruby-on-rails-apps)
- [Will Johnson | How Does The Model Interact With The Database In Ruby on RailsHow Does The Model Interact With The Database In Ruby on Rails | Will Johnson](https://williamjohnson.dev/how-does-the-model-interact-with-the-database/)
## Projects
- [gregnavis/active_record_doctor](https://github.com/gregnavis/active_record_doctor)
- [ViliusLuneckas/rails-cache-inspector](https://github.com/ViliusLuneckas/rails-cache-inspector)
- [soutaro/steep](https://github.com/soutaro/steep)
- [ParamagicDev/snowpacker](https://github.com/ParamagicDev/snowpacker)
- [foambubble/foam](https://github.com/foambubble/foam)
## Podcasts
- [Should You Interview Job Candidates Via Slack Or TikTok? | FounderQuest](https://www.founderquestpodcast.com/episodes/should-you-interview-job-candidates-via-slack-or-tiktok)
- [#166 Country of Liars | Reply All](https://gimletmedia.com/shows/reply-all/llhe5nm/166-country-of-liars)
- [5by5 | Ruby on Rails Podcast #337: Chipping Away at a Monolith with Tori Huang](https://5by5.tv/rubyonrails/337)
- [How open source saved htop (The Changelog #413)](https://changelog.com/podcast/413)
- [The Art of Product: 144: Launching SavvyCalpauseplayskip-backwardskip-forward](https://artofproductpodcast.com/episode-144)
## Summary
After that list, any guesses as to what I've been up to recently?
If you found something on this list interesting, let me know! I'd love to chat about your thoughts about any of the above resources so reply in the comments or [tweet at me!](https://twitter.com/andrewmcodes).
Hopefully I can finish a bigger post I'm working on by next weekend, but for now, happy coding!!
---
# 8 Tailwind CSS resources to help your next project takeoff
> Eight Tailwind CSS resources to speed up your next project: component libraries, typography and layout helpers, and developer-experience plugins.
Let's face it, no matter whether it's our first or hundredth time, staring at a blank Tailwind CSS is intimidating. The enormity of the task at hand starts to crash over you as the excitement begins to build. Here are eight resources that I reach for first when I need to move quickly or want inspiration that may prove helpful to you.
At the end of list I will let you know which of the following I add to every Tailwind CSS project, NO MATTER WHAT!
## Theme Components
- [praveenjuge/kutty](https://github.com/praveenjuge/kutty)
- [estevanmaito/windmill-dashboard](https://github.com/estevanmaito/windmill-dashboard)
## Text & Typography Components
- [jhta/tailwindcss-truncate-multiline](https://github.com/jhta/tailwindcss-truncate-multiline)
- [tailwindlabs/tailwindcss-typography](https://github.com/tailwindlabs/tailwindcss-typography)
## Layout Components
- [tailwindlabs/tailwindcss-custom-forms](https://github.com/tailwindlabs/tailwindcss-custom-forms)
- [Log1x/tailwindcss-container-sizes](https://github.com/Log1x/tailwindcss-container-sizes)
## Bonus: Developer XP
- [rogden/tailwind-config-viewer](https://github.com/rogden/tailwind-config-viewer)
- [jorenvanhee/tailwindcss-debug-screens](https://github.com/jorenvanhee/tailwindcss-debug-screens)
Out of all of these great resources, I find [Tailwind Debug Screens](https://github.com/jorenvanhee/tailwindcss-debug-screens) to be the most helpful, most of the time. In addition to the time you will save by using the plugin, it will also help you create better responsive designs and become more familiar with sizing in Tailwind by sight.
Happy coding!
---
# How to inline SVG files in your Bridgetown site
> A short tutorial on how to inline SVG files in your Bridgetown site with bridgetown-svg-inliner.
A short tutorial on how to use [bridgetown-svg-inliner](https://github.com/ayushn21/bridgetown-svg-inliner) to inline SVG assets on your [Bridgetown](https://bridgetownrb.com) website.
Previously this post was about [andrewmcodes/bridgetown-inline-svg](https://github.com/andrewmcodes/bridgetown-inline-svg) but that plugin has since been put into maintenance mode in favor of this MIT licensed library by [Ayush Newatia.](https://twitter.com/ayushn21)
## Prerequisites
This tutorial assumes that you already have a Bridgetown website up and running. If you don't, check out [Bridgetown's Quick Instructions](https://www.bridgetownrb.com/docs/#quick-instructions) for instructions on how to generate your first site.
## Install
Run the following command to install the [plugin](https://github.com/ayushn21/bridgetown-svg-inliner) in your site:
```bash
bundle add "bridgetown-svg-inliner" -g bridgetown_plugins
```
## Usage
Bridgetown creates an images folder by default at `src/images`, which you can use to store your SVG assets or you can create your own folder like `assets`, just make sure it is nested under `src/`.
For this tutorial, I used the annotation outline icon from [Heroicons](https://heroicons.com).
### Basic Usage
Create a SVG called `icon.svg` at `src/images/icon.svg` and call it using the Ruby helper or Liquid tag:
```erb
<%%= svg "/images/icon.svg" %>
```
```liquid
{% svg "/images/icon.svg" %}
```
This will inline the SVG as is in your file once the page renders.
## Advanced Usage
You can pass options to the plugin which will then be applied to the resulting element. If it already exists in your SVG file, it will be overwritten.
For example, this snippet:
```erb
<%%= svg "/images/icon.svg", class: "foo", width: "100%" %>
```
```liquid
{% svg "/images/icon.svg", class: "foo", width: "100%" %}
```
will result in the following HTML:
```html
```
## Reference
- [ayushn21/bridgetown-svg-inliner](https://github.com/ayushn21/bridgetown-svg-inliner)
---
# Creating a blog with Bridgetown and Netlify CMS
> A step-by-step tutorial on adding Netlify CMS to a Bridgetown site so you can edit and publish content from a Git-backed admin UI.
This is a quick tutorial to showcase how you can quickly integrate Netlify CMS into your [Bridgetown](https://www.bridgetownrb.com) site.
The code for this tutorial can be found at:
[andrewmcodes/bridgetown-netlify-cms-starter](https://github.com/andrewmcodes/bridgetown-netlify-cms-starter)
Let's get started!
### Setup
For detailed instructions on getting Bridgetown set up on your local machine, take a look at the [Bridgetown Getting Started Documentation](https://www.bridgetownrb.com/docs/) and the [Bridgetown Installation Guides](https://www.bridgetownrb.com/docs/installation).
The TL;DR is you need Ruby `>= 2.5`, Bundler, Node `>= 10.13`, Yarn, and the Bridgetown gem installed.
You can install the gem by running the following command in your terminal:
```bash
gem install bridgetown
```
As far as the other dependencies go, you don't have to use the same versions that I am as long as you meet the minimum requirements above, but this is what I am currently using:
- Ruby 2.7.1
- Bundler 2.4.1
- Node 13.11.0
- Yarn 1.22.4
### Creating a new Bridgetown site
The first thing we are going to do is generate a new Bridgetown site.
Run the following command in your terminal:
```bash
bridgetown new bridgetown-netlify-cms-starter
cd bridgetown-netlify-cms-starter
```
Our new site has been generated! :tada:
Let's take a look! Run `yarn start` in your terminal and open `http://localhost:4000` in your browser.

#### Optional Styling
Just to make this a little prettier, I am going to add [new.css](https://newcss.net), which just adds styles to your default HTML. If you'd like to do the same, add the following in your head component at `src/_components/head.liquid`:
```html
```
And remove the contents of `frontend/styles/index.scss`.
### Adding Netlify CMS to your site
_I won't be going in great depth about the specific features of Netlify CMS, I encourage taking a looking at the [Netlify CMS Documentation](https://www.netlifycms.org/docs/intro/) to learn more._
We are going to create an `admin` folder with two files: `index.html` and `config.yml`:
```bash
mkdir src/admin
touch src/admin/index.html
touch src/admin/config.yml
```
Paste the following inside of `src/admin/index.html`:
```html
Netlify CMS
```
And in `src/admin/config.yml`
```yaml
backend:
name: git-gateway # required for using Github
branch: main # the default branch you want CMS changes merged to
commit_messages: # Optional: configure the commit messages Netlify CMS will use when publishing changes
create: "feat({{collection}}): :sparkles: Create {{slug}}"
update: "chore({{collection}}): :recycle: Update {{slug}}"
delete: "chore({{collection}}): :recycle: Delete {{slug}}"
uploadMedia: "feat(assets): :bento: Upload {{path}}"
deleteMedia: "chore(assets): :wastebasket: Delete {{path}}"
local_backend: true # Enable the CMS locally
media_folder: src/images/uploads # location of where we want images uploaded via the CMS put
collections:
- name: blog # collection name
label: Blog # label in the CMS
folder: src/_posts/ # location of the files that make up the collection
extension: .md # extension of those files
format: frontmatter # format to use
create: true # allow creation of new items in this collection
slug: "{{year}}-{{month}}-{{day}}-{{title}}" # the slug to use when creating new items
editor:
preview: false # According to the documentation, this won't work with our setup, but I didn't try
fields: # Fields for the collection
- { label: Layout, name: layout, widget: hidden, default: post }
- { label: Title, name: title, widget: string }
- { label: Publish Date, name: date, widget: datetime }
- { label: Body, name: body, widget: markdown }
- name: pages
label: Pages
editor:
preview: false
files:
- label: Index Page
name: index
file: src/index.md
fields:
- { label: Layout, name: title, widget: hidden, default: home }
- { label: Body, name: body, widget: markdown }
- label: About Page
name: about
file: src/about.md
fields:
- { label: Title, name: title, widget: hidden, default: About }
- { label: Layout, name: layout, widget: hidden, default: page }
- { label: Permalink, name: permalink, widget: string, default: "/about/" }
- { label: Body, name: body, widget: markdown }
```
For more information on these config options, checkout the [Netlify CMS Configuration Options Documentation](https://www.netlifycms.org/docs/configuration-options/)
I decided what fields needed to be used by looking at the frontmatter in the example pages that Bridgetown created with the new site.
For our posts, this looks like:
```md
---
title: "Your First Post on Bridgetown"
category: updates
---
```
and in our config for Netlify CMS:
```yaml
fields:
- { label: Layout, name: layout, widget: hidden, default: post }
- { label: Title, name: title, widget: string }
- { label: Publish Date, name: date, widget: datetime }
- { label: Body, name: body, widget: markdown }
```
I neglected to add category, which would be [a great contribution to this repository](https://github.com/andrewmcodes/bridgetown-netlify-cms-starter) if you are interested!
You should now be able to navigate to `http://localhost:4000/admin` in your browser and see this page:

In order to use the CMS locally, run `npx netlify-cms-proxy-server` in a separate terminal window or run `yarn add -D netlify-cms-proxy-server` and modify `start.js`:
```diff
concurrently([
{ command: "yarn webpack-dev", name: "Webpack", prefixColor: "yellow"},
{ command: "sleep 4; yarn serve --port " + port, name: "Bridgetown", prefixColor: "green"},
- { command: "sleep 8; yarn sync", name: "Live", prefixColor: "blue"}
+ { command: "sleep 8; yarn sync", name: "Live", prefixColor: "blue"},
+ { command: "sleep 12; yarn netlify-cms-proxy-server", name: "CMS", prefixColor: "red"}
], {
restartTries: 3,
killOthers: ['failure', 'success'],
}).then(() => { console.log("Done.");console.log('\033[0G'); }, () => {});
```
Now the CMS will start with the rest of your server. You can play with it locally and check for errors, but the real power is once we get this live!
### Create GitHub repo
Create a new GitHub repository and push this code to your default branch. If you have the [GitHub CLI](https://github.com/cli/cli), that process would look something like:
```bash
# I am using main as my default branch
gco -b main
git add .
git commit -m "feat: :tada: Initial" -m "Initial commit"
gh repo create bridgetown-netlify-cms-starter --public
git push --set-upstream origin main
```
### Create Netlify site
1. Log in to Netlify
1. Press the 'New site from Git' button
1. Choose your repository
1. Set your build command to `yarn deploy`
1. Set the publish directory to `output`
1. Deploy site

### Netlify Identity
In order to log in to our CMS, we need to enable Netlify Identity on the `Identity` tab for our new site.
#### Registration preferences
**Before setting this, make sure you have created your first user to make your life easier (next section)** I would recommend setting this to invite only vs open once you have a configured user.
#### External providers
I find it is way easier to use an external provider (like GitHub) and highly suggest doing the same. There is a weird bug with the invitation links that I haven't solved yet for normal signups and this will remove that headache.
#### Services
Enable the Git Gateway to allow Netlify to connect your site to GitHub's API, which is required for using Netlify CMS.
### Using the CMS
Navigate to your deployed site and go to the `/admin` route. For example, the admin page for this starter is located at `https://bridgetown-netlify-cms-starter.netlify.app/admin`
Your page should look like:

Click the `Continue with GitHub` button. After you authenticate with GitHub, you should be redirected to your CMS!

**Note:** At this point, I would go back to your site settings and set the registration preferences to **invite only**!
### Publishing
From here you should be all set! You can create a new blog post, edit content on your pages, upload images, and more!
After changing the index page for example, hit the `publish` button at the top of the page and publish now.
What this will do is add a commit to your GitHub repo with the changes and if Netlify is set to deploy your default branch (this is default behavior), the Netlify will automatically redeploy the site with the changes.
To go back to your site, change your url to the root, or click the user icon in the top right of the CMS and log out.
**Note:** there is some weird bug that pops up after it logs you out. Either refresh the page or just change the url back to your root url.
After the deploy finishes (it is very quick if you followed along), the content you changed or added should be reflected! I updated the index page, and my site now looks like:

### Wrap up
From this point, you can continue changing your Bridgetown site, and configure the CMS config file as needed. Hopefully this gives you all the excuse you need to try [Bridgetown](https://www.bridgetownrb.com)! If you encounter any issues or find a bug, feel free to report it on the [repository](https://github.com/andrewmcodes/bridgetown-netlify-cms-starter).
You can find the demo for this project [here](https://bridgetown-netlify-cms-starter.netlify.app/).
Happy coding!
---
# Rails 6 Band-Aid for Webpacker::Manifest::MissingEntryError
> A workaround for Webpacker::Manifest::MissingEntryError in the Rails 6 test environment: force Webpacker to compile test packs from your test helper.
At [CodeFund](https://codefund.io), we try to keep our dependencies, including Rails, as up to date as possible. We upgraded to Rails 6 a few months ago, and I've pretty much forgotten any issues we ran into during the upgrade. For what it's worth, the upgrade was very smooth, but there was one issue we ran into that was lost in my memories before a friend showed me a familiar error message today:
```sh
# Webpacker::Manifest::MissingEntryError:
# Webpacker can't find foo/bar in /app/public/packs-test/manifest.json. Possible causes:
# 1. You want to set webpacker.yml value of compile to true for your environment
# unless you are using the `webpack -w` or the webpack-dev-server.
# 2. webpack has not yet re-run to reflect updates.
# 3. You have misconfigured Webpacker's config/webpacker.yml file.
# 4. Your webpack configuration is not creating a manifest.
# Your manifest contains:
# {
# }
```
The error essentially is letting you know that Webpacker tried to locate the `foo/bar` asset entry in the `manifest.json` file that gets generated when Webpacker compiles your test assets, but it could not find it. Even more interesting, the manifest is completely empty.
It is worth noting that I _only ran into this issue in my test environment_, which is exactly what was happening to my buddy. If your Webpacker config is close to the default, assets are precompiled into `public/packs-test/*`, which you an see on the second line of the error above.
An issue was created on Webpacker's GitHub repo that basically describes this scenario. It is [issue #1494](https://github.com/rails/webpacker/issues/1494) if you are curious. However, all of the solutions that worked for others were not working for me, nor my friend who had also found the same issue.
At the time, I figured that we had incorrectly configured something and eventually gave up trying to find the cause and instead focused on finding a solution. It is also worth noting that we are using Minitest at CodeFund, but my friend was using Rspec, which rules out one of my earlier ideas that this could be a Minitest specific issue.
I am still not quite sure the cause, and one of my hopes for posting this article is that someone else has run across this and actually fixed the core issue instead of the band-aid I chose to go with.
If you encounter this type of error in your tests while upgrading to Rails 6, here is one way of solving it:
```ruby
# test/test_helper.rb
unless Webpacker.compiler.fresh?
puts "== Webpack compiling =="
Webpacker.compiler.compile
puts "== Webpack compiled =="
end
```
Adding this method in your `test_helper` or `spec_helper` before the tests run will force Webpacker to make sure the test-packs are present and up to date, and if not it will compile them. If you do some source diving, you will see this comment above the `fresh?` method in `Webpacker::Compiler`:
> Returns true if all the compiled packs are up to date with the underlying asset files.
The reason the manifest is empty is because Webpacker needs to be compiled, because the currently compiled packs are not up to date or were never generated in the first place. Webpacker is supposed to do this automatically, but for my friend and I, it wasnβt.
You can see the usage of this in [CodeFund's `test_helper.rb`](https://github.com/gitcoinco/code_fund_ads/blob/5f9a7165b7a49ed73a81c7987e8a13ba18f9e0a6/test/test_helper.rb#L22). If you try to run the test suite and `public/packs-test` has not been created or the test packs are not up to date, you will see this in your terminal:
```sh
β bin/rails test
== Webpack compiling ==
== Webpack compiled ==
# Running tests with run options --seed 1234:
...
```
As to why this is happening, I am still not sure. I am still leaning towards this being caused from a misconfiguration or some behavior specific to our setup, but after being asked about it today, I figured it was worth sharing in case you run across it in your own app.
If you have run into this before and fixed the underlying cause, please leave a comment below or mention me on [Twitter](https://twitter.com/andrewmcodes)! I will make sure to update this post if we up solving it. In the meantime, the solution above is working great for us.
Happy ~~coding~~ debugging!!
---
# Build and deploy a static site with Ruby, Bridgetown, TailwindCSS, and Netlify
> Build a Bridgetown static site with Tailwind CSS and deploy it to Netlify. This older tutorial is archived but still useful.
> **Heads up!** This post is a bit outdated.
- [Demo Repository](https://github.com/andrewmcodes/bridgetown_tailwind)
- [Demo Website](https://bridgetown-tailwind.netlify.app)
## What is Bridgetown
According to their [website](https://www.bridgetownrb.com), Bridgetown is:
> A Webpack-aware, Ruby-powered static site generator for the modern Jamstack era.
You may think of JavaScript when you hear the term "static site generator", but one of the oldest, and most popular static site generators is Jekyll, and it is also built with Ruby. The Jekyll project is over 10 years old, and remains a popular tool. Bridgetown seems to be a fresh take on Jekyll, and brings a nice balance of Ruby and JavaScript.
It is also worth pointing out that Bridgetown is pre version 1.0 at the time of writing. The stability notice on their README is worth keeping in mind:
> Given Bridgetown's strong inherited bones (see background below) and our continued emphasis on good testing, we're pretty confident Bridgetown is ready to use today. Butβ¦you might want to exercise a bit of extra caution using this in production. π We are already (after all, the Bridgetown website is built with Bridgetown), but YMMV.
## Tutorial
Let's create our first static site with Bridgetown!
### Prerequisites
Make sure you have Ruby, Bundler, Node, and Yarn installed. These are the versions I am using:
```sh
β ruby -v
ruby 2.6.6p146 (2020-03-31 revision 67876) [x86_64-darwin19]
β bundler -v
Bundler version 2.1.4
β node -v
v13.11.0
β yarn -v
1.22.4
```
### Install Bridgetown
```sh
gem install bridgetown -N
```
### Create new project
```sh
bridgetown new bridgetown_tailwind
cd bridgetown_tailwind
yarn start
```
You can now view your site live at http://localhost:4000/
### Tailwind
Let's add TailwindCSS to our new site:
```sh
yarn add -D tailwindcss postcss-import postcss-loader
yarn tailwind init
```
This will create a `tailwind.config.js` file at the root of our directory.
We will want to run PurgeCSS on our files, so update `tailwind.config.js` to be:
```js
module.exports = {
purge: {
mode: "production",
content: ["./src/**/*.html"],
},
theme: {
extend: {},
},
variants: {},
plugins: [],
};
```
Next, we need to update our Webpack config file to use PostCSS.
In `webpack.config.js`, change:
```js
{
test: /\.(s[ac]|c)ss$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
{
loader: "sass-loader",
options: {
sassOptions: {
includePaths: [
path.resolve(__dirname, "src/_components"),
path.resolve(__dirname, "src/_includes"),
],
},
},
},
],
},
```
to:
```js
{
test: /\.(s[ac]|c)ss$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
{
loader: "sass-loader",
options: {
sassOptions: {
includePaths: [
path.resolve(__dirname, "src/_components"),
path.resolve(__dirname, "src/_includes"),
],
},
},
},
{
loader: "postcss-loader",
options: {
ident: "postcss",
plugins: [
require("postcss-import"),
require("tailwindcss"),
require("autoprefixer"),
],
},
},
],
},
```
Lastly, we need to import Tailwind into our stylesheet.
Open `./frontend/styles/index.scss` and replace the default styles with:
```css
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
```
If we run `yarn start` again, we should see Tailwind styles being applied!
### Update styles
This step is optional but we can update some of our styles if we want.
Here is what I did:
```css
// frontend/styles/index.scss
body {
display: flex;
min-height: 100vh;
flex-direction: column;
}
main {
flex: 1;
}
```
```html
{% include head.html %}
{% include navbar.html %}
{{ content }}
{% include footer.html %}
```
```html
--- layout: default ---
```
```html
```
If you added these styles, your homepage should now look like:

### Deployment with Netlify
1. Login to Netlify
2. Select `New Site from Git` 
3. Choose your Git provider
4. Select your repo
5. Set your `Build Command` to `yarn deploy`
6. Set your `Publish directory` to `output/` 
7. Click `Deploy site`
Your site will deploy and you should be able to view it at the preview link that Netlify provides!
## Summary
If you have had Ruby/Rails/Jekyll experience, you should feel right at home with Bridgetown. Bridgetown also removes the barrier to entry for integrating webpack and all the goodies the JavaScript community has to offer.
Even though the library is still pre 1.0, I think it would still be worth your time to check out [Bridgetown](https://www.bridgetownrb.com) and see what you think!
You can find the code for this tutorial [here](https://github.com/andrewmcodes/bridgetown_tailwind) or view [demo](https://bridgetown-tailwind.netlify.app).
Happy coding!
---
# Instantly speed up your Rails application by self-hosting your fonts
> Improve Rails page speed by self-hosting web fonts instead of relying on third-party font CDNs.
A font can make or break your design, and as a result many of us are probably not using the default system fonts. [Google Fonts](https://fonts.google.com) makes it really easy to find the perfect font, but it can come with a performance cost. If you are loading a font directly from Google, the following tutorial is guaranteed to speed up your Rails application.
Make your Rails app do this:

(GET IT?!?)
To demonstrate, let's build a quick demo app.
This is a tutorial, but I will assume you have a basic understanding of Ruby on Rails. If not, and you need me to elaborate on anything, let me know in the comments.
## Create a new Rails project
```sh
rails new self_hosted_webfonts_demo --skip-sprockets --skip-spring
cd self_hosted_webfonts_demo
```
I am using Rails 6.0.2.2, which comes default with Webpacker 4.2.2, but I want to take advantage of features in v5, so I am going to update the gem and node package to v5.1.1. You are not required to do this in your application, but will need to if you are following along with this tutorial. Make sure you run `bundle install && yarn install`.
After we have upgraded webpacker, let's create a basic Welcome controller, and set the index as the root route in `config/routes.rb`:
```sh
bin/rails generate controller welcome index
```
```rb
# config/routes.rb
Rails
.application
.routes
.draw do
get "welcome/index"
root "welcome#index"
end
```
## Choose a font
Now that we have a landing page, we should snazz it up a bit with a nice font. If you're like me, this is usually when you head over to [Google Fonts](https://fonts.google.com). To keep it simple, I am going to use [Lato](https://fonts.google.com/specimen/Lato). Since I am not too sure all the styles and weights I need right now, I will just go ahead and select all the available styles (sound familiar?) and copy the link that Google provides.

Now that we have our fonts, letβs add the link to the head of our application in `app/views/layouts/application.html.erb` on the line above your `stylesheet_link_tag` (line #7 if you are on a fresh Rails app):
```html
```
While we are here, let's change `stylesheet_link_tag` to `stylesheet_pack_tag` and create our application styles file:
```diff
- <%%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track": "reload" %>
+ <%%= stylesheet_pack_tag "application", media: "all", "data-turbolinks-track": "reload" %>
```
```sh
touch `app/javascript/packs/application.scss`
```
Inside of `application.scss`, add the following CSS rules to specify the font family:
```scss
// app/javascript/packs/application.scss
html {
font-family: "Lato", sans-serif;
}
```
Now if we start the Rails server (`bin/rails s`), and navigate to `localhost:3000`, we should see our simple landing page being rendered with our nice, new font.

## The Problem...
Even though our view looks much better with the new font, we have just degraded the performance of our application and introduced a render blocking resource. When we load Lato, we are actually loading a stylesheet, and the browser will not render our page until it finishes retrieving the file from Google's servers.
Introducing a render blocking resource isn't great, but what's worse is we are now relying on Google to send us that file for us to render our page. Users will now be waiting longer for the page to load, and that time will fluctuate depending on how much traffic Googleβs servers are handling.

Not great. Lighthouse isn't happy about it either.
However, there are solutions to this problem. I am going to show you the method I believe is the fastest to implement and easiest to understand, but understand there are several other fixes you could use instead with their own pros and cons.
## The Solution
Enter the [typefaces](https://www.bricolage.io/typefaces-easiest-way-to-self-host-fonts/) project from Gatsby founder [Kyle Mathews](https://twitter.com/kylemathews). I highly recommend reading about his motivations behind the project, but the TL;DR is we can use Webpacker to install fonts on our server and self-host them ourselves instead of relying on Google.
Since I am already hosting everything else on my server, it makes perfect sense in a Rails environment to take this approach - and it's super quick to swap out Google Fonts for this solution.
A quick search on NPM for `typeface lato` will reveal the package we are looking for, which we can easily install:
```sh
yarn add typeface-lato
```
Now let's remove the old way we were getting the font:
```diff
-
```
And the last step is requiring the package in our `application.js` pack:
```js
// app/javascript/packs/application.js
require("typeface-lato");
```
If we fire the Rails server back up and checkout `localhost:3000`, the font should still be Lato! A quick look at the `webpacker-dev-server` logs will reveal that we are now self-hosting the same font styles and weights that we were before:

Let's see if we have fixed the performance regression via Lighthouse:

Lighthouse is no longer reporting a render blocking resource, we have boosted our performance!
## Summary
We should be good to go! This is a simple, quick migration, which will reduce your time to first meaningful paint, and overall performance. It is also easily overlooked (speaking from personal experience).
It is worth noting that these Lighthouse audits were run against the Rails development server, and are not a true substitute for running them in production, but should give us a good enough idea of where we are at. For more accurate results, you should run these audits in production or start the application in production mode locally.
This change also positions you to make further enhancements, like requiring the fonts in a separate JavaScript pack, which will allow you to take advantage of `javascript_packs_with_chunks_tag`. I will leave that for you to explore, but you can see an example, along with the code for this tutorial, [here](https://github.com/andrewmcodes/self_hosted_webfonts_demo).
Hopefully this was helpful! If you have taken another approach, I would be curious to hear about it in the comments.
Happy coding!
---
# Rails Coverage Tools: CodeFactor
> Add CodeFactor to a Ruby on Rails application for automated code review, repository analysis, and README status badges.
## [CodeFactor](https://codefactor.io)
According to their documentation:
> CodeFactor instantly performs Code Review with every GitHub Commit or PR. Zero setup time. Get actionable feedback within seconds. Customize rules, get refactoring tips and ignore irrelevant issues.
In addition to automated code review, CodeFactor also has auto-fix functionality, which is pretty cool.
For Rails apps specifically, CodeFactor can check:
- Yamllint
- ESLint
- stylelint
- Rubocop
If this sounds interesting, let's look at how to set this up.
## Tutorial
We will be creating a demo app to showcase how to utilize CodeFactor on your projects. The completed code can be found [here](https://github.com/andrewmcodes/codefactor_demo) if you'd like to just look over that.
If you'd like to build it together, let's get started!
### Setup
Let's create a new Rails app and `cd` into it:
```sh
rails new codefactor_demo
cd codefactor_demo
```
### Create Repository
Open GitHub and create a new repository. I named mine `codefactor_demo`.
Open your command line again and let's upstream our code.
```sh
git add .
git commit -m "first commit"
git remote add origin https://github.com/YOUR_USERNAME/codefactor_demo.git
git push -u origin master
```
Your code should now be online in your repo.
### Configuration
Navigate to [codefactor.io](https://codefactor.io) and log in with your preferred method. I chose to use my GitHub account.

Once logged in, you should be taken to your dashboard.
### Add Repository
Let's add a new repository. From your CodeFactor dashboard, click `Add`, next to `Repositories`:

You will be taken to a screen that will let you search and select your desired repo. I am adding our demo project repo:

Click the `Import` button to import the repository.
Once your repository has been imported, it will show up on your dashboard:

If we click on our repo, we will be taken to a show page for our repo:

From here you can look at information about your repository and configure settings for the tools CodeFactor will use to check your repo.
### README Badge
If we would like to add the CodeFactor README badge to our project, click the badge in the top right corner of the project page:

This will open a modal with a few format options for our badge. I simply copied the markdown code and pasted it on my README.
This badge should update as your code quality changes according to CodeFactor.
## Summary
CodeFactor is a neat tool if you'd like to run some standard linters on your Rails project, like Rubocop and ESLint. The unfortunate part is that it doesn't look like you can add in tools other than the ones provided. The auto-fix functionality is really helpful if you'd not only like to run the linters but add a commit to the branch that fails checks.
Overall, I think this is a tool worth checking out. However, since I don't personally use the available tools for Rails projects, it wasn't as helpful to me personally as I hoped. Hopefully you will find different!
### Helpful links
- [CodeFactor](https://www.codefactor.io)
- [Demo repo](https://github.com/andrewmcodes/codefactor_demo)
- [CodeFactor show page for demo repo](https://www.codefactor.io/repository/github/andrewmcodes/codefactor_demo)
- [CodeFactor Default Configs](https://github.com/codefactor-io/default-configs)
Happy coding!
P.S. If you aren't sure how to set up ESLint, Rubocop, or the other listed linters, leave a comment or message me on [Twitter](https://twitter.com/andrewmcodes) and let me know if you'd like a post about this!
---
# A11Y in Rails: Automated Linting with AccessLintπ
> Add automated accessibility (a11y) linting to a Ruby on Rails app with AccessLint, which flags issues on every pull request before they ship.
## AccessLint
According to their documentation, [AccessLint] is:
> AccessLint brings automated web accessibility testing into your development workflow. When a pull request is opened, AccessLint reviews the changes and comments with any new accessibility issues, giving you quick, timely, and targeted feedback, before code goes live.
We will be creating a demo app to showcase how to utilize AccessLint on your projects. The completed code can be found [here.]
### Setup
Let's create a new Rails app and `cd` into it:
```sh
rails new access_lint_demo
cd access_lint_demo
```
Install dependencies:
```sh
bundle install
yarn install
```
And setup the database:
```sh
bin/rails db:setup
```
Now, let's start the Rails server:
```shell
rails s
```
If you want to run the `webpack-dev-server`, run this in another tab:
```shell
bin/webpack-dev-server
```
If you navigate to `localhost:3000` in your browser, you should see the Rails welcome page:
![rails_welcome_page]
### Create Repository
Open GitHub and create a new repository. I named mine `access_lint_demo`.
Open your command line again and let's upstream our code.
```sh
git add .
git commit -m "first commit"
git remote add origin https://github.com/YOUR_USERNAME/access_lint_demo.git
git push -u origin master
```
Your code should now be online in your repo.
### Configure AccessLint
Navigate to [AccessLint] in your browser, and click `Sign in with Github`:
![access_lint_home_page]
After you authenticate with GitHub, you should be redirected back to the AccessLint setup page. Click `Set up a new installation`:
![access_lint_setup]
You should get redirected to the AccessLint app on the GitHub Marketplace. Click `Open Source` under the `Pricing and setup` header, and then `Install it for free`:
![github_marketplace]
Choose whether you want to install the AccessLint app for all your repos or specifically select your demo repo, and accept the permissions.
AccessLint should now be installed!
![access_lint_dashboard]
### Testing
Let's test it out on a new branch. Run the following in your terminal:
```sh
git checkout -b access-lint-test
```
This should create a new branch in your demo repo. Now, let's scaffold some code:
```sh
bin/rails g scaffold Post title:string content:text
bin/rails db:migrate
```
This will scaffold out some resources for us and add `Post` to our database schema. Most importantly, it will create some new views.
Restart your Rails server and open `localhost:3000/posts` to make sure everything is working correctly
![posts_index_page]
Let's also make a change to `app/views/posts/_form.html.erb` that will trigger a failing lint. We are going to add an inaccessible image to the Post index page:
Add the following to `app/views/posts/index.html.erb`:
```html
```
Since this image does not have an `alt` attribute, it should be flagged by AccessLint.
Let's commit this code to see if that is correct:
```sh
git add .
git commit -m "create Post resource"
git push --set-upstream origin access-lint-test
```
Now open the repo on GitHub and open a pull request for these changes:
![github_new_pr]
AccessLint should run automatically if we have set it up correctly. After it runs, it should flag our missing `alt` attribute:
![failing_access_lint]
Let's follow the instructions AccessLint has given us to fix the issue and add an `alt` tag to our image:
```html
```
Let's commit this code to see if that fixes the issue:
```sh
git add .
git commit -m "add alt attribute to image on Post#index"
git push
```
If all is well, the AccessLint check should now pass!
![passing_access_lint]
## Summary
AccessLint is a helpful tool if you want to automated web accessibility testing in your Rails app. Unfortunately, the tool is a bit limited currently.
From the documentation:
> Note that server-side code (e.g. image_tag and label_tag in Rails) is not evaluated. Only fully formed HTML tags will be tested.
Regardless, AccessLint is a nice way to start introducing accessibility testing. Accessibility is very important when developing on the web, and this tool will help make sure your code does not prevent users from interacting with your web app. In future posts, we will continue investigate tools to help us with accessibility in our Rails apps.
As mentioned at the beginning of this post, you cannot fully automate accessibility testing away, and none of these tools are substitutes for actually learning accessibility best practices.
## Resources
- [W3C: Accessibility]
- [The A11Y Project]
- [Accessibility on Rails]
[accesslint]: https://accesslint.com
[here]: https://github.com/andrewmcodes/access_lint_demo
[rails_welcome_page]: <%= imagekit_url 'posts/a11y-in-rails-automated-linting-with-accesslint/rails-welcome-page.jpg', :medium %>
[access_lint_home_page]: <%= imagekit_url 'posts/a11y-in-rails-automated-linting-with-accesslint/access-lint-home-page.jpg', :medium %>
[access_lint_setup]: <%= imagekit_url 'posts/a11y-in-rails-automated-linting-with-accesslint/access-lint-setup.jpg', :medium %>
[github_marketplace]: <%= imagekit_url 'posts/a11y-in-rails-automated-linting-with-accesslint/github-marketplace.jpg' %>
[access_lint_dashboard]: <%= imagekit_url 'posts/a11y-in-rails-automated-linting-with-accesslint/access-lint-dashboard.jpg', :medium %>
[posts_index_page]: <%= imagekit_url 'posts/a11y-in-rails-automated-linting-with-accesslint/posts-index-page.jpg', :medium %>
[github_new_pr]: <%= imagekit_url 'posts/a11y-in-rails-automated-linting-with-accesslint/github-new-pr.jpg', :medium %>
[failing_access_lint]: <%= imagekit_url 'posts/a11y-in-rails-automated-linting-with-accesslint/failing-access-lint.jpg' %>
[passing_access_lint]: <%= imagekit_url 'posts/a11y-in-rails-automated-linting-with-accesslint/passing-access-lint.jpg' %>
[w3c: accessibility]: https://www.w3.org/standards/webdesign/accessibility
[the a11y project]: https://a11yproject.com
[accessibility on rails]: https://reinteractive.com/posts/355-accessibility-on-rails
---
# Rails Coverage Tools: Coverband
> Add Coverband to a Rails application to measure production code usage and find unused Ruby code, gems, and views.
## Coverband
According to their documentation, Coverband is:
> A gem to measure production code usage, showing a counter for the number of times each line of code that is executed. Coverband allows easy configuration to collect and report on production code usage. (...) The primary goal of Coverband is giving deep insight into your production runtime usage of your application code, while having the least impact on performance possible
TL;DR
Coverband gives you insights into what code is being loaded in your Rails application, which you can also think of as code that is being used by the application. This can be a very useful tool for refactoring, and removing code that is no longer used or needed.
## Demo
I will be working from [this repo](https://github.com/andrewmcodes/rails_coverage_tools). You can look through that code, or you can follow along by using [my Rails app template](https://github.com/andrewmcodes/rails_template) I used to create this project.
The rest of the tutorial will assume you already have a functioning Rails app!
## Installation
Add the Coverband gem to your `Gemfile`. I personally decided to keep it in my development group, because I didn't see any value in having the dashboard available in production.
```ruby
# Gemfile
gem "coverband", group: :development
# or
group :development do
gem "coverband"
end
```
Then run:
```sh
bundle install
```
## Configuration
### Redis
Coverband stores coverage data in Redis. According to the documentation, the Redis endpoint is looked for in this order:
```ruby
ENV["COVERBAND_REDIS_URL"]
ENV["REDIS_URL"]
localhost
```
You can also specifically set this in a Coverband initializer file. If you are using the template that Iβm using then you should be all set, otherwise make sure you are running Redis locally, bundle the `redis` gem, and your app can access it.
### Initializer
Let's configure the gem. Create an initializer file for Coverband, which should look like the following:
```ruby
# config/coverband.rb
Coverband.configure do |config|
config.ignore += %w[config/application.rb config/boot.rb config/puma.rb bin/* config/environments/* lib/tasks/*]
end
```
These settings just tell Coverband to ignore our specified files and paths.
### Route
Now, lets add a route for coverband so we can view the web dashboard:
```ruby
# config/routes.rb
Rails.application.routes.draw { mount Coverband::Reporters::Web.new, at: "/coverband" if Rails.env.development? }
```
It is worth noting that if you are running this tool in production, you should protect this route with proper authentication.
## Usage
We should be all set to see what Coverband can provide us!
Fire up the Rails server and navigate to `localhost:3000/coverband`
Alternatively, you can run the following Rake task and static files will be created in `coverage/`. I would recommend making sure this directory is added to your `.gitignore`.
```sh
rake coverband:coverage
```
You should now be seeing is Coverband's mountable web interface to easily view Coverband reports.

If we click on a file with 0% coverage, you will see the following message:
> This file was never loaded during app runtime or loading (or was loaded before Coverband loaded)!
Basically this mean the code inside the file has not been loaded, and therefore not used.
If we click on a file with partial coverage, you should see something like:

This view will highlight the lines that have been used, and those that haven't. Take care before you start ripping out code though, it's possible that you just haven't exercised that code yet.
In the `posts_controller` file in my example above, the code inside our `new` and `create` methods is not being used. I am going to open up the UI, and create a new post. You will notice that the coverage report looks a little different now:

It is important to exercise some due diligence before removing code that Coverband flags. In the example above, after some investigation I realized I in fact do not need `config/spring.rb` because I am not using `spring` in this project. This is the power of this library, the ability to point you towards areas in your codebase that may be safe to remove; however, if I had removed the flagged code in the `posts_controller` then I would be in some trouble.
### Tracking Gems
It is also possible to use Coverband to track gem usage. This is still in experimental stages and not recommended for production according to the docs.
To see it in action, first let's update our initializer:
```ruby
# config/initializers/coverband.rb
Coverband.configure do |config|
config.track_gems = true
config.ignore += %w[config/application.rb config/boot.rb config/puma.rb bin/* config/environments/* lib/tasks/*]
end
```
According to the docs:
> When tracking gems, it is important that Coverband#start is called before the gems to be tracked are required. The best way to do this is to require coverband before Bundle.require is called
So lets update `application.rb` to make sure coverband is loaded before `Bundler.require` is called:
```ruby
# config/application.rb
require "coverband"
Bundler.require(*Rails.groups)
```
Restart the Rails server and you should now have a gem tab if you navigate back to `localhost:3000/coverband`.

This can help give you insight into gems that may be safe to remove.
### Tracking Views
There is a config option to watch your views, but it was not working for me on `Rails 6.0.2.1` and `Ruby 2.7` so I won't go into it now.
See [the advanced configuration documentation](https://github.com/danmayer/coverband#advanced-config) for more information.
## Summary
Coverband is a great tool to help you find code in your Rails app that may be safe to remove. The tool is not perfect though, so take care that the code can actually be safely removed. I personally used this tool on [CodeFund's](https://github.com/gitcoinco/code_fund_ads) codebase and found some code that could be removed! And who doesn't like deleting code? Definitely recommend adding this tool to your tool belt.
#### Links
- [coverband gem](https://github.com/danmayer/coverband#advanced-config)
- [rails template](https://github.com/andrewmcodes/rails_template)
- [repo for this post](https://github.com/andrewmcodes/rails_coverage_tools)
Happy Coding!
---
# Stopping a runaway Rails server
> How to stop a runaway Ruby on Rails server that won't quit on ctrl-c, using the shutup gem to kill it with a single command.
_Many of us have been there. You hit `ctrl-c` on your Ruby on Rails server, but nothing happens. No matter what keys you hit on your keyboard, the Rails server is still running, and you can't stop it. You have a runaway train on your hands._
## The Problem
If you have ever developed with Ruby on Rails, there is a good chance you have encountered a runaway Rails server. This is basically an instance of the Ruby on Rails server that you cannot easily stop.
Two examples of when you may need this is if you try to start your Rails server and get an error message that one is already running, or you get into a weird state with pry and `ctrl-c` won't stop the server in a timely manner.
Regardless of how you got to this point isn't really important, you have a runaway train on your hands, and you need to stop it.
Here is how you can do that:
## Shutup
`shutup` is a gem to help you quickly stop a running Rails server.
To install the gem, make sure you have Ruby installed.
Type the following into your command line:
```sh
gem install shutup
```
Now, whenever you have a Rails server you want to stop, just type the following in your command line to shut it down:
```sh
shutup
```
If the command succeeded, you should see something like this:
```sh
β shutup
Killed process id: 46707
```
If it fails, you will see:
```sh
β shutup
Error reading the pid file.
```
## Conclusion
You could achieve the same effect with Bash or ZSH aliases, or just running the entire process by hand, but this gem removes the need to do that. It's a simple gem, but it's one I install whenever I install a new version of Ruby.
Check it out at: [lorenzosinisi/shutup](https://github.com/lorenzosinisi/shutup)
Happy coding!!
---
# Hiding Ruby 2.7 Deprecation Warnings in Rails 6
> Three ways to silence noisy Ruby 2.7 deprecation warnings in a Rails 6 app using the RUBYOPT environment variable.
If you have upgraded your Rails app to Ruby 2.7, you are probably seeing a lot of deprecation messages in your console. You should first make sure that none of these messages are coming from your code, and address them if they are! If the deprecations are coming mostly from Rails, it may be time to disable the messages and save yourself from messy terminal output.
The TL;DR is that you need to use `RUBYOPT='-W:no-deprecated -W:no-experimental'` to disable the deprecations. This will also disable experimental feature warnings as well.
Here are some options you have to make that happen. But first, lets create a new Rails app to experiment with!
## Generate a new Rails app with Ruby 2.7
You can either use the CLI or [this template](https://github.com/andrewmcodes/rails_template/generate).
If you use the CLI, I recommend something like:
```sh
rails new silence_ruby_2_7_deprecations -d postgresql --webpack=stimulus
```
## Method #1: Using `dotenv-rails`
If you are using the [dotenv-rails](https://github.com/bkeepers/dotenv/) gem, or another method of using `.env` files, simply add the following to your `.env` file:
```sh
export RUBYOPT='-W:no-deprecated -W:no-experimental'
```
Then run the following in the root of the project:
```sh
source .env
```
You should no longer be seeing the Ruby 2.7 deprecation warnings coming out of Rails! π
## Method #2: Prefixing commands
Another option you have for ignoring the Ruby 2.7 deprecation warnings is to prefix all of your Rails commands with `RUBYOPT='-W:no-deprecated -W:no-experimental'`.
Example:
- `rails server` would become `RUBYOPT='-W:no-deprecated -W:no-experimental' rails server`
- `rails console` would become `RUBYOPT='-W:no-deprecated -W:no-experimental' rails console`
- etc.
This is obviously not ideal but it will work!
## Method #3: Updating your environment
If you want to disable these deprecation messages everywhere, you can add the following to your `~/.zshrc` or `~/.bashrc`:
`export RUBYOPT='-W:no-deprecated -W:no-experimental'`
This will disable deprecation and experimental feature warnings for all versions of Ruby, for all projects. I have heard of this creating issues for some so you may want to be careful using this method if you work on multiple apps that aren't on Ruby 2.7.
[View repo for this post](https://github.com/andrewmcodes/silence-ruby-2-7-deprecations)
Hopefully this helps! Happy coding!
---
# Dare To Give Your Junior Developers Permission To Fail
> My rejected CFP for RailsConf 2020 about how to be a better mentor to junior developers.
## Abstract
You must be willing to let your junior developers try, and possibly fail, at implementing patterns or tools they feel strongly about. This creates an environment that fosters mentorship, rapid growth, and respect. Less experienced developers must be willing to push boundaries, take risks, and fail and the right environment will help them feel safe and supported as they learn and grow. This talk will focus on how to be a good mentor, and the qualities you should seek when searching for a mentor.
## Details
### Outline
- Introduction
- My journey from junior to where I am now after only 2 years of experience of Rails
- Brief story about when my mentor, Nate Hopkins, gave me space to experiment and what an impact that had on my imposter syndrome and confidence
- Explain what kind of space juniors need to grow
- Why failure is important
- Fostering mutual respect and creating a safe space
- Encouraging experimentation
- Benefits of creating an environment that encourages creativity
- Mental health and burnout
- Turning failures into teaching moments
- Walking beside, instead of in front of, juniors
- Conclusion
### Desired Outcomes
The main takeaway for mentors and senior developers is a better understanding, or gentle reminder, that they can help level up junior developers by getting out of their way and giving them the space to explore their ideas. For juniors, this talk will give them an insight into what a great mentor is, and what they should be looking for in a mentor.
### Intended Audience
This talk is aimed at mentors and junior developers.
## Pitch
The more experience you have, the further removed you are from the needs of your junior developers. With only two years experience, I have a fresh perspective on the positive and negative mentorship tactics that many juniors are currently being exposed to. The main premise behind this talk comes from a discussion I had with my team lead about a pattern I had introduced that others didn't seem to like. The takeaway from that conversation is my lead wanted to give me the space to explore my ideas. If I succeeded, we would have a great new pattern to use moving forward. If I failed, it wasn't something we couldn't recover from, and I would have learned a great deal. The key to these two possible outcomes is that regardless of whether I succeeded or failed in my attempt, my mentor was excited to help me realize the lessons I learned from the experiment that would help me to become a better developer moving forward.
The other reason this talk should be considered is because juniors and mids need to see someone like them on the stage. I have been to two RailsConf's and one RubyConf and took a lot out of the many talks I attended but none of them look like me. Most of the speakers are older, well respected Rubyists and it's hard to justify taking the time to submit a talk when you think only talks from these more experienced developers will be accepted. By giving this talk, my hope is that I inspire other developers with similar experience to submit their conference talk ideas and continue growing the Ruby community.
[Mirror](https://speakerline.io/proposals/7652)
---
# How to set up Ruby on Rails 6 and TailwindCSS 1.1.4
> Build a Rails 6 app with Tailwind CSS 1.1.4, including setup, Webpack, scaffolding, configuration, and styled views.
## Tutorial
For the purpose of this tutorial, we will assume you have Ruby and the Rails gem installed. Please visit the [Getting Started with Rails Guide](https://guides.rubyonrails.org/getting_started.html) if you do not.
I will also be working through the creation of code in [this repo](https://github.com/andrewmcodes/tailwind_css_1_1_4_rails_demo). Please use the repo as a resource to help you in case you get stuck or open issues if it's broken!!
I created this demo using the following:
- Ruby 2.7
- Node 13.7.0
- Rails 6.0.2.1
- Webpacker 4.2.2
- TailwindCSS 1.1.4
- psql (PostgreSQL) 12.1
## Create a new Rails project
```sh
rails new tailwind_css_rails_demo -d postgresql
cd tailwind_css_rails_demo
rails db:create
```
This will create a new Ruby on Rails project with PostgreSQL configured for you. You can omit the `-d postgresql` flag if you would prefer to use SQLite or MySQL.
## Running Rails and Webpack
I prefer to run the Rails server in one command line tab and webpack-dev-server in another since it's much faster. In development, Rails can tell whether the webpack-dev-server has compiled your packs and will compile them inline if it has not been done.
Let's go ahead and get the app running:
```sh
# Terminal tab 1
rails s
```
And webpack-dev-server:
```sh
# Terminal tab 2
./bin/webpack-dev-server
```
You should now see Rails welcome page if you navigate to `localhost:3000` in your browser.

## Generate a new resource
I personally like to see more than one record in the database for tutorials to make the app seem more "real". You can skip this part if you are not interested in adding some seed data, and would rather create a Home controller or something similar like [in the previous tutorial](https://dev.to/andrewmcodes/use-tailwind-css-1-0-in-your-rails-app-4pm4).
If you would like some records in your database, lets scaffold out a small resource:
```sh
rails generate scaffold Post title:string content:text
```
Rails will then generate several files for us, but we will only focus on a few.
Use the following command to run the generated migration:
```sh
rails db:migrate
```
Now, lets add some seed data. Open `db/seeds.rb` and add the following:
```ruby
# db/seeds.rb
10.times { |n| Post.create!(title: "Post title - ##{n}", content: "This is the content for the #{n.ordinalize} post.") }
```
It's not important for this tutorial for you to fully grok that code, but I would be happy to explain it in more detail if you reach out or let me know in the comments. The TL;DR is that we now have 10 unique Post records in our database.
The last thing we need to do before getting to the fun part is to update our `config/routes.rb` file to make the root path for the app the index page for posts.
```ruby
# config/routes.rb
Rails
.application
.routes
.draw do
resources :posts
root to: "posts#index"
end
```
Restart the Rails server, navigate to `localhost:3000`, and you should see a table with our random data, with links to other CRUD actions.
## Install TailwindCSS
Now to the fun stuff.
Run the following command in your terminal to install TailwindCSS
```sh
yarn add tailwindcss
```
Let's also add the Tailwind config file:
```sh
./node_modules/.bin/tailwind init
```
This should create a `tailwind.config.js` file at the root of your project. This file can be used to customize the TailwindCSS defaults, add plugins, and more. You can learn more about this from [Tailwind's docs](https://tailwindcss.com/docs/configuration)
We also need to update our PostCSS config that comes default with Rails 6 with two new requires:
```js
require('tailwindcss'),
require('autoprefixer'),
```
I have been told the best order for these requires is as I have them below, but I think just adding them to the top of your PostCSS config will work for the majority of people:
```js
// postcss.config.js
module.exports = {
plugins: [
require("autoprefixer"),
require("postcss-import"),
require("tailwindcss"),
require("postcss-flexbugs-fixes"),
require("postcss-preset-env")({
autoprefixer: {
flexbox: "no-2009",
},
stage: 3,
}),
],
};
```
## Configure Tailwind
_There are a few ways you can do this but this is my personal preference._
Remove the assets folder, we won't be needing it since we will rely fully on Webpacker:
```sh
rm -rf app/assets
```
Create a new stylesheet file:
```sh
touch app/javascript/src/application.scss
```
Since we are using `postcss-import` and Webpack, the [Tailwind docs](https://tailwindcss.com/docs/installation/) instruct us to add the following to our stylesheet file:
```scss
// app/javascript/src/application.scss
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
```
We also need to add following line in `app/javascript/packs/application.js`:
```js
import "../src/application.scss";
```
The last step is to tell Rails to use our pack files. In `app/views/layouts/application.html.erb`, change:
```erb
<%%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
```
to:
```erb
<%%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
<%%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
```
Restart the Rails server and webpack-dev-server and you should now see the following on `localhost:3000`:

Tailwind should now be working so lets tweak our views to see some Tailwind goodness.
## Update views to use TailwindCSS
In `app/views/layouts/application.html.erb` change:
```html
<%%= yield %>
```
to:
```html
<%%= yield %>
```
and in `app/views/posts/index.html.erb` lets replace the scaffolded page with the following:
```html
<%%= notice %>
Posts
Title
Content
<%%% @posts.each do |post| %>
<%%= post.title %>
<%%= post.content %>
<%%= link_to 'Show', post %>
<%%= link_to 'Edit', edit_post_path(post) %>
<%%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %>
<%% end %>
<%%= link_to 'New Post', new_post_path %>
```
You should now see the following page when you navigate to `localhost:3000`

## Abstraction
This table looks so much better, but the classes for these elements are long and repetitive. Let's clean that up a bit by abstracting them to our scss file.
Create two new classes in `app/javascript/src/application.scss`:
```scss
.table-header {
@apply px-6 bg-gray-100 text-gray-900 align-middle border border-solid border-gray-200 py-3 text-xs uppercase border-l-0 border-r-0 whitespace-no-wrap font-semibold text-left;
}
.table-content {
@apply border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-no-wrap p-4 text-left;
}
```
Then in our HTML, change all of the table head and body element classes accordingly. We also darkened the header text to make the change standout.
When you reload the page, you should see the same screen as before, only with darker table headings.
## Summary
Hopefully this is helpful to those of you looking to add TailwindCSS to your Rails app.
If you are interested in more information about this article of using Rails and TailwindCSS, leave a comment or reach out to me on Twitter and I am happy to chat.
[View repo for this post](https://github.com/andrewmcodes/tailwind_css_1_1_4_rails_demo)
Happy coding! π
---
# CI for Ruby on Rails: GitHub Actions vs. CircleCI
> The finale of a three-part series on CI for Ruby on Rails, comparing GitHub Actions and CircleCI to help you choose the right setup.
_This is part of a three part series where I will walk you through setting up your CI suite with GitHub Actions, CircleCI, and then comparing which you may want to use if you are setting up continuous integration for your Rails app._
## Part 3: Comparison
You can find the conclusion of this series on the [CodeFund Blog](https://codefund.io/blog/ci-for-ruby-on-rails-github-actions-vs-circleci).
Thanks for following along!
---
# CI for Ruby on Rails: CircleCI
> Part two of a CI for Ruby on Rails series: build a complete CircleCI pipeline with Docker images, caching, database setup, tests, and linters.
_This is part of a three part series where I will walk you through setting up your CI suite with GitHub Actions, CircleCI, and then comparing which you may want to use if you are setting up continuous integration for your Rails app._
## Part 2: CircleCI
### 1. Set the CircleCI version
```yaml
version: 2.1
```
### 2. Create your job(s), and choose a docker image to use
CircleCI offers a lot of images for us to get started [here](https://hub.docker.com/r/circleci/ruby/tags/). Alternatively, you can use their [dockerfile-wizard](https://github.com/CircleCI-Public/dockerfile-wizard) to create your own custom image.
```yaml
jobs:
build:
docker:
- image: circleci/ruby:2.6.5-node-browsers
environment:
PG_HOST: localhost
PG_USERNAME: ubuntu
RAILS_ENV: test
RACK_ENV: test
DEFAULT_HOST: codefund.io
PARALLEL_WORKERS: "1"
REDIS_CACHE_URL: redis://127.0.0.1:6379
REDIS_QUEUE_URL: redis://127.0.0.1:6379
WORDPRESS_URL: "https://codefund.io"
```
This tells our job that we want to run the commands we will define later inside of a container built with the `circleci/ruby:2.6.5-node-browsers` image, and we want the environment variables listed to be inside of that container.
### 3. Define services
For a typical Rails app, you are probably using Redis for caching or tools like Sidekiq, and you also probably have a database. Defining services in your config allows us to use additional containers to run these types of tools. We use Redis in the app, but it is not needed for running the tests.
```yaml
jobs:
build:
docker:
- image: circleci/ruby:2.6.5-node-browsers
environment:
PG_HOST: localhost
PG_USERNAME: ubuntu
RAILS_ENV: test
RACK_ENV: test
DEFAULT_HOST: codefund.io
PARALLEL_WORKERS: "1"
REDIS_CACHE_URL: redis://127.0.0.1:6379
REDIS_QUEUE_URL: redis://127.0.0.1:6379
WORDPRESS_URL: "https://codefund.io"
- image: circleci/postgres:11.2
environment:
POSTGRES_USER: ubuntu
POSTGRES_DB: code_fund_ads_test
```
### 4. Now we have defined our build step, we need to set our working directory
```yaml
working_directory: ~/repo
```
### 5. Add steps
Now it is time to run commands inside of our container. We will start by checking out the code.
```yaml
steps:
- checkout
```
### 6. Add dependencies
We may need to add some additional dependencies in our container. You can do so by using `run` and tools like APT or curl.
```yaml
- run: |
sudo apt-get update
sudo apt-get install -y postgresql-client
curl -o- -L https://yarnpkg.com/install.sh | bash
```
### 7. Caching
Thankfully, CircleCI provides some good documentation for getting started with your tools of choice for caching dependencies. I recommend checking that out, which also has some examples.
[Cache Documentation](https://circleci.com/docs/2.0/caching/)
The first step is to restore the cache from previous builds if it exists.
```yaml
- restore_cache:
name: Restore gem cache
keys:
- gem-cache-v4-{{ arch }}-{{ .Branch }}-{{ checksum "Gemfile.lock" }}
- gem-cache-v4-{{ arch }}-{{ .Branch }}
- gem-cache-v4-{{ arch }}
- gem-cache-v4
- restore_cache:
name: Restore yarn cache
keys:
- yarn-cache-v4-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }}
- yarn-cache-v4-{{ arch }}-{{ .Branch }}
- yarn-cache-v4-{{ arch }}
- yarn-cache-v4
- run:
name: Set up assets cache key
command: find app/javascript -type f -exec md5sum {} \; > dependency_checksum
- restore_cache:
name: Restore assets cache
keys:
- assets-cache-v4-{{ arch }}-{{ .Branch }}-{{ checksum "dependency_checksum" }}
- assets-cache-v4-{{ arch }}-{{ .Branch }}
- assets-cache-v4-{{ arch }}
- assets-cache-v4
```
### 8. Bundle, Yarn, and Precompile Assets
Next, we will want to run Bundler and Yarn to install our dependencies if they were not restored from the cache, and precompile our assets.
```yaml
- run:
name: Install gem dependencies
command: |
gem install bundler:2.1.1
bundle check || bundle install --jobs=6 --retry=3 --path vendor/bundle
- run:
name: Install yarn dependencies
command: yarn install --ignore-engines --frozen-lockfile
- run:
name: Precompile assets
command: RAILS_ENV=test bundle exec rails webpacker:compile
```
NOTE: You may be able to skip the asset compilation, that is up to you.
### 9. Caching our dependencies
Once we have installed our dependencies, we can save the cache.
```yaml
- save_cache:
name: Save gem cache
paths:
- vendor/bundle
key: gem-cache-v4-{{ arch }}-{{ .Branch }}-{{ checksum "Gemfile.lock" }}
- save_cache:
name: Save yarn cache
paths:
- ~/.cache/yarn
key: yarn-cache-v4-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }}
- save_cache:
name: Save assets cache
paths:
- public/packs-test
- tmp/cache/webpacker
key: assets-cache-v4-{{ arch }}-{{ .Branch }}-{{ checksum "dependency_checksum" }}
```
### 10. Setup Database
One last item we need to take care of prior to running the tests and linters is setting up our database.
```yaml
- run:
name: Set up DB
command: bundle exec rails db:drop db:create db:structure:load --trace
```
### 11. Run Tests
Now we can finally run our tests. The first thing we do is run a Zeitwerk check. If this fails, we will want to fail the build. We have added this step due to a bug slipping out that we didn't catch and have found it useful. Next we will run our tests, and save any artifacts from that. We use the [minitest-reporters](https://github.com/kern/minitest-reporters) gem, which will save screenshots of failing system tests, which we will want to see if the build fails. The reason we have `set +e` in there is so that the store artifacts step will run if the system tests fail.
```yaml
- run:
name: Run zeitwerk check
command: bundle exec rails zeitwerk:check
- run:
name: Run tests
command: |
bundle exec rails test
set +e
bundle exec rails test:system
- store_artifacts:
path: tmp/screenshots
destination: screenshots
```
### 12. Run Linters
The last step is to run any linters or other checks you want.
```yaml
- run:
name: Run standardrb check
command: bundle exec standardrb --format progress
- run:
name: Run ERB lint check
command: bundle exec erblint app/views/**/*.html.erb
- run:
name: Run prettier-standard check
command: yarn run --ignore-engines prettier-standard --check "app/**/*.js"
```
Now our config is complete and should look like:
```yaml
version: 2.1
jobs:
build:
docker:
- image: circleci/ruby:2.6.5-node-browsers
environment:
CAMPAIGN_DEMO_ID: "395"
PG_HOST: localhost
PG_USERNAME: ubuntu
RAILS_ENV: test
RACK_ENV: test
DEFAULT_HOST: codefund.io
PARALLEL_WORKERS: "1"
REDIS_CACHE_URL: redis://127.0.0.1:6379
REDIS_QUEUE_URL: redis://127.0.0.1:6379
WORDPRESS_URL: "https://codefund.io"
- image: circleci/postgres:11.2
environment:
POSTGRES_USER: ubuntu
POSTGRES_DB: code_fund_ads_test
working_directory: ~/repo
steps:
- checkout
- run: |
sudo apt-get update
sudo apt-get install -y postgresql-client
curl -o- -L https://yarnpkg.com/install.sh | bash
- restore_cache:
name: Restore gem cache
keys:
- gem-cache-v4-{{ arch }}-{{ .Branch }}-{{ checksum "Gemfile.lock" }}
- gem-cache-v4-{{ arch }}-{{ .Branch }}
- gem-cache-v4-{{ arch }}
- gem-cache-v4
- restore_cache:
name: Restore yarn cache
keys:
- yarn-cache-v4-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }}
- yarn-cache-v4-{{ arch }}-{{ .Branch }}
- yarn-cache-v4-{{ arch }}
- yarn-cache-v4
- run:
name: Set up assets cache key
command: find app/javascript -type f -exec md5sum {} \; > dependency_checksum
- restore_cache:
name: Restore assets cache
keys:
- assets-cache-v4-{{ arch }}-{{ .Branch }}-{{ checksum "dependency_checksum" }}
- assets-cache-v4-{{ arch }}-{{ .Branch }}
- assets-cache-v4-{{ arch }}
- assets-cache-v4
- run:
name: Install gem dependencies
command: |
gem install bundler:2.1.1
bundle check || bundle install --jobs=6 --retry=3 --path vendor/bundle
- run:
name: Install yarn dependencies
command: yarn install --ignore-engines --frozen-lockfile
- run:
name: Precompile assets
command: RAILS_ENV=test bundle exec rails webpacker:compile
- save_cache:
name: Save gem cache
paths:
- vendor/bundle
key: gem-cache-v4-{{ arch }}-{{ .Branch }}-{{ checksum "Gemfile.lock" }}
- save_cache:
name: Save yarn cache
paths:
- ~/.cache/yarn
key: yarn-cache-v4-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }}
- save_cache:
name: Save assets cache
paths:
- public/packs-test
- tmp/cache/webpacker
key: assets-cache-v4-{{ arch }}-{{ .Branch }}-{{ checksum "dependency_checksum" }}
- run:
name: Set up DB
command: bundle exec rails db:drop db:create db:structure:load --trace
- run:
name: Run zeitwerk check
command: bundle exec rails zeitwerk:check
- run:
name: Run tests
command: |
bundle exec rails test
set +e
bundle exec rails test:system
- store_artifacts:
path: tmp/screenshots
destination: screenshots
- run:
name: Run standardrb check
command: bundle exec standardrb --format progress
- run:
name: Run ERB lint check
command: bundle exec erblint app/views/**/*.html.erb
- run:
name: Run prettier-standard check
command: yarn run --ignore-engines prettier-standard --check "app/**/*.js"
```
This is the configuration that we currently use for CodeFund, which you can find [here](https://github.com/gitcoinco/code_fund_ads/blob/master/.circleci/config.yml).
While this setup will work great, there are some enhancements we can add like parallelism, which we will explore in a future post.
_Special thanks to the team at CircleCI for their feedback on this post._
---
# Perfectionism: The Death of Progress
> My rejected CFP for RailsConf 2019 on how to combat perfectionism as a developer.
## Abstract
Perfectionism is a self-destructive, obsessive belief system summarized by the following mantra: if I do everything perfectly, I can avoid painful feelings of imperfection. Perfectionists set high expectations for themselves, and often react poorly when they fail to live up to them. Perfectionists are typically stuck in a cycle where each new task is another opportunity for perceived failure; quickly leading to the development of mental health issues. Perfectionism can lead to a variety of dangerous mental illnesses, such as depression, anxiety, and stress; but it can be managed.
## Details
### Outline
- What is perfectionism
- Dangers of perfectionism
- Overcoming perfectionism
### Desired Outcomes
- Those suffering from perfectionism will gain some new tools to combat this disorder, and others will be made more aware of the signs associated with the disorder and be able to better work with coworkers or peers that suffer from this.
### Intended audience
- Developers of all ages who struggle with perfectionism
- Those who know someone struggling with perfectionism
## Pitch
It is difficult to justify what makes you qualified to speak about a mental health issue, due to its complex and sensitive nature; however, I feel that personal experience is what qualifies me to speak about this topic. As someone with a very type A driven personality, I struggle with perfectionism on a daily basis. I will never forget working on a static website that took me over a year after constant changes to the stack and UI design. Since this project, which ended up getting shut down, I have learned to be aware of my perfectionist tendencies, largely as a result of this project. I ended up getting professional help last spring because of my perfectionism, and the associated mental health issues that arose as a result, because my mental health became so bad that I was ready to shut down my overactive brain at any cost.
[Mirror](https://speakerline.io/proposals/7651)
---
# Use Tailwind CSS 1.1 in your Rails App
> A step-by-step walkthrough of adding Tailwind CSS 1.1 to a new Ruby on Rails app with Webpacker, from the npm install through PostCSS configuration.
For the purpose of this tutorial, we will assume you have Ruby and the Rails gem installed. Please visit the [Getting Started with Rails Guide](https://guides.rubyonrails.org/getting_started.html) if you do not.
## Create a new Rails project
```sh
rails new rails_tailwind --skip-coffee --webpack -d postgresql
cd rails_tailwind
rails db:create
```
This will create a new Rails project for you with webpack and Postgres configured for you and create our databases. We will not use coffeescript, which is why we add the `--skip-coffee` flag. You can also omit the `-d postgresql` flag if you like, but if you want to deploy to something like Heroku, I would recommend adding it. If you keep the Postgres flag, make sure you have Postgres installed and it is running. You can install Postgres on macOS by running `brew install postgresql && brew services start postgresql`
## Running Rails and Webpack
You need to run the Rails server and webpack-dev-server in two terminal tabs/windows unless you use Docker or a gem like Foreman.
For now, we will just create two terminal windows. In one window, run:
```sh
rails s
```
and in the other:
```sh
./bin/webpack-dev-server
```
You should see rails welcome page if you navigate to `localhost:3000` in your browser.

## Generate a Home controller
In order to see the Tailwind styles that we will integrate later, we at minimum need a controller and view.
```sh
rails generate controller Home index
```
You can remove the generated JS, SCSS, and helper file, we won't be needing them.
```sh
rm app/helpers/home_helper.rb app/assets/javascripts/home.js app/assets/stylesheets/home.scss
```
## Configure your routes
Change your `config/routes.rb` file to:
```rb
# frozen_string_literal: true
Rails.application.routes.draw do
root 'home#index'
resources :home, only: :index
end
```
Restart your Rails server, and now you should see the following on `localhost:3000`

## Install Tailwind CSS
Run the following command in your terminal:
```sh
yarn add tailwindcss --dev
```
This should add the Tailwind package to your `package.json`.
To create a custom config file, you can run:
```sh
./node_modules/.bin/tailwind init
```
This should create a `tailwind.config.js` file at the root of your project. This file can be used to customize the Tailwind defaults. Read more [here](https://tailwindcss.com/docs/configuration)
Next, add the following two lines to `postcss.config.js`
```js
require('tailwindcss'),
require('autoprefixer'),
```
Your `postcss.config.js` file should now look like this:
```js
module.exports = {
plugins: [
require("autoprefixer"),
require("postcss-import"),
require("tailwindcss"),
require("postcss-flexbugs-fixes"),
require("postcss-preset-env")({
autoprefixer: {
flexbox: "no-2009",
},
stage: 3,
}),
],
};
```
## Configure Tailwind
_There are a few ways you can do this but this is my personal preference._
Remove the assets folder:
```sh
rm -rf app/assets
```
Rename the `app/javascript` directory to `app/frontend`:
```sh
mv app/javascript app/frontend
```
Tell webpacker to use this new folder by changing the source_path in `config/webpacker.yml` from: `source_path: app/javascript` to `source_path: app/frontend`.
Next, we need to setup our stylesheets:
```sh
touch app/frontend/packs/stylesheets.css
```
Paste the following into our new `stylesheets.css` file. _This is straight from the [tailwind docs](https://tailwindcss.com/docs/installation#step-2-add-tailwind-to-your-css)_
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```
Add the following line in `app/frontend/packs/application.js`:
```js
import "./stylesheets.css";
```
The last step is to tell Rails to use our packs. In `app/views/layouts/application.html.erb`, change:
```erb
<%%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
```
to:
```erb
<%%= stylesheet_pack_tag 'stylesheets', 'data-turbolinks-track': 'reload' %>
<%%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
```
Restart the Rails server and webpack-dev-server and you should now see the following on `localhost:3000`

Tailwind should now be working so lets tweak our views to see some Tailwind goodness.
## Update views to use TailwindCSS
In `app/views/layouts/application.html.erb` change:
```erb
<%%= yield %>
```
to:
```erb
<%%= yield %>
```
and in `app/views/home/index.html.erb` change:
```erb
Home#index
Find me in app/views/home/index.html.erb
```
to:
```erb
Ruby on Rails + TailwindCSS
β€οΈ A match made in heavenοΈοΈ β€οΈ
Tailwind Docs
```
You should now see the following page when you navigate to `localhost:3000`

And now you have Tailwind CSS working in your Rails app!
If you are interested in using PurgeCSS to remove unused styles, I recommend checking out [GoRails Episode #294](https://gorails.com/episodes/purgecss?autoplay=1)
Happy coding!
---
# About
> Ruby on Rails software engineer, podcaster, and creator based in Phoenix, Arizona.
Hey, I'm Andrew. I'm a full stack Ruby on Rails engineer, podcaster, and creator working remotely from Phoenix, Arizona.
I work at [Podia](https://podia.com), where I spend a lot of time thinking about Rails, developer tooling, CI, and fast feedback loops. I've been doing the Rails thing for about a decade now.
I co-host [Remote Ruby](https://remoteruby.com) with Chris Oliver, Jason Charnes, and David Hill. Before that I was on Ruby for All, The Ruby Blend, and Ruby Rogues, plus a handful of guest episodes across the Ruby podcast world.
When I'm not podcasting, I'm usually working in the open. Most of that is Ruby, Rails, [ViewComponent](https://viewcomponent.org), [Bridgetown](https://www.bridgetownrb.com), and smaller projects I built to scratch my own itches.
I grew up in North Carolina, studied Computer Science, and eventually landed in Arizona. Away from the keyboard, I'm usually playing video games, hiking around Phoenix, or tweaking [my setup](/uses/).
## Elsewhere
[Bluesky](https://bsky.app/profile/andrewm.codes) Β· [GitHub](https://github.com/andrewmcodes) Β· [LinkedIn](https://www.linkedin.com/in/andrewmcodes/) Β· [Remote Ruby](https://remoteruby.com)
## Speaking & podcasts
If you'd like me at your conference, on your podcast, or generally in a room with a microphone, [reach out on Bluesky](https://bsky.app/profile/andrewm.codes). I'm slow to reply but I do reply.
## Colophon
This site is built with [Bridgetown](https://www.bridgetownrb.com) (ERB + Bridgetown::Component), Tailwind v4, and [Radix UI Colors](https://www.radix-ui.com/colors). Geist Sans and Geist Mono, with a Mauve and Ruby palette. Search uses a static index. Deployed to Cloudflare Workers Static Assets. Source is on [GitHub](https://github.com/andrewmcodes/andrewm.codes).
---
# Changelog
> A running log of meaningful changes to andrewm.codes. Sourced from CHANGELOG.md in the repo.
## [9.1.1](https://github.com/andrewmcodes/andrewm.codes/compare/v9.1.0...v9.1.1) β 2026-09-12
### Bug Fixes
* **ci:** ignore copilot planning commits in commitlint ([#602](https://github.com/andrewmcodes/andrewm.codes/issues/602)) ([605f529](https://github.com/andrewmcodes/andrewm.codes/commit/605f529520798d4000152e13d1ae72b932366c2a))
### Documentation
* **blog:** add webmentions tutorial ([#598](https://github.com/andrewmcodes/andrewm.codes/issues/598)) ([c066e78](https://github.com/andrewmcodes/andrewm.codes/commit/c066e78c13b3a0f6c9bfefd043fb21220d466876))
* **post:** update gem release tutorial for release-please v4 and trusted publishing ([#601](https://github.com/andrewmcodes/andrewm.codes/issues/601)) ([bbcd078](https://github.com/andrewmcodes/andrewm.codes/commit/bbcd07835ed70721bd968229ca0d1a15298973be))
## [9.1.0](https://github.com/andrewmcodes/andrewm.codes/compare/v9.0.0...v9.1.0) β 2026-09-12
### Features
* always lead selected writing with the latest post ([#597](https://github.com/andrewmcodes/andrewm.codes/issues/597)) ([38d2a44](https://github.com/andrewmcodes/andrewm.codes/commit/38d2a443a5340895c7179932d5f42751cb97c707))
* improve llm content discovery ([#589](https://github.com/andrewmcodes/andrewm.codes/issues/589)) ([8cfb728](https://github.com/andrewmcodes/andrewm.codes/commit/8cfb72825bc31a8d891c45865c07a8e57fd047d0))
### Performance
* optimize page asset delivery ([#594](https://github.com/andrewmcodes/andrewm.codes/issues/594)) ([f785c6a](https://github.com/andrewmcodes/andrewm.codes/commit/f785c6a0c9dcfa598ed55b524ae074852d28f094))
## [9.0.0](https://github.com/andrewmcodes/andrewm.codes/compare/v8.1.1...v9.0.0) β 2026-09-12
### Features
* **design:** act on the critique β Plex Sans, fewer rules, real state cues ([51456a5](https://github.com/andrewmcodes/andrewm.codes/commit/51456a5640519a23225c622b9c2c1744baa7d04f))
* **design:** replace the visual world with slate, ruby, and a named type scale ([1157bea](https://github.com/andrewmcodes/andrewm.codes/commit/1157bea02768c696247e3a51ade6cc40f255b191))
* **home:** add a derived evidence ledger to the hero ([d049d0b](https://github.com/andrewmcodes/andrewm.codes/commit/d049d0b4c22d8ffc98cdf29ff8a8316e5c68bcf9))
* **layout:** give every section a label rail so the desktop reads as composed ([4599b9c](https://github.com/andrewmcodes/andrewm.codes/commit/4599b9c2f4b81224f9ec04448919c1ef9d50f0d9))
* **post:** give the reading page a margin and finally build its contents rail ([75cf33f](https://github.com/andrewmcodes/andrewm.codes/commit/75cf33ffc3d93194e1066173be6327932a02b070))
* **site:** source-list shell, imagekit hosting, seo hardening, and a theme cross-fade ([3ca2ace](https://github.com/andrewmcodes/andrewm.codes/commit/3ca2ace911632a70e47d08803ea07e55b8837d3b))
* **site:** source-list shell, imagekit hosting, seo hardening, and a theme cross-fade ([cbd7b7c](https://github.com/andrewmcodes/andrewm.codes/commit/cbd7b7cebc49d57cc255ee359c75effc2bf3f07d))
* **styles:** add the radix slate ramp ([d81e286](https://github.com/andrewmcodes/andrewm.codes/commit/d81e2869cb0a573ff070c442f09329f3303d42c4))
* **webmentions:** redesign the post response section ([f6cd0f8](https://github.com/andrewmcodes/andrewm.codes/commit/f6cd0f8395738a35cf5343b825e37b5c844c6de4))
### Bug Fixes
* address design review and stabilize archive filters ([9bcec5a](https://github.com/andrewmcodes/andrewm.codes/commit/9bcec5a0062d6f9cf777034ed858237d2959cd14))
* address review regressions in shell lifecycle ([e62c0f5](https://github.com/andrewmcodes/andrewm.codes/commit/e62c0f5279b3806a4d25ddf0f2de3fe323144436))
* align component RBS declarations ([a2db634](https://github.com/andrewmcodes/andrewm.codes/commit/a2db634987d5204ce676c86131347869e4f6506c))
* **chrome:** guarantee focus, and clear the defects the chrome was carrying ([57f75f9](https://github.com/andrewmcodes/andrewm.codes/commit/57f75f92ab050bdda3020994aef72e2b644126ae))
* **cmdk:** restore focus to the trigger when the palette is opened with the shortcut ([8471e00](https://github.com/andrewmcodes/andrewm.codes/commit/8471e00b1576f85f618e93610928adfe3c733045))
* **design:** apply the finish review's material fixes ([45d0048](https://github.com/andrewmcodes/andrewm.codes/commit/45d0048a144885a770ce1798ab9a374055c7db12))
* **design:** clear the defects the first render surfaced ([de8a9d2](https://github.com/andrewmcodes/andrewm.codes/commit/de8a9d20bffae9ad33247da1bf4b39c2edf05114))
* **design:** close the finish review's last items and four latent defects ([eaac5f4](https://github.com/andrewmcodes/andrewm.codes/commit/eaac5f4914b608d62db9af6f9dbb85c718b22819))
* **design:** close the verdict's three regressions and three partials ([2974b4a](https://github.com/andrewmcodes/andrewm.codes/commit/2974b4aec4836194bee3a124f9c13ca2fe7e75cc))
* **design:** unify the last radii, delete a colour trap, and order the post head ([ed29415](https://github.com/andrewmcodes/andrewm.codes/commit/ed29415b1feb450128780baf95fcc39544d7a6d4))
* install falcon for local development ([#564](https://github.com/andrewmcodes/andrewm.codes/issues/564)) ([06d7e7d](https://github.com/andrewmcodes/andrewm.codes/commit/06d7e7d79c2c46eee9fecec77581d7b2364d95f0))
* refresh favicon assets to match ruby palette ([1072337](https://github.com/andrewmcodes/andrewm.codes/commit/1072337b07e90626e5c78b9485ed35eeddd8e5f4))
* remove speaking page section index ([#588](https://github.com/andrewmcodes/andrewm.codes/issues/588)) ([d026cf9](https://github.com/andrewmcodes/andrewm.codes/commit/d026cf937709adf51d10e03355fcc69c5657c740))
* **seo:** repair links and align sitemap metadata ([f245fae](https://github.com/andrewmcodes/andrewm.codes/commit/f245fae7a552dd26f6e06fb181c97a9ef99f3f42))
* **styles:** bridge mint-6 into the tailwind theme ([df2783c](https://github.com/andrewmcodes/andrewm.codes/commit/df2783c07acff4037df30aa2430ec1209c833bf5))
* **types:** satisfy tsc in copy_code and reading_progress ([03d3c9a](https://github.com/andrewmcodes/andrewm.codes/commit/03d3c9a4c3af747d7946d6bcbb89f017ed4ed99b))
### Performance
* **ci:** parallelize Lighthouse and cut runs per URL ([#562](https://github.com/andrewmcodes/andrewm.codes/issues/562)) ([459e8b0](https://github.com/andrewmcodes/andrewm.codes/commit/459e8b0553dd459166adc3fdac99621d84715049))
### Refactors
* **components:** collapse seven row and card components into two primitives ([68ac658](https://github.com/andrewmcodes/andrewm.codes/commit/68ac65812cc95ad43fcd3b0f67b4eaf9805c509b))
* **components:** drop Image's unreachable variants ([eb52a45](https://github.com/andrewmcodes/andrewm.codes/commit/eb52a45c8c176c918fed0aec01f157814c1333fc))
* replace email links with bluesky contact links ([#565](https://github.com/andrewmcodes/andrewm.codes/issues/565)) ([4a7d78e](https://github.com/andrewmcodes/andrewm.codes/commit/4a7d78e5cbfe7fe421bb35be8da624c522238d57))
* **styles:** prune the dead amber steps and the last hand-built label ([6c7119b](https://github.com/andrewmcodes/andrewm.codes/commit/6c7119b780a8bbef9050be2a1f0911b27afcf889))
### Documentation
* **design:** capture product context and design system ([8d6f0d8](https://github.com/andrewmcodes/andrewm.codes/commit/8d6f0d8129061b0ad153c5f52bdc84f36ed909b1))
* **design:** refresh the design system and document slate ([c4b6a20](https://github.com/andrewmcodes/andrewm.codes/commit/c4b6a2046d5ff6674a4f18a36e2fd87242828db0))
* **product:** record the standing design preference and the layout primitive rule ([5341c03](https://github.com/andrewmcodes/andrewm.codes/commit/5341c03f07b164c932d34f6aca1586318ef776ed))
### Chores
* release 9.0.0 ([db19704](https://github.com/andrewmcodes/andrewm.codes/commit/db19704b1488a6636a2bc277c68781e62d6552cc))
## [8.1.1](https://github.com/andrewmcodes/andrewm.codes/compare/v8.1.0...v8.1.1) β 2026-07-10
### Bug Fixes
* changelog and featured projects ([f6a2b11](https://github.com/andrewmcodes/andrewm.codes/commit/f6a2b115f750732cfd7ef6e40c8b12ad53a3202b))
* changelog rendering, button sizing, favicon, featured work ([#549](https://github.com/andrewmcodes/andrewm.codes/issues/549)) ([e51510e](https://github.com/andrewmcodes/andrewm.codes/commit/e51510e7c9fb0dad7582c165bda2fe6c63912e7d))
### Documentation
* add ai skills ([#548](https://github.com/andrewmcodes/andrewm.codes/issues/548)) ([128ebc4](https://github.com/andrewmcodes/andrewm.codes/commit/128ebc4342d865e120ec67501144a303ad26179f))
## [8.1.0](https://github.com/andrewmcodes/andrewm.codes/compare/v8.0.0...v8.1.0) β 2026-07-10
### Features
* **home:** homepage positioning β hero, CTAs, featured work, now ([#546](https://github.com/andrewmcodes/andrewm.codes/issues/546)) ([7e5501c](https://github.com/andrewmcodes/andrewm.codes/commit/7e5501c1dd42058ea3b3745c3e27fcf2d329fc4a)), closes [#520](https://github.com/andrewmcodes/andrewm.codes/issues/520)
### Bug Fixes
* **deps:** skip sharp native build to unblock pnpm install ([e5ae6db](https://github.com/andrewmcodes/andrewm.codes/commit/e5ae6db3b9863c66506c8902b60edd457be21208))
## v8.0.0 / 2026-05-20
Complete rewrite for Bridgetown 2.2+. ERB, ViewComponent, Tailwind v4, Radix UI Colors, Cloudflare Pages.
## [3.4.0](https://github.com/andrewmcodes/andrewm-codes-website/compare/v3.3.0...v3.4.0) β 2022-05-26
### Features
- add Minimalist Habit Tracker Template for Obsidian post ([#186](https://github.com/andrewmcodes/andrewm-codes-website/issues/186)) ([0b64c1a](https://github.com/andrewmcodes/andrewm-codes-website/commit/0b64c1ae6b74d9eda922f41f6aa64a11daf61a17))
## [3.3.0](https://github.com/andrewmcodes/andrewm-codes-website/compare/v3.2.0...v3.3.0) β 2022-05-05
### Features
- add twitter-avatar ([#182](https://github.com/andrewmcodes/andrewm-codes-website/issues/182)) ([3ff3ad8](https://github.com/andrewmcodes/andrewm-codes-website/commit/3ff3ad883b2aaf2bcd73880ef49cf5a961679c54))
## [3.2.0](https://github.com/andrewmcodes/andrewm-codes-website/compare/v3.1.0...v3.2.0) β 2022-03-30
### Features
- add "Create Repository from Current Directory with the GitHub CLI" ([#146](https://github.com/andrewmcodes/andrewm-codes-website/issues/146)) ([334be17](https://github.com/andrewmcodes/andrewm-codes-website/commit/334be1745a8bc54c22afb813f1f4d42190fa2dd8))
- add "How to Deploy Your Bridgetown Site to Github Pages" ([#141](https://github.com/andrewmcodes/andrewm-codes-website/issues/141)) ([5fe6fbb](https://github.com/andrewmcodes/andrewm-codes-website/commit/5fe6fbbd08e230f1ea795ab1fee15b3df2da579c))
- Upgrade Bridgetown to v1.0 ([#139](https://github.com/andrewmcodes/andrewm-codes-website/issues/139)) ([edadced](https://github.com/andrewmcodes/andrewm-codes-website/commit/edadcedfc5b48f3d06db997ce06b7db01d7d7314))
## [3.1.0](https://github.com/andrewmcodes/andrewm-codes-website/compare/v3.0.0...v3.1.0) β 2022-02-27
### Features
- add snippets ([#105](https://github.com/andrewmcodes/andrewm-codes-website/issues/105)) ([a945faa](https://github.com/andrewmcodes/andrewm-codes-website/commit/a945faa52766d25944b5d8b79f2672f9af8196a6))
- add snippets for installing homebrew on a M1 and Intel Mac ([#122](https://github.com/andrewmcodes/andrewm-codes-website/issues/122)) ([b8c3e6d](https://github.com/andrewmcodes/andrewm-codes-website/commit/b8c3e6d6e1d29e19f83fd124eaf27b661a2c6ff2))
- publish Stop Hoarding Notes ([#110](https://github.com/andrewmcodes/andrewm-codes-website/issues/110)) ([bac6f5c](https://github.com/andrewmcodes/andrewm-codes-website/commit/bac6f5ccd4da65096a548766071f0bc5ff9d46df))
- **snippets:** add `Enable Repeating Keys in VS Code on macOS` ([#128](https://github.com/andrewmcodes/andrewm-codes-website/issues/128)) ([4c70833](https://github.com/andrewmcodes/andrewm-codes-website/commit/4c708330f8ab887ec08a8240ca007349ddff53a7))
### Bug Fixes
- use Tailwind CLI to trigger esbuild for JIT mode ([f0cb8f8](https://github.com/andrewmcodes/andrewm-codes-website/commit/f0cb8f864fd30f4bcffcbf55ad160ff6c99516b3))
## [3.0.0](https://github.com/andrewmcodes/andrewm-codes-website/compare/v2.2.0...v3.0.0) β 2022-02-02
### β BREAKING CHANGES
- switch from snowpack to esbuild (#85)
### Features
- add 'Getting Started with Obsidian' ([#79](https://github.com/andrewmcodes/andrewm-codes-website/issues/79)) ([dc1586e](https://github.com/andrewmcodes/andrewm-codes-website/commit/dc1586e49e88bb9f42e8b289f6f350a2643eb43c))
- add new podcast episode with drew ([49dc52d](https://github.com/andrewmcodes/andrewm-codes-website/commit/49dc52d93739604953ddaaaac097b558bd58de83))
- add turbo ([#86](https://github.com/andrewmcodes/andrewm-codes-website/issues/86)) ([c7ee00f](https://github.com/andrewmcodes/andrewm-codes-website/commit/c7ee00f26be5a7ccc7011cee1aab12c0e60e1a6b))
- change font to improve loading ([#87](https://github.com/andrewmcodes/andrewm-codes-website/issues/87)) ([0d206f1](https://github.com/andrewmcodes/andrewm-codes-website/commit/0d206f1270e1d1e779c8bafb51dab3ec023a8d66))
- switch from snowpack to esbuild ([#85](https://github.com/andrewmcodes/andrewm-codes-website/issues/85)) ([b27dbb2](https://github.com/andrewmcodes/andrewm-codes-website/commit/b27dbb25f7f38f8ae39532402122fd40745b3dbb))
- upgrade bridgetown to 1.0.0.beta1 ([#83](https://github.com/andrewmcodes/andrewm-codes-website/issues/83)) ([5a7ba19](https://github.com/andrewmcodes/andrewm-codes-website/commit/5a7ba19ba70390c9c3464fe4b6e1d9066d217520))
### Bug Fixes
- get Tailwind's JIT working ([453509d](https://github.com/andrewmcodes/andrewm-codes-website/commit/453509d621d4252cbdc1553192c7aff16a681850))
- move watcher into deploy condition ([d07f452](https://github.com/andrewmcodes/andrewm-codes-website/commit/d07f4527ec7da4799d7eff743d72f2586fcc5ad1))
- update button styles to not have invisible text ([#74](https://github.com/andrewmcodes/andrewm-codes-website/issues/74)) ([1ee6c7c](https://github.com/andrewmcodes/andrewm-codes-website/commit/1ee6c7c838ab080e42b51421e9c991f66258705f))
- use bridgetown-feed plugin ([323924c](https://github.com/andrewmcodes/andrewm-codes-website/commit/323924ce3653045ef7791d672af0305eae02ecba)), closes [#88](https://github.com/andrewmcodes/andrewm-codes-website/issues/88)
### Performance Improvements
- update cache headers ([#78](https://github.com/andrewmcodes/andrewm-codes-website/issues/78)) ([d507cf3](https://github.com/andrewmcodes/andrewm-codes-website/commit/d507cf30b08214c31263ad934098d7547ed15a75))
## [2.2.0](https://github.com/andrewmcodes/andrewm-codes-website/compare/v2.1.0...v2.2.0) β 2022-01-10
### Features
- Add rubber duck show ([#70](https://github.com/andrewmcodes/andrewm-codes-website/issues/70)) ([daf102a](https://github.com/andrewmcodes/andrewm-codes-website/commit/daf102a6642381b63bedb86f3448026b5ca640f2))
## [2.1.0](https://github.com/andrewmcodes/andrewm-codes-website/compare/v2.0.1...v2.1.0) β 2022-01-10
### Features
- add correct headers ([#68](https://github.com/andrewmcodes/andrewm-codes-website/issues/68)) ([05cec55](https://github.com/andrewmcodes/andrewm-codes-website/commit/05cec55f3dc2db65c99632042af169ac870bfcd9))
## [2.0.1](https://github.com/andrewmcodes/andrewm-codes-website/compare/v2.0.0...v2.0.1) β 2022-01-09
### Bug Fixes
- use bridgetown url in production ([#65](https://github.com/andrewmcodes/andrewm-codes-website/issues/65)) ([a3d2c5a](https://github.com/andrewmcodes/andrewm-codes-website/commit/a3d2c5afa941889e682aa07b7533d200616afd29)), closes [#64](https://github.com/andrewmcodes/andrewm-codes-website/issues/64)
## [2.0.0](https://github.com/andrewmcodes/andrewm-codes-website/compare/v1.0.0...v2.0.0) β 2022-01-09
### β BREAKING CHANGES
- merge in v2.0.0 aka Canary (#55)
### Features
- merge in v2.0.0 aka Canary ([#55](https://github.com/andrewmcodes/andrewm-codes-website/issues/55)) ([e4906b2](https://github.com/andrewmcodes/andrewm-codes-website/commit/e4906b2b381fdfcdcf166430c4e90499b65cf022))
## 1.0.0 (2022-01-09)
### Features
- add aperture and new dependabot config ([c47533d](https://github.com/andrewmcodes/andrewm-codes-website/commit/c47533d4191f95d8333ab80c22a055b064eed7a6))
- add asdf article ([ee631ad](https://github.com/andrewmcodes/andrewm-codes-website/commit/ee631ade19d57b5916030e83f42fe9194f050e9d))
- add indielogin links ([2c8c181](https://github.com/andrewmcodes/andrewm-codes-website/commit/2c8c181cd8f74868940573722a6dea14a8b5664b))
- add json feed ([bc56b61](https://github.com/andrewmcodes/andrewm-codes-website/commit/bc56b61b5a1b6427f6489cc151aaae0e86ad7beb))
- add plausible ([#1](https://github.com/andrewmcodes/andrewm-codes-website/issues/1)) ([d7d022a](https://github.com/andrewmcodes/andrewm-codes-website/commit/d7d022a1ce865127dc31cfb32d695b55cc32ae2b))
- add redesigning my website article ([7530442](https://github.com/andrewmcodes/andrewm-codes-website/commit/7530442571f8a71bc07131acb005375613d45911))
- add relme tags ([c3e2370](https://github.com/andrewmcodes/andrewm-codes-website/commit/c3e23704335069fd1073213641e4e82c3f2aaacc))
- add share widget for posts ([#22](https://github.com/andrewmcodes/andrewm-codes-website/issues/22)) ([4aa3551](https://github.com/andrewmcodes/andrewm-codes-website/commit/4aa35511e0d55e0554d27de14db458d096628ca8))
- add site degraded alert due to cloudinary issue ([#33](https://github.com/andrewmcodes/andrewm-codes-website/issues/33)) ([d8f918d](https://github.com/andrewmcodes/andrewm-codes-website/commit/d8f918d718879eb9b76747425c4a8cc5692c4536))
- add webmention integration ([1eacf85](https://github.com/andrewmcodes/andrewm-codes-website/commit/1eacf855c8f95be29fd0e1a59ca2b3777c69d362))
- **add webmention links:** feat(add webmention links): ([328cb26](https://github.com/andrewmcodes/andrewm-codes-website/commit/328cb26072dcae339e90e013f526354091d320c7))
- add webpacker posts ([#9](https://github.com/andrewmcodes/andrewm-codes-website/issues/9)) ([30bd0ba](https://github.com/andrewmcodes/andrewm-codes-website/commit/30bd0bafa77c7fed1c6a75a4e54f47eddd957f06))
- **blog:** add Automating Ruby Gem Releases with GitHub Actions ([55c088f](https://github.com/andrewmcodes/andrewm-codes-website/commit/55c088f66707d6749a6e824beb91c3672104ef1a))
- **links:** add hacker news and keybase links ([4860baf](https://github.com/andrewmcodes/andrewm-codes-website/commit/4860bafb1f1e9ff4866a6f158392caf6cdb58a6a))
- **posts/webpacker-6:** add css-minimizer-webpack-plugin and an image asset guide ([#12](https://github.com/andrewmcodes/andrewm-codes-website/issues/12)) ([c3616c2](https://github.com/andrewmcodes/andrewm-codes-website/commit/c3616c2d84b23425b5afed0d7337746728a98d37))
- **posts/webpacker-6:** update to beta 2 ([#11](https://github.com/andrewmcodes/andrewm-codes-website/issues/11)) ([826635f](https://github.com/andrewmcodes/andrewm-codes-website/commit/826635ff0acf53d9541385fc7c6b1107faee05aa))
- redesign ([#4](https://github.com/andrewmcodes/andrewm-codes-website/issues/4)) ([ec2a853](https://github.com/andrewmcodes/andrewm-codes-website/commit/ec2a85347ff970d2de8847b829fe803e1654d758))
- site overhaul ([#14](https://github.com/andrewmcodes/andrewm-codes-website/issues/14)) ([3927b5c](https://github.com/andrewmcodes/andrewm-codes-website/commit/3927b5cf512df7ed9917d354dc9706d7c1003d7b))
- update webpacker 6 articles ([#10](https://github.com/andrewmcodes/andrewm-codes-website/issues/10)) ([5bd2a4a](https://github.com/andrewmcodes/andrewm-codes-website/commit/5bd2a4a1d3a21ee645d492bd1f9f801ac09c20ae))
- use dev cover image as og image ([#20](https://github.com/andrewmcodes/andrewm-codes-website/issues/20)) ([d3e534a](https://github.com/andrewmcodes/andrewm-codes-website/commit/d3e534ad1e8d5ae97d2a75670e926766b6583a9b))
### Bug Fixes
- add missing twitter social image ([351d3a8](https://github.com/andrewmcodes/andrewm-codes-website/commit/351d3a8890e7aca4011bd9dde1b9bf0b82aebc56))
- bug with scripts ([#27](https://github.com/andrewmcodes/andrewm-codes-website/issues/27)) ([5235b2b](https://github.com/andrewmcodes/andrewm-codes-website/commit/5235b2bd6afe5ddb747aa2a3bd0f5bb15f0626dd))
- cheat cors ([c090c39](https://github.com/andrewmcodes/andrewm-codes-website/commit/c090c391c8e50c9858a80d0b887570c70e89f61f))
- remove prevent default ([8e1abad](https://github.com/andrewmcodes/andrewm-codes-website/commit/8e1abad0923a0a2411f55392c3b7a288aaa51c0a))
- use correct domain for webmention ([f6a61b2](https://github.com/andrewmcodes/andrewm-codes-website/commit/f6a61b2c74961b1d842bb1bead23cef7c166ecb0))
### Performance Improvements
- **fonts:** addressive massive perf drain from misconfigured fonts ([#32](https://github.com/andrewmcodes/andrewm-codes-website/issues/32)) ([64f74a5](https://github.com/andrewmcodes/andrewm-codes-website/commit/64f74a52d2955f4220696603599ad494c8acc783))
---
# Projects
> Every public repository Andrew Mason maintains β Ruby gems, Bridgetown plugins, and developer tooling β ordered by most recent activity.
- [doorkeeper-cimd](https://github.com/andrewmcodes/doorkeeper-cimd): Client ID Metadata Document (CIMD) support for Doorkeeper. (Ruby, 0 stars, 0 forks)
- [awesome-stars](https://github.com/andrewmcodes/awesome-stars): An Awesome List of my Awesome Stars (20 stars, 2 forks)
- [andrewm.codes](https://github.com/andrewmcodes/andrewm.codes): My personal website built on Bridgetown (Ruby, 14 stars, 3 forks)
- [obsidian-objects](https://github.com/andrewmcodes/obsidian-objects): Schema-driven, object-based note-taking for Obsidian using native Markdown, Properties, and Bases. (TypeScript, 6 stars, 1 fork)
- [shortcut-source-saver](https://github.com/andrewmcodes/shortcut-source-saver): Back up your Apple Shortcuts into a git repository with generated docs and browsable web reviews (Go, 0 stars, 0 forks)
- [dotfiles](https://github.com/andrewmcodes/dotfiles): My personal dot and settings files (Shell, 3 stars, 0 forks)
- [zdotdir](https://github.com/andrewmcodes/zdotdir): Personal ZSH Configuration (Shell, 3 stars, 0 forks)
- [digital-brain-registry-search](https://github.com/andrewmcodes/digital-brain-registry-search): Raycast extension to search package registries (npm, RubyGems, GitHub, Homebrew, VS Code, Obsidian) and generate Obsidian software-note frontmatter for the digital-brain vault. (TypeScript, 0 stars, 0 forks)
- [js-configs](https://github.com/andrewmcodes/js-configs): Shareable JavaScript and Node.js tooling configuration packages (Prettier, commitlint) β a pnpm + Changesets monorepo. (JavaScript, 0 stars, 0 forks)
- [dotfiles-nvim](https://github.com/andrewmcodes/dotfiles-nvim): Personal lazy.nvim config for a Rails/Stimulus/React/ERB + AI workflow. Includes Keycombat β a browser game to learn the keymaps. (Lua, 0 stars, 0 forks)
- [overhrd](https://github.com/andrewmcodes/overhrd): A Procfile process manager for herdr users β a herdr-backed reimplementation of Overmind (Go, 0 stars, 0 forks)
- [obsidian-import](https://github.com/andrewmcodes/obsidian-import): Import structured metadata from canonical sources into an Obsidian vault as plain Markdown notes β Ruby gem with a Charm TUI and scriptable CLI. (Ruby, 0 stars, 0 forks)
- [haml-lint-action](https://github.com/andrewmcodes/haml-lint-action): A GitHub Action that lints your HAML code with HAML Lint! (Ruby, 4 stars, 1 fork)
- [forem_lite](https://github.com/andrewmcodes/forem_lite): A simple tool to help you get articles from Forem/Dev.to (Ruby, 11 stars, 0 forks)
- [bundler-leak-action](https://github.com/andrewmcodes/bundler-leak-action): bundler-leak GitHub action (Dockerfile, 0 stars, 0 forks)
- [bundler-audit-action](https://github.com/andrewmcodes/bundler-audit-action): Bundler Audit Action (Dockerfile, 17 stars, 17 forks)
- [actions](https://github.com/andrewmcodes/actions): Shared GitHub Actions (Shell, 2 stars, 0 forks)
- [vscode-ruby-class-name](https://github.com/andrewmcodes/vscode-ruby-class-name) (TypeScript, 1 star, 0 forks)
- [consolidated_screening_list](https://github.com/andrewmcodes/consolidated_screening_list): Ruby client for interfacing with the Consolidated Screening List (CSL) (Ruby, 0 stars, 0 forks)
- [release-please-demo](https://github.com/andrewmcodes/release-please-demo) (Ruby, 2 stars, 0 forks)
- [pruner](https://github.com/andrewmcodes/pruner): CLI tool to delete Git branches (Ruby, 20 stars, 0 forks)
- [turbo_debug](https://github.com/andrewmcodes/turbo_debug) (Ruby, 2 stars, 0 forks)
- [redux-on-rails](https://github.com/andrewmcodes/redux-on-rails): I DIDNT WANT THIS BUT YOU MADE ME DO IT (Ruby, 3 stars, 0 forks)
- [rails-extension-power-pack](https://github.com/andrewmcodes/rails-extension-power-pack): An extension pack of my favorite VS Code extensions for Ruby on Rails development. (12 stars, 0 forks)
- [andrewmcodes_gem](https://github.com/andrewmcodes/andrewmcodes_gem): My gem card (Ruby, 5 stars, 0 forks)
- [warp-radix](https://github.com/andrewmcodes/warp-radix): Warp theme based on the Radix color system (2 stars, 0 forks)
- [warp-one-dark-darker](https://github.com/andrewmcodes/warp-one-dark-darker): Warp theme based on One Dark Darker (31 stars, 2 forks)
- [obsidian-beginner-vault-template](https://github.com/andrewmcodes/obsidian-beginner-vault-template): A minimal template for your first Obsidian vault aimed at setting good defaults. (89 stars, 12 forks)
- [standardrb-action](https://github.com/andrewmcodes/standardrb-action): StandardRB Action: A GitHub Action to run StandardRB against your code! (Ruby, 32 stars, 22 forks)
- [dishwasher](https://github.com/andrewmcodes/dishwasher): A CLI tool to help you easily delete forked repositories. (Ruby, 10 stars, 0 forks)
- [huebrew](https://github.com/andrewmcodes/huebrew) (Ruby, 0 stars, 0 forks)
- [vercel_ruby](https://github.com/andrewmcodes/vercel_ruby): WIP Vercel Serverless Functions written in Ruby (HTML, 2 stars, 5 forks)
- [alfred-themes](https://github.com/andrewmcodes/alfred-themes): My personal Alfred 5 themes (5 stars, 0 forks)
- [stimulus_reflex_table_filter](https://github.com/andrewmcodes/stimulus_reflex_table_filter): Demo app showing how you can filter a table in a Ruby on Rails app with StimulusReflex (Ruby, 17 stars, 2 forks)
- [shotgun](https://github.com/andrewmcodes/shotgun): Ready to go Rails App with TailwindCSS, ViewComponent, Devise, and more! (Ruby, 25 stars, 2 forks)
- [.github](https://github.com/andrewmcodes/.github): Default community health files for @andrewmcodes (4 stars, 0 forks)
- [project-kit](https://github.com/andrewmcodes/project-kit): Generic project scaffolding platform built on declarative manifests, capability overlays, upstream ecosystem generators (bundle gem, rails new), and code patchers. v1 ships ruby-gem, rails-app, ruby-script, and ts-cli. (Ruby, 0 stars, 0 forks)
- [lazygit-learn](https://github.com/andrewmcodes/lazygit-learn): Learn lazygit keyboard shortcuts β Duolingo-style (JavaScript, 0 stars, 0 forks)
- [andrewmcodes](https://github.com/andrewmcodes/andrewmcodes): andrewmcodes public readme (7 stars, 0 forks)
- [claude-plugins](https://github.com/andrewmcodes/claude-plugins): Personal marketplace for Claude Code plugins (0 stars, 0 forks)
- [awesome-bridgetown](https://github.com/andrewmcodes/awesome-bridgetown): A curated list of awesome things related to Bridgetown (17 stars, 4 forks)
- [bridgetown-esbuild-minifySyntax](https://github.com/andrewmcodes/bridgetown-esbuild-minifySyntax): Benchmarking usage of minifySyntax with esbuild and Bridgetown (JavaScript, 1 star, 0 forks)
- [rails-ci](https://github.com/andrewmcodes/rails-ci) (Ruby, 0 stars, 0 forks)
- [bridgetown-torchlight-demo](https://github.com/andrewmcodes/bridgetown-torchlight-demo): A demo Bridgetown website that has Torchlight syntax highlighting configured! (JavaScript, 3 stars, 0 forks)
- [remote-ruby-vault](https://github.com/andrewmcodes/remote-ruby-vault): Obsidian vault of Remote Ruby episodes for personal research (JavaScript, 1 star, 0 forks)
- [bridgetown-gh-pages-demo](https://github.com/andrewmcodes/bridgetown-gh-pages-demo): Demo of the new gh-pages configuration in Bridgetown v1.0 (JavaScript, 1 star, 0 forks)
- [advent_of_code](https://github.com/andrewmcodes/advent_of_code): Solutions to Advent of Code (Ruby, 2 stars, 0 forks)
- [vscode-tailwindcss-extension-pack](https://github.com/andrewmcodes/vscode-tailwindcss-extension-pack): An extension pack for VSCode of extensions for developing with Tailwind CSS. (1 star, 0 forks)
---
# Speaking
> Talks I've given, podcasts I host, shows I've been on, and CFPs I've submitted. Want me on yours? Get in touch.
## Podcasts
- [Remote Ruby](https://remoteruby.com): Three Rubyists having conversations and interviewing others about Ruby and web development. I host alongside Chris Oliver, Jason Charnes, and David Hill. (co-host, current)
- [Ruby for All](https://rubyforall.com): A podcast for people newer to Ruby. Conversations with juniors, mentors, and the community building each other up. I co-hosted with Julie J. (co-host, 2022β2024)
- The Ruby Blend: A spontaneous chat about Ruby, Rails, and whatever else was on our minds that week. I co-hosted with Nate Hopkins and Ron Cooke. (co-host, 2022β2023)
- [Ruby Rogues](https://devchat.tv/podcasts/ruby-rogues/): A panel discussion about Ruby, programming, and software development. I was a panelist alongside Charles Max Wood and a rotating cast. (panelist, 2018β2022)
## Talks
- [We Are the Ruby Community](https://youtu.be/ZBrBMC_sH2A): Many developers feel like observers of the Ruby community rather than participants in it. This talk reframes what community actually means, breaks down the invisible barriers that make people feel like they don't belong, and shows practical ways anyone can participate right away. The goal is to show that the Ruby community isn't something you join, it's something you help create. (Blastoff Rails, 2026-06-12)
- [Learn Enough Bridgetown to be Dangerous](https://www.bridgetownconf.rocks/#talk-bridgetown-dangerous): A virtual talk about getting started with Bridgetown, the Ruby static site generator I've built every version of this site on. (BridgetownConf, 2022-09-15)
- [Start Your Ruby Podcast Today! No Experience Required!](https://www.youtube.com/watch?v=Y6QVKq3iM5s): My first conference talk, given with my Remote Ruby co-hosts Jason Charnes and Chris Oliver. A practical guide to starting a podcast as an engineer. (RailsConf, 2022-05-19)
## CFPs
- [We Are the Ruby Community](https://andrewm.codes/cfps/we-are-the-ruby-community.md): My accepted keynote proposal for Blastoff Rails 2026 on why everyone already belongs in the Ruby community.
- [Dare To Give Your Junior Developers Permission To Fail](https://andrewm.codes/cfps/dare-to-give-your-junior-developers-permission-to-fail.md): My rejected CFP for RailsConf 2020 about how to be a better mentor to junior developers.
- [Perfectionism: The Death of Progress](https://andrewm.codes/cfps/perfectionism-the-death-of-progress.md): My rejected CFP for RailsConf 2019 on how to combat perfectionism as a developer.
## Appearances
- [The Halloween Party - Dev Horror Stories!](https://www.youtube.com/watch?v=sFxZlkkihFI): Spine-tingling stories of production gremlins, db goblins, code ghosts and much more. (Rubber Duck Dev Show, 2022-10-26)
- [Special Guests: Andrew Mason and Collin Jilbert! ](https://www.youtube.com/watch?v=9HVEuIlf5RA): Collin Jilbert and I join the Rubber Duck Dev duo to talk (Rubber Duck Dev Show, 2022-07-20)
- [Andrew Mason: I Expected College To Be Basically Boot Camp](https://justtheusefulbits.com/jtub/andrew-mason-i-expected-college-to-be-basically-boot-camp/): Noah Gibbs and I talk about the prison and court systems, why FTP is a terrible protocol, reading code, ADHD and a lot more. (Computer Science: Just the Useful Bits, 2022-06-21)
- [Start Your Ruby Podcast Today!](https://www.youtube.com/watch?v=Y6QVKq3iM5s): Talk I gave with Jason Charnes and Chris Oliver going through the lessons and advice we had for starting and maintaining a successful podcast. (RailsConf 2022, 2022-05-19)
- [Podcast Panel from Railsconf 2022](https://remoteruby.com/182): Railsconf is back in person! We sat down live to record with Jemma Issroff, Brittany Martin, Robby Russel, Andrew Culver, Nicholas Schwaderer, and Colleen Schnettler to discuss everything Ruby on Rails. (RailsConf 2022, 2022-05-17)
- [Episode 1 - Andrew Mason](https://www.buzzsprout.com/1927628/9957746-episode-1-andrew-mason): I join Drew Bragg for the first episode of his new podcast to discuss ADHD, workflows, Bridgetown, and more. (Code and the Coding Coders who Code it, 2022-02-01)
- [Special Guest: Andrew Mason!](https://www.youtube.com/watch?v=8SEcHvOXCwo): A chill convo about the Remote Ruby podcast, Ruby Radar and other Ruby goodness! (Rubber Duck Dev Show, 2022-01-05)
- [Podcast Panel from Railsconf 2021](https://remoteruby.com/156): We are LIVE from Denver at RubyConf 2021 with Jemma Issroff, Jason Charnes, Emily Giurleo, Nick Schwaderer, and Jason Swett (RailsConf 2021, 2021-10-24)
- [Rails 7 with DHH](https://www.youtube.com/watch?v=6xKvqYGKI9Q): DHH joins us to talk about Rails 7, the future of Rails, and more. (Remote Ruby, 2021-10-17)
- [ViewComponents in Action with Andrew Mason](https://www.therubyonrailspodcast.com/320): I join Brittany Martin for a conversation primarily focused on how I was implementing ViewComponent at CodeFund. (The Ruby on Rails Podcast, 2020-05-27)
- [Joined by Andrew Mason](https://share.transistor.fm/s/19f4de7e): I share my path to programming through graphic design, as well as how I joined Ruby Rogues, became involved with open source, became invested in the Ruby community, and more. (Remote Ruby, 2019-09-20)
- [Two Docker Noobs Talk About Docker](https://www.codewithjason.com/podcast/9478304-011-two-docker-noobs-talk-about-docker-with-andrew-mason/): A long, rambling, undisciplined discussion of a number of things including Jason Swett's and I's respective experiences with Docker. (The Code with Jason Podcast, 2019-08-21)
- [MRS 074: Andrew Mason](https://podcasts.apple.com/us/podcast/episode-74-mrs-074-andrew-mason/id1237404328?i=1000428406734): I am introduced as the newest panelist for Ruby Rogues and share my background and how I got into computer programming. (My Ruby Story, 2019-01-24)
- [Testing Q&A with Listener Andrew Mason](https://www.codewithjason.com/ruby-testing-podcast/): As a listener and junior Rails developer, I come on the show to ask some questions about Rails testing. (Ruby Testing Podcast, 2018-10-18)
---
# Uses
> The tools I rely on day to day. Updated whenever I switch something out for something better.
## Workstation
The desk, screens, and input devices I use every day.
- [**Apple MacBook Pro 14-inch (M5)**](https://www.apple.com/macbook-pro/). Mac since college; no plans to change.
- [**Ultimate Hacking Keyboard v2**](https://ultimatehackingkeyboard.com/). Split design. Fixed my wrist pain in 2020 and still going.
- [**Logitech MX Vertical**](https://www.logitech.com/en-us/products/mice/mx-vertical-ergonomic-mouse.910-005447.html). Looks weird, feels great.
- [**Samsung 32" Odyssey Neo G8**](https://www.samsung.com/us/monitors/gaming/). Mini-LED, 4K, absurdly fast. My main panel.
- [**LG 27UN880-B UltraFine 27" UHD**](https://www.lg.com/us/monitors). Second screen on the ergo arm.
## Development
Where I write code and run commands.
- [**VS Code**](https://code.visualstudio.com). Daily driver since college. I'll use JetBrains IDEs for larger projects.
- [**Warp**](https://app.warp.dev/referral/2W6LEL). Replaced iTerm. Shareable blocks, command palette, AI suggestions.
## Productivity
Notes, shortcuts, and a little structure for the day.
- [**Raycast**](https://www.raycast.com). Spotlight replacement with custom extensions and workflows. Replaced Alfred.
- [**Obsidian**](https://obsidian.md). The first note app that stuck with my ADHD brain. Daily.
- [**Session**](https://go.setapp.com/invite/3sztpuuq). Pomodoro timer with analytics. Helps me actually sit down and focus.
## Design
For interface work and component design.
- [**Figma**](https://www.figma.com). Full-time at Podia. Component composition is great.
## AI
Coding assistants and tools for reviewing their work.
- [**Claude Code**](https://claude.com/claude-code). Agentic coding in the terminal. Where most of my dev work happens now.
- [**Codex**](https://openai.com/codex/). OpenAI's coding agent. Second opinion alongside Claude Code.
- **Plannotator**. Review and annotate plans and diffs before I let an agent run with them.
- **herdr**. Wrangles my agents and background tasks.
---
# Writing
> Notes on Rails, podcasting, and the long unglamorous middle. Occasionally proofread.
- [Adding Webmentions to a Bridgetown Site](https://andrewm.codes/p/adding-webmentions-to-a-bridgetown-site.md): How I added webmentions to my static Bridgetown site using webmention.io, Bridgy, and a nightly GitHub Actions job, so replies and likes from Bluesky show up right under my posts.
- [Exporting my Pieces code snippets to Obsidian](https://andrewm.codes/p/exporting-my-pieces-code-snippets-to-obsidian.md): After 358 code snippets, I exported everything out of Pieces into plain Markdown I own in Obsidian. Here's the Ruby script, and why owning your data matters.
- [Kill Process Running on a Specific Port](https://andrewm.codes/p/kill-process-on-port.md): I often have to kill processes that weren't stopped correctly on different ports and can never remember the command.
- [Living with ADHD: The Benefits of Openness and Vulnerability](https://andrewm.codes/p/adhd.md): Adventures in adjusting to life with ADHD - Join me on my journey as I talk about working with, and understanding, my ADHD diagnosis.
- [INTP: My Personality Type](https://andrewm.codes/p/personality.md): Notes on my INTP personality type, how I relate to the description, and why it helps explain how I think and work.
- [How to Add a Progress Bar Around Your Twitter Avatar](https://andrewm.codes/p/twitter-avatar.md): Add a personalized progress bar to your Twitter avatar with the help of BlackMagic.so's Profile Progress Bar Tool. Follow me on Twitter to see it in motion!
- [Minimalist Habit Tracking Template for Obsidian](https://andrewm.codes/p/minimalist-habit-tracker-template-for-obsidian.md): A short tutorial on how to build a minimalist habit tracker template for Obsidian using the Dataview plugin.
- [Create Repository from Current Directory with the GitHub CLI](https://andrewm.codes/p/gh-create-repo.md): Use gh to create a repo using your current directory as the source and push to GitHub without having to set your upstream.
- [How to Deploy Your Bridgetown Site to Github Pages](https://andrewm.codes/p/deploy-bridgetown-to-github-pages.md): It has never been easier to deploy your Bridgetown site to GitHub Pages thanks to a new bundled configuration in Bridgetown v1.0
- [Enable Repeating Keys in VS Code on macOS](https://andrewm.codes/p/vscode-enable-repeating-keys-macos.md): If you want to use Vim in VS Code, you have to enable repeating keys, which can be frustrating if you are new to Vim.
- [Install Brew on an Intel Mac](https://andrewm.codes/p/brew-install-intel-mac.md): The one-liner that installs Homebrew on an Intel Mac running macOS Monterey, the PATH export it needs in ~/.zshrc, and how to verify with brew doctor.
- [Install Brew on a M1 Mac](https://andrewm.codes/p/brew-install-m1-mac.md): The one-liner that installs Homebrew on an M1 Mac running macOS Monterey, the /opt/homebrew shellenv line for ~/.zprofile, and how to verify it.
- [Stop Hoarding Notes](https://andrewm.codes/p/stop-hoarding-notes.md): My ideas on why you should become just as comfortable deleting notes as you do code
- [Getting Started with Obsidian](https://andrewm.codes/p/getting-started-with-obsidian.md): A beginners guide to setting up Obsidian, an advanced markdown note taking app, for the first time.
- [Alfred Custom Terminal Snippet](https://andrewm.codes/p/alfred-custom-terminal.md): An AppleScript that wires Alfred's terminal integration to a custom terminal like Warp or Archipelago, plus the Alfred settings that enable it.
- [How to Unhide Desktop Icons on macOS](https://andrewm.codes/p/unhide-macos-desktop-icons.md): If your desktop icons disappear, you may need to toggle the desktop back on via the command line.
- [How to install Ruby on Rails 6.1 with asdf on macOS Big Sur](https://andrewm.codes/p/how-to-install-ruby-on-rails-6-1-with-asdf-on-macos-big-sur.md): Setup Ruby, Node, and PostgreSQL with asdf to quickly get up and running with Rails
- [Automating Ruby Gem Releases with GitHub Actions](https://andrewm.codes/p/automating-ruby-gem-releases-with-github-actions.md): Automate Ruby gem releases with GitHub Actions and Release Please: conventional-commit versioning, changelog generation, and publishing to RubyGems.
- [Redesigning my website](https://andrewm.codes/p/redesigning-my-website.md): Why and how I rebuilt andrewm.codes with Bridgetown to put content first, and the stack behind it: ERB, Tailwind CSS, and Strapi.
- [Webpacker 6: Image Asset Guide](https://andrewm.codes/p/webpacker-6-image-asset-guide.md): How to serve images and SVGs from a Webpacker 6 Rails app using require.context and asset_pack_path. Part of the archived Webpacker 6 guide.
- [Webpacker 6: Troubleshooting Guide](https://andrewm.codes/p/webpacker-6-troubleshooting-guide.md): Tools and techniques for debugging Webpacker 6 and Webpack build errors in Rails. Archived, since Webpacker is no longer maintained.
- [Webpacker 6: SCSS/Sass Loaders](https://andrewm.codes/p/webpacker-6-scss-sass-loaders.md): How to compile SCSS and Sass in a Webpacker 6 Rails app with sass-loader and sass. Part of the archived Webpacker 6 upgrade guide.
- [Webpacker 6: PostCSS Loaders](https://andrewm.codes/p/webpacker-6-postcss-loaders.md): How to process .pcss files in Webpacker 6 with postcss-loader and PostCSS 8. Part of the archived Webpacker 6 upgrade guide.
- [Webpacker 6: CSS Loaders](https://andrewm.codes/p/webpacker-6-css-loaders.md): How to process CSS in Webpacker 6 with css-loader, style-loader, and mini-css-extract-plugin. Part of the archived Webpacker 6 upgrade guide.
- [Webpacker 6: Tailwind CSS 2.0 Integration](https://andrewm.codes/p/webpacker-6-tailwind-css-2-0-integration.md): How to add Tailwind CSS 2.0 to a Rails 6 app running Webpacker 6 with PostCSS. Archived, since Webpacker is no longer maintained.
- [Webpacker 6: Upgrade Guide](https://andrewm.codes/p/webpacker-6-upgrade-guide.md): Upgrade a Rails app from Webpacker 5 to 6: the Gemfile bump, the install task, and the new pack tags. Part of the archived Webpacker 6 guide.
- [Webpacker 6: Tutorial Setup](https://andrewm.codes/p/webpacker-6-tutorial-setup.md): Set up a demo Rails 6.1 app to follow along with the archived Webpacker 6 upgrade series, from new app to root route.
- [Webpacker 6](https://andrewm.codes/p/webpacker-6.md): An archived guide to setting up and upgrading Webpacker 6 in Rails applications, including loaders, assets, and verification steps.
- [gem install mysql2](https://andrewm.codes/p/gem-install-mysql2.md): How to fix the mysql2 gem's 'library not found for -lssl' native extension error on macOS using cmake and OpenSSL build flags.
- [Ruby's Shovel Method: Digging Deeper](https://andrewm.codes/p/ruby-s-shovel-method-digging-deeper.md): A short, fun look at Ruby's shovel operator (<<): what it does on arrays and strings, and why you can chain it even though you probably shouldn't.
- [How I Use VSCode](https://andrewm.codes/p/how-i-use-vscode.md): A snapshot of my Visual Studio Code setup: the settings and extensions I use day to day, shared via How I VSCode.
- [15 Resources I Learned Something From This Weekend](https://andrewm.codes/p/15-resources-i-learned-something-from-this-weekend.md): A weekend roundup of 15 things worth your time: five Ruby on Rails blog posts, five open source projects, and five podcast episodes.
- [8 Tailwind CSS resources to help your next project takeoff](https://andrewm.codes/p/8-tailwind-css-resources-to-help-your-next-project-takeoff.md): Eight Tailwind CSS resources to speed up your next project: component libraries, typography and layout helpers, and developer-experience plugins.
- [How to inline SVG files in your Bridgetown site](https://andrewm.codes/p/how-to-inline-svg-files-in-your-bridgetown-site.md): A short tutorial on how to inline SVG files in your Bridgetown site with bridgetown-svg-inliner.
- [Creating a blog with Bridgetown and Netlify CMS](https://andrewm.codes/p/creating-a-blog-with-bridgetown-and-netlify-cms.md): A step-by-step tutorial on adding Netlify CMS to a Bridgetown site so you can edit and publish content from a Git-backed admin UI.
- [Rails 6 Band-Aid for Webpacker::Manifest::MissingEntryError](https://andrewm.codes/p/rails-6-band-aid-for-webpacker-manifest-missingentryerror.md): A workaround for Webpacker::Manifest::MissingEntryError in the Rails 6 test environment: force Webpacker to compile test packs from your test helper.
- [Build and deploy a static site with Ruby, Bridgetown, TailwindCSS, and Netlify](https://andrewm.codes/p/build-and-deploy-a-static-site-with-ruby-bridgetown-tailwindcss-and-netlify.md): Build a Bridgetown static site with Tailwind CSS and deploy it to Netlify. This older tutorial is archived but still useful.
- [Instantly speed up your Rails application by self-hosting your fonts](https://andrewm.codes/p/instantly-speed-up-your-rails-application-by-self-hosting-your-fonts.md): Improve Rails page speed by self-hosting web fonts instead of relying on third-party font CDNs.
- [Rails Coverage Tools: CodeFactor](https://andrewm.codes/p/rails-coverage-tools-codefactor.md): Add CodeFactor to a Ruby on Rails application for automated code review, repository analysis, and README status badges.
- [A11Y in Rails: Automated Linting with AccessLintπ](https://andrewm.codes/p/a11y-in-rails-automated-linting-with-accesslint.md): Add automated accessibility (a11y) linting to a Ruby on Rails app with AccessLint, which flags issues on every pull request before they ship.
- [Rails Coverage Tools: Coverband](https://andrewm.codes/p/rails-coverage-tools-coverband.md): Add Coverband to a Rails application to measure production code usage and find unused Ruby code, gems, and views.
- [Stopping a runaway Rails server](https://andrewm.codes/p/stopping-a-runaway-rails-server.md): How to stop a runaway Ruby on Rails server that won't quit on ctrl-c, using the shutup gem to kill it with a single command.
- [Hiding Ruby 2.7 Deprecation Warnings in Rails 6](https://andrewm.codes/p/hiding-ruby-2-7-deprecation-warnings-in-rails-6.md): Three ways to silence noisy Ruby 2.7 deprecation warnings in a Rails 6 app using the RUBYOPT environment variable.
- [How to set up Ruby on Rails 6 and TailwindCSS 1.1.4](https://andrewm.codes/p/how-to-set-up-ruby-on-rails-6-and-tailwindcss-1-1-4.md): Build a Rails 6 app with Tailwind CSS 1.1.4, including setup, Webpack, scaffolding, configuration, and styled views.
- [CI for Ruby on Rails: GitHub Actions vs. CircleCI](https://andrewm.codes/p/ci-for-ruby-on-rails-github-actions-vs-circleci.md): The finale of a three-part series on CI for Ruby on Rails, comparing GitHub Actions and CircleCI to help you choose the right setup.
- [CI for Ruby on Rails: CircleCI](https://andrewm.codes/p/ci-for-ruby-on-rails-circleci.md): Part two of a CI for Ruby on Rails series: build a complete CircleCI pipeline with Docker images, caching, database setup, tests, and linters.
- [Use Tailwind CSS 1.1 in your Rails App](https://andrewm.codes/p/use-tailwind-css-1-0-in-your-rails-app.md): A step-by-step walkthrough of adding Tailwind CSS 1.1 to a new Ruby on Rails app with Webpacker, from the npm install through PostCSS configuration.