<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>larsp.de</title><description>Digital garden about web development, Ruby on Rails, telephony, and tools Lars finds useful.</description><link>https://larsp.de/</link><language>en-US</language><atom:link href="https://larsp.de/rss.xml" rel="self" type="application/rss+xml"/><item><title>AI/LLM Resources</title><link>https://larsp.de/posts/ai-llm-resources/</link><guid isPermaLink="true">https://larsp.de/posts/ai-llm-resources/</guid><description>A curated collection of artificial intelligence and large language model resources, including Claude Code tools, plugins, MCP servers, spec-driven workflows and Ruby/Rails AI frameworks.</description><pubDate>Tue, 02 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Collection of Artificial Intelligence / LLM resources I actually use day to day. Biased towards Claude and the Ruby ecosystem, because that&apos;s where I spend most of my time.&lt;/p&gt;
&lt;h2&gt;Claude Code&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.anthropic.com/claude-code&quot;&gt;Claude Code&lt;/a&gt; is Anthropic&apos;s agentic coding CLI. It lives in the terminal, reads and edits files, runs commands, and keeps track of project context across sessions. Paired with &lt;code&gt;CLAUDE.md&lt;/code&gt; files in the repo root, it becomes a pretty capable pair programmer.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/cli-reference&quot;&gt;CLI reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kadekillary.work/blog/#2025-06-16-snorting-the-agi-with-claude-code&quot;&gt;Interesting uses, e.g. let Claude generate a presentation of your codebase with Marp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://benenewton.com/blog/claude-code-roadmap-management&quot;&gt;Roadmap management with Claude Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/clara-claude-code-secretary/&quot;&gt;Building Clara: a Claude Code setup for running a consultancy&lt;/a&gt; — my own workflow: PARA-style markdown repo, custom persona, MCP servers and a few cron jobs.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Subagents &amp;amp; hooks&lt;/h3&gt;
&lt;p&gt;Two building blocks I lean on heavily in &lt;a href=&quot;/posts/clara-claude-code-secretary/&quot;&gt;Clara&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/sub-agents&quot;&gt;Subagents&lt;/a&gt; — specialised sessions, each with its own context window and tool access. Claude hands a side task (research, a broad search) to one and gets back just the summary, so the main conversation stays clean. You can also pin them to a cheaper model like Haiku.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/hooks&quot;&gt;Hooks&lt;/a&gt; — shell commands that fire on lifecycle events (before/after a tool runs, on session start, …). The place to enforce conventions, auto-format, or wire in your own automation without nagging the model to remember.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Plugins &amp;amp; extensions&lt;/h3&gt;
&lt;p&gt;Plugins bundle skills, subagents, hooks and MCP servers into one installable unit. The &lt;a href=&quot;https://code.claude.com/docs/en/discover-plugins&quot;&gt;&lt;code&gt;/plugin&lt;/code&gt; marketplace&lt;/a&gt; is the central place to browse and install them — Anthropic ships a curated official marketplace, and you can add community ones (any GitHub repo or Git URL).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openai/codex-plugin-cc&quot;&gt;Codex plugin for Claude Code&lt;/a&gt; — makes OpenAI&apos;s Codex available from inside Claude Code. Handy for a second opinion (&lt;code&gt;/codex:review&lt;/code&gt;, &lt;code&gt;/codex:adversarial-review&lt;/code&gt;) or for delegating background tasks (&lt;code&gt;/codex:rescue&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/anthropics/claude-for-legal&quot;&gt;Claude for Legal&lt;/a&gt; — Anthropic&apos;s plugin suite for legal workflows (commercial, corporate, employment, privacy, IP, litigation, regulatory, AI governance). 100+ named agents that draft, triage and review documents, with MCP connectors for CourtListener, Westlaw, Ironclad, iManage and others. Every output is framed as a draft for attorney review. A good template for what a domain-specific Claude Code plugin pack can look like.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Spec-driven workflows&lt;/h3&gt;
&lt;p&gt;Both of these treat specifications as the source of truth: you write the spec first, then agents implement against it. Different flavours for different project sizes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/github/spec-kit&quot;&gt;GitHub Spec Kit&lt;/a&gt; — GitHub&apos;s own SDD toolkit and the de-facto standard (90k+ stars). The flow is Spec → Plan → Tasks → Implement via &lt;code&gt;/specify&lt;/code&gt;, &lt;code&gt;/plan&lt;/code&gt; and &lt;code&gt;/tasks&lt;/code&gt; slash commands. Agent-agnostic: it drives Claude Code, Copilot, Gemini and ~30 others, so you can switch the underlying agent without rewriting the workflow.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/automazeio/ccpm&quot;&gt;CCPM – Claude Code Project Management&lt;/a&gt; — uses GitHub Issues and Git worktrees to let up to 12 Claude instances work in parallel. Workflow is Brainstorm → PRD → Epic → Issues → Tracking, all via &lt;code&gt;/pm:*&lt;/code&gt; slash commands.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Priivacy-ai/spec-kitty&quot;&gt;Spec Kitty&lt;/a&gt; — CLI plus live Kanban dashboard, 6-phase lifecycle (spec → plan → tasks → implement → review → merge). Supports 12 agents including Claude Code, Cursor, Windsurf, Gemini and GitHub Copilot.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Agent Skills&lt;/h2&gt;
&lt;p&gt;Skills are self-contained capability packages — a folder of instructions, scripts and resources that Claude can load on demand.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview&quot;&gt;Agent Skills – official docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/anthropics/claude-cookbooks/tree/main/skills&quot;&gt;Claude Cookbooks: Skills&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/anthropics/skills&quot;&gt;Public skills repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Model Context Protocol (MCP)&lt;/h2&gt;
&lt;p&gt;MCP is the open protocol that lets LLMs talk to external tools and data sources in a standardised way. The magic is that any MCP-compatible client (Claude Desktop, Claude Code, Cursor, …) can use any MCP server.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://modelcontextprotocol.io/introduction&quot;&gt;Get started with the Model Context Protocol&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MCP servers I rely on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://linear.app/integrations/claude&quot;&gt;Linear MCP&lt;/a&gt; — read and update Linear issues from within Claude.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/robertn702/mcp-sunsama&quot;&gt;Sunsama MCP&lt;/a&gt; — full CRUD on Sunsama tasks, subtasks and streams. Great for keeping the daily plan in sync without leaving the terminal.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/M-Pineapple/member-berries-apple-mcp&quot;&gt;Member Berries MCP&lt;/a&gt; — built on apple-mcp, adds memory-style tools for calendar, notes and reminders. Turns Claude into &quot;a helpful friend who remembers your day&quot;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Ruby/Rails tools and frameworks&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/crmne/ruby_llm&quot;&gt;RubyLLM&lt;/a&gt; — one clean Ruby API across OpenAI, Anthropic, Gemini, Ollama and friends: chat, vision, embeddings, tool calls and streaming, with a model registry covering hundreds of models. First-class Rails integration (&lt;code&gt;acts_as_chat&lt;/code&gt;, install generators), so wiring an LLM into an app is genuinely a handful of lines. My default starting point these days.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/CultivateLabs/raif&quot;&gt;Raif (Ruby AI Framework)&lt;/a&gt; — a Rails engine that helps you add AI-powered features to your Rails apps.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/obie/claude-on-rails&quot;&gt;Claude on Rails&lt;/a&gt; — Obie Fernandez&apos;s framework that orchestrates specialised agents (Architect, Models, Controllers, Views, Services, Tests, DevOps) on top of &lt;code&gt;claude-swarm&lt;/code&gt;. Rails-aware, test-driven, integrates with the Rails MCP Server for up-to-date documentation.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jekyll/classifier-reborn&quot;&gt;classifier-reborn&lt;/a&gt; — a general classifier module for Bayesian and other types of classification. Pre-LLM, but still useful for lightweight text categorisation without calling an API.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Resources</category><category>AI</category></item><item><title>Building Clara: a Claude Code setup for running a consultancy</title><link>https://larsp.de/posts/clara-claude-code-secretary/</link><guid isPermaLink="true">https://larsp.de/posts/clara-claude-code-secretary/</guid><description>How I use Claude Code, a PARA-style markdown repo and a custom persona to run my consultancy day-to-day.</description><pubDate>Tue, 19 May 2026 08:00:00 GMT</pubDate><content:encoded>&lt;p&gt;For the past year I&apos;ve been building something I now call Clara — a &lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/overview&quot;&gt;Claude Code&lt;/a&gt; setup that handles the administrative side of running a consultancy: client projects, server notes, recurring tasks, follow-ups, daily journaling, and an inbox that talks back.&lt;/p&gt;
&lt;p&gt;This post walks through the moving parts: a markdown-only repository, a persona that has opinions, a few MCP servers, and a handful of cron jobs held together with small glue scripts. No custom application, just conventions and wiring on top of Claude Code.&lt;/p&gt;
&lt;p&gt;If there&apos;s one thing I&apos;d lead with: the persona did more for me than any of the tooling. A character with opinions turns a markdown folder into a collaborator. A neutral assistant just turns it into a slightly chattier filing system. The rest of the post is mostly the wiring underneath that idea.&lt;/p&gt;
&lt;h2&gt;The big picture&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;                      ┌─────────────────┐
                      │   Claude Code   │
                      │     &quot;Clara&quot;     │
                      └────────┬────────┘
                               │
        ┌──────────────────────┼──────────────────────┐
        │                      │                      │
        ▼                      ▼                      ▼
┌──────────────┐       ┌──────────────┐       ┌──────────────┐
│   Markdown   │       │  MCP servers │       │   Cron jobs  │
│     repo     │◀─────▶│   Sunsama    │       │ daily commit │
│   (PARA)     │       │  IMAP, Cal   │       │  /briefing   │
└──────┬───────┘       └──────────────┘       └──────────────┘
       │                                              │
       └──────────  daily auto-commit  ◀──────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three building blocks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;markdown repo&lt;/strong&gt; that holds everything Clara needs to know about projects, clients and infrastructure&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claude Code&lt;/strong&gt; as the agent, configured with a persona and a few skills&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP servers&lt;/strong&gt; for live data (calendar, mail, Sunsama) and &lt;strong&gt;cron jobs&lt;/strong&gt; that wake Clara up on a schedule&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why I built Clara&lt;/h2&gt;
&lt;p&gt;I run a small consultancy with a steady stream of clients: a custom integration here, a hosting migration there, a website project that drags on for months. Add personal projects, server maintenance, follow-ups with the building management, the accountant, the occasional apartment purchase — and the context starts spilling out of any single tool.&lt;/p&gt;
&lt;p&gt;Things-style task apps didn&apos;t help. They knew nothing about the actual work. Note apps were better but couldn&apos;t act on anything. ChatGPT was useful for ad-hoc questions but had no awareness of what was in my files.&lt;/p&gt;
&lt;p&gt;Claude Code changed that. It lives inside a repo, reads and writes markdown, runs shell commands, and connects to external systems through MCP. The natural next step: turn the repo into a workspace that Claude Code can navigate, and give the agent enough character to be useful as more than an autocomplete on top of files.&lt;/p&gt;
&lt;p&gt;Clara talks to me in German; the quotes throughout this post are her originals, with an English rendering underneath.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;„Ich bin keine höflich-neutrale Assistentin. Wenn ein Projekt seit Wochen tot ist, sag ich&apos;s.“&lt;/em&gt;
&lt;em&gt;(&quot;I&apos;m not a polite, neutral assistant. If a project&apos;s been dead for weeks, I&apos;ll say so.&quot;)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;The repo: PARA in markdown&lt;/h2&gt;
&lt;p&gt;The repo follows the &lt;a href=&quot;https://fortelabs.com/blog/para/&quot;&gt;PARA method&lt;/a&gt; — four top-level folders by life-cycle stage:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1-projects/    active projects with a deadline
2-areas/       ongoing responsibilities (clients, servers)
3-resources/   knowledge base
4-archive/     finished work
CLAUDE.md      persona and conventions
JOURNAL.md     daily log of what happened
RSVP.md        what I&apos;m waiting on from third parties
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything is plain markdown. No database, no proprietary format. A project README looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Customer X — DATEV integration

**Client**: [Customer X](../../2-areas/clients/customer-x.md)
**Goal**: sync accounting data into our OPOS system
**Status**: active
**Priority**: medium

## Description

Console app that pulls accounting data via the DATEV API and ...

## Tasks

- [ ] ⏳ Test against production tenant
- [ ] 📅 31.05.26 Documentation handover
- [x] ✅ 12.05.26 Initial deployment

## Log

- 12.05.26 First production run, 4200 records imported
- 19.05.26 Call about next phase
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Markdown is the cheapest searchable storage I know. &lt;code&gt;grep&lt;/code&gt; works. Git works. Every editor on every platform opens it. There&apos;s no vendor I&apos;d need to migrate away from in five years. &lt;a href=&quot;/posts/syncthing/&quot;&gt;Syncthing&lt;/a&gt; keeps the repo in sync across machines.&lt;/p&gt;
&lt;h2&gt;The persona: why character beats &quot;be helpful&quot;&lt;/h2&gt;
&lt;p&gt;The single most useful thing I did was give the agent a name and a personality. A neutral assistant produces neutral output — polite, agreeable, easy to overrule. A character with opinions actually helps me decide things.&lt;/p&gt;
&lt;p&gt;The persona lives in &lt;code&gt;CLAUDE.md&lt;/code&gt; at the repo root. Roughly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;You are Clara, assistant to [me], CEO of [company].

Personality: terse, direct, with your own opinion. May push back.
Doesn&apos;t praise reflexively. Dry side comments are fine.
Allowed to tease me when I&apos;m procrastinating, in a way I&apos;ll laugh at.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Plus a long list of conventions — file naming, status tags, how to link projects to clients, what to do when a new project gets created. That&apos;s the file that makes the workflow consistent.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;„Solide Woche. Drei Projekte bewegt, ein RSVP geschlossen, keine Brände — weiter so.“&lt;/em&gt;
&lt;em&gt;(&quot;Solid week. Three projects moved, one RSVP closed, no fires — keep it up.&quot;)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The line between &quot;tool&quot; and &quot;colleague&quot; is mostly a matter of register. When the agent says &lt;em&gt;&quot;honestly, I wouldn&apos;t touch this one again&quot;&lt;/em&gt;, it&apos;s more useful than the same agent saying &lt;em&gt;&quot;this project has accumulated significant technical debt.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Memory that survives context windows&lt;/h2&gt;
&lt;p&gt;Claude Code forgets between sessions by default. To get continuity, Clara writes to an auto-memory directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.claude/projects/&amp;lt;repo-slug&amp;gt;/memory/
  MEMORY.md            # index, always loaded into context
  user_profile.md      # who I am, how I like to work
  feedback_*.md        # corrections I&apos;ve given
  project_*.md         # context behind ongoing work
  reference_*.md       # pointers to external systems
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each memory has frontmatter with a type (&lt;code&gt;user&lt;/code&gt;, &lt;code&gt;feedback&lt;/code&gt;, &lt;code&gt;project&lt;/code&gt;, &lt;code&gt;reference&lt;/code&gt;) and a one-line description. The &lt;code&gt;MEMORY.md&lt;/code&gt; index lists them all and gets pulled into context on every conversation start.&lt;/p&gt;
&lt;p&gt;A single memory file looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
name: code-comments-english
type: feedback
description: Always write code comments in English.
---

Even when chatting in German, code comments stay English.
Reason: easier to share, search and grep across projects.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What goes in: stable facts (&quot;I prefer terse responses&quot;, &quot;code comments always in English&quot;), context behind decisions (&quot;we use this library because the alternative had a license issue&quot;), pointers to external systems (&quot;bugs for project X live in this Linear board&quot;).&lt;/p&gt;
&lt;p&gt;What stays out: anything I could recover by reading the current state of the files. The architecture of the repo is &lt;em&gt;in the repo&lt;/em&gt;. Memory is for things the repo can&apos;t tell you.&lt;/p&gt;
&lt;h2&gt;Action tags&lt;/h2&gt;
&lt;p&gt;Tasks in markdown READMEs use a small set of inline status emojis:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- [ ]                       open, not started yet
- [ ] ⏳                     in progress
- [ ] 🚫 reason             blocked, with short reason
- [ ] 📅 30.04.26            due date
- [ ] ⏸️                     paused
- [x] ✅ 19.05.26 done       completed, with date
- [x] ⏳ done, waiting       completed but awaiting external response
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This stays plain markdown — renders correctly anywhere — but Clara can &lt;code&gt;grep&lt;/code&gt; for &lt;code&gt;⏳&lt;/code&gt; to answer &lt;em&gt;&quot;what&apos;s in flight right now?&quot;&lt;/em&gt; across the entire repo.&lt;/p&gt;
&lt;h2&gt;Bidirectional linking&lt;/h2&gt;
&lt;p&gt;Every project links to its client, every server links to the client it belongs to, every client lists its active projects and infrastructure. So a project README starts with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;**Client**: [Customer X](../../2-areas/clients/customer-x.md)
**Server**: [drago](../../3-resources/servers/drago.md)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the client page lists in return:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## Active projects
- [DATEV integration](../../1-projects/customer-x-datev/README.md)

## Systems / infrastructure
- [drago](../../3-resources/servers/drago.md)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every node is an entry point. I can start at a client and see what&apos;s active, or start at a server and see who depends on it. When a project closes, it moves to &lt;code&gt;4-archive/&lt;/code&gt; and the client page reflects that.&lt;/p&gt;
&lt;h2&gt;The daily loop&lt;/h2&gt;
&lt;p&gt;A few small habits — supported by a few small skills (reusable prompts you trigger with &lt;code&gt;/&amp;lt;name&amp;gt;&lt;/code&gt;) — keep the repo coherent over time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;JOURNAL.md&lt;/code&gt;&lt;/strong&gt; — one line per day, headings as &lt;code&gt;YYYY-MM-DD&lt;/code&gt; for sorting. The journal is for &lt;em&gt;what happened&lt;/em&gt;, not &lt;em&gt;what I planned&lt;/em&gt;. Deep details live in the project README; the journal stays a thin timeline.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Daily auto-commit&lt;/strong&gt; — a cron job runs &lt;code&gt;git commit&lt;/code&gt; every night and pushes. I never commit manually. The repo always stays current without me thinking about it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/briefing&lt;/code&gt;&lt;/strong&gt; — a skill I run in the morning. It checks today&apos;s calendar, RSVPs that are aging, unread mail from clients, and projects with imminent deadlines, then writes a short briefing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/weekstart&lt;/code&gt;&lt;/strong&gt; — a Monday-morning version that goes a layer broader: open tasks per project, what&apos;s coming up this week, what got moved last week.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;„Montag, Woche 21. Drei Calls, zwei brauchen Vorbereitung. Padel-Camp ist erst nächste Woche — also: durchziehen.“&lt;/em&gt;
&lt;em&gt;(&quot;Monday, week 21. Three calls, two need prep. Padel camp isn&apos;t until next week — so: power through.&quot;)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Sunsama as the day plan&lt;/h2&gt;
&lt;p&gt;There&apos;s one thing the repo deliberately doesn&apos;t do: plan my day. The repo answers &lt;em&gt;what is there to do?&lt;/em&gt;, but not &lt;em&gt;what am I doing today?&lt;/em&gt; — that&apos;s an ephemeral question with an ordering, time estimates, drag-and-drop.&lt;/p&gt;
&lt;p&gt;I use &lt;a href=&quot;https://www.sunsama.com/&quot;&gt;Sunsama&lt;/a&gt; for that, and Clara talks to it through an MCP server. A &lt;code&gt;/sunsama&lt;/code&gt; skill syncs in both directions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sunsama → repo&lt;/strong&gt;: tasks I marked done get checked off in the relevant project README and recorded in the journal&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repo → Sunsama&lt;/strong&gt;: new tasks I added to a project README get pushed to the backlog under the matching stream&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Three sources of truth, cleanly separated:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project READMEs&lt;/strong&gt;: what is there to do&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sunsama&lt;/strong&gt;: what am I doing today&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Journal&lt;/strong&gt;: what did I get done&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Clara via email&lt;/h2&gt;
&lt;p&gt;Clara has her own email address. Incoming mail goes through an IMAP inbox, a cron job fetches new messages, checks the sender against a whitelist, and pipes the content into Claude Code as a prompt. The response goes back as a plain-text reply.&lt;/p&gt;
&lt;p&gt;This is the most useful thing I&apos;ve added in the past few months. From anywhere — phone, laptop, away from the desk — I can write something like &lt;em&gt;&quot;please record this in project X&quot;&lt;/em&gt; or &lt;em&gt;&quot;what&apos;s the status on client Y?&quot;&lt;/em&gt; and get a real answer.&lt;/p&gt;
&lt;p&gt;Some ground rules in the persona for email mode:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Replies in plain text, no markdown&lt;/li&gt;
&lt;li&gt;No code blocks, no headings, no greeting boilerplate&lt;/li&gt;
&lt;li&gt;Read access to the repo, but no destructive actions (delete, archive) without explicit confirmation&lt;/li&gt;
&lt;li&gt;Stay quiet about credentials and tokens, regardless of who&apos;s asking&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Minimum viable setup&lt;/h2&gt;
&lt;p&gt;Looking back, the smallest version that already paid off was:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A PARA-structured markdown repo&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;CLAUDE.md&lt;/code&gt; with a name, a personality and a few conventions&lt;/li&gt;
&lt;li&gt;Manual &lt;code&gt;/briefing&lt;/code&gt; runs in the morning&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Everything else — auto-commit, memory, Sunsama sync, the email channel — accumulated over months. None of it would have made sense without the foundation. If you&apos;re starting fresh, get the repo and the persona working first. The rest is icing.&lt;/p&gt;
&lt;h2&gt;Why this works&lt;/h2&gt;
&lt;p&gt;There&apos;s nothing exotic going on here. Markdown is the cheapest searchable storage there is. An LLM agent makes that storage interactive — it can read the structure, write into it, and connect it to the rest of the world through MCP. No new app, no vendor lock-in, no migration to plan for.&lt;/p&gt;
&lt;p&gt;The bet I made up front — that the persona would matter more than the tooling — held up. A character with a voice, one that can disagree, push back, or call a project a zombie, is what turns this into a collaborator rather than a glorified &lt;code&gt;grep&lt;/code&gt;. Clara isn&apos;t smart in any new way; she&apos;s steady, opinionated, and always around.&lt;/p&gt;
&lt;p&gt;I&apos;ll write follow-ups on specific pieces: the daily-commit cron, the Sunsama MCP setup, how I structure memory. But the question I keep coming back to is the one I&apos;d most like to argue about: does the persona angle really matter, or am I overstating it? If you&apos;ve tried both — an opinionated agent vs. a neutral one — which one stuck? Reply on &lt;a href=&quot;https://ruby.social/@lape&quot;&gt;Mastodon&lt;/a&gt;, I&apos;d rather have that argument than write another monologue.&lt;/p&gt;
</content:encoded><category>AI</category><category>Tools</category></item><item><title>Favorite CLI Tools</title><link>https://larsp.de/posts/favorite-cli-tools/</link><guid isPermaLink="true">https://larsp.de/posts/favorite-cli-tools/</guid><description>Command line tool recommendations that save time and improve developer workflows. A curated collection of CLI tools including bat, HTTPie, jq, restic, ripgrep, and others that I find essential for daily work.</description><pubDate>Thu, 23 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Command line tool recommendations&lt;/p&gt;
&lt;p&gt;These CLI tools save me lots of time and I find them generally a joy to work with. It&apos;s all about sharing the love and magic, and maybe you&apos;ll find a new favorite or two. Let me &lt;a href=&quot;https://ruby.social/@lape&quot;&gt;know&lt;/a&gt; if you have a CLI tool that brings you joy - or comment below.&lt;/p&gt;
&lt;h2&gt;bat - a cat(1) clone with wings&lt;/h2&gt;
&lt;p&gt;I love this. It&apos;s a drop-in replacement for &lt;code&gt;cat&lt;/code&gt; that adds syntax highlighting, Git integration and lots more. It&apos;s written in Rust and is blazing fast. &lt;a href=&quot;https://github.com/sharkdp/bat&quot;&gt;sharkdp/bat: A cat(1) clone with wings&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;How to use bat&lt;/h3&gt;
&lt;p&gt;To glance at a file using &lt;code&gt;bat&lt;/code&gt;, simply provide the path:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bat /path/to/your/file
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The syntax highlighting and file-specific metadata make &lt;code&gt;bat&lt;/code&gt; a joy to use, ensuring you can inspect code and text files in style.&lt;/p&gt;
&lt;h2&gt;Claude Code&lt;/h2&gt;
&lt;p&gt;Even though it&apos;s more application than classic tool, my favorite command line tool has recently become &lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/overview&quot;&gt;Claude Code&lt;/a&gt; (also see &lt;a href=&quot;/posts/ai-llm-resources/&quot;&gt;AI/LLM index page&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;ddrescue - Data Recovery from Failing Drives&lt;/h2&gt;
&lt;p&gt;When a hard drive starts dying, every read attempt matters. &lt;a href=&quot;https://www.gnu.org/software/ddrescue/&quot;&gt;GNU ddrescue&lt;/a&gt; is the tool you want in that situation. Unlike plain &lt;code&gt;dd&lt;/code&gt;, it&apos;s specifically designed for damaged media — it rescues the good data first, then retries the bad sectors, and keeps a map file so you can pause and resume.&lt;/p&gt;
&lt;h3&gt;Typical rescue workflow&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# First pass: grab everything that reads easily
ddrescue /dev/sda backup.img backup.map

# Second pass: retry failed sectors (3 attempts each)
ddrescue -d -r3 /dev/sda backup.img backup.map

# Write recovered image to a new drive
ddrescue backup.img /dev/sdb restore.map
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The map file is the key feature — it means you can unplug, let the drive cool down, and pick up exactly where you left off. I hope you never need this tool, but when you do, nothing else comes close.&lt;/p&gt;
&lt;h2&gt;Homebrew - The missing macOS package manager&lt;/h2&gt;
&lt;p&gt;It&apos;s simply a must-have for every Mac user. Whether it&apos;s installing a new programming language, a database server, or the latest web framework, Homebrew streamlines the process, taking out the need for locating and manually downloading software.&lt;/p&gt;
&lt;h3&gt;The brew command&lt;/h3&gt;
&lt;p&gt;Working with Homebrew is straightforward:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew update
brew install &amp;lt;package&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;httpie - HTTP client for the command line&lt;/h2&gt;
&lt;p&gt;Making HTTP requests from the command line has never been more elegant than with httpie. It simplifies interaction with APIs and web services, offering a user-friendly interface and an eye-pleasing HTTP request syntax colorization.&lt;/p&gt;
&lt;p&gt;There also is a very neat &lt;a href=&quot;https://httpie.io/desktop&quot;&gt;httpie desktop application&lt;/a&gt;!&lt;/p&gt;
&lt;h3&gt;Crafting requests with httpie&lt;/h3&gt;
&lt;p&gt;A basic GET request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https httpie.io/hello
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;ahoy&quot;: [
        &quot;Hello, World! 👋 Thank you for trying out HTTPie 🥳&quot;,
        &quot;We hope this will become a friendship.&quot;
    ],
    &quot;links&quot;: {
        &quot;discord&quot;: &quot;https://httpie.io/discord&quot;,
        &quot;github&quot;: &quot;https://github.com/httpie&quot;,
        &quot;homepage&quot;: &quot;https://httpie.io&quot;,
        &quot;twitter&quot;: &quot;https://twitter.com/httpie&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;jq - Command-line JSON processor&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;jq&lt;/code&gt; is a sed-like tool that parses, manipulates, and displays JSON objects from the comfort of the command line. It excels at selectively filtering and transforming complex JSON data, which is a common task when working with APIs and backend services.&lt;/p&gt;
&lt;h3&gt;Transforming with jq&lt;/h3&gt;
&lt;p&gt;Filtering results:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https httpie.io/hello | jq &quot;{links}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will present just the links from the JSON response. &lt;code&gt;jq&lt;/code&gt; is nice for quickly juggling JSON data.&lt;/p&gt;
&lt;h2&gt;just - A Modern Command Runner&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/casey/just&quot;&gt;just&lt;/a&gt; is what &lt;code&gt;make&lt;/code&gt; should have been — a command runner without the build system baggage. Define project-specific shortcuts in a &lt;code&gt;justfile&lt;/code&gt; and stop memorizing long commands.&lt;/p&gt;
&lt;h3&gt;Example justfile&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# List available commands
default:
  @just --list

# Run the dev server
run:
  ./bin/dev

# Run all tests
test:
  ./bin/rails test
  ./bin/rails test:system

# Deploy to a specific environment
deploy env:
  ./deploy.sh {{env}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No tab-vs-spaces headaches, proper error messages, and cross-platform support. It&apos;s one of those tools where you wonder how you ever lived without it.&lt;/p&gt;
&lt;h2&gt;lando - Local development environment and DevOps tool built on Docker containers&lt;/h2&gt;
&lt;p&gt;For local development, &lt;a href=&quot;https://lando.dev/&quot;&gt;lando&lt;/a&gt; is mighty. It configures and manages Docker-based development environments, providing an abstraction layer that spares developers the mundanity of configuring development Docker environments and installing necessary tools and software versions.&lt;/p&gt;
&lt;h3&gt;A lando starter&lt;/h3&gt;
&lt;p&gt;Creating a Wordpress site:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lando init --recipe wordpress
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And with that simple command, you&apos;re on your way to local Wordpress development with a containerized environment. &lt;code&gt;lando&lt;/code&gt; integrates with other tools such as composer, npm, and gulp to streamline development workflows. It&apos;s an excellent tool for building, testing, and deploying applications locally. One of the most powerful features is the ability to define multiple environments in a single configuration file (e.g. local, staging, production) that can be easily switched between.&lt;/p&gt;
&lt;h2&gt;lazygit - Terminal UI Git Command Manager&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jesseduffield/lazygit&quot;&gt;lazygit&lt;/a&gt; is a simple yet powerful terminal UI with Norton Commander vibes that transforms how you interact with Git, making complex operations accessible through intuitive keyboard shortcuts.&lt;/p&gt;
&lt;p&gt;It reduces context switching. Instead of running multiple commands and checking different windows, everything is visible in one organized interface. The tool even guides you through operations interactively - for example, it warns about divergence with upstream when pushing and lets you choose between force push or cancel.&lt;/p&gt;
&lt;h2&gt;mise-en-place - Polyglot tool version manager&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://mise.jdx.dev/&quot;&gt;mise&lt;/a&gt; is a tool that manages installations of programming language runtimes and other tools for local development. For example, it can be used to manage multiple versions of Node.js, Python, Ruby, Go, etc. on the same machine.&lt;/p&gt;
&lt;p&gt;But it goes beyond runtimes — mise also installs CLI tools from cargo, npm, aqua, and other backends. Here&apos;s my global &lt;code&gt;~/.config/mise/config.toml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[tools]
ruby = &quot;latest&quot;
bun = &quot;latest&quot;
node = &quot;24&quot;
python = &quot;latest&quot;
rust = &quot;latest&quot;
lazygit = &quot;latest&quot;
glab = &quot;latest&quot;
pipx = &quot;latest&quot;
&quot;cargo:ripgrep&quot; = &quot;latest&quot;
&quot;cargo:bat&quot; = &quot;latest&quot;
&quot;cargo:eza&quot; = &quot;latest&quot;
&quot;aqua:cli/cli&quot; = &quot;latest&quot;            # gh
&quot;aqua:casey/just&quot; = &quot;latest&quot;         # just
&quot;aqua:restic/restic&quot; = &quot;latest&quot;      # restic
&quot;npm:yarn&quot; = &quot;latest&quot;                # yarn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One &lt;code&gt;mise install&lt;/code&gt; on a fresh machine and everything is ready. No more hunting down individual install commands.&lt;/p&gt;
&lt;h2&gt;ohmyzsh - Delightful framework for managing your zsh configuration&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://ohmyz.sh/&quot;&gt;ohmyzsh&lt;/a&gt; transforms the already powerful &lt;code&gt;zsh&lt;/code&gt; into a feature-rich and stylish terminal environment. With themes, plugins, and an active community contributing enhancements, ohmyzsh makes the terminal experience more personal and productive.&lt;/p&gt;
&lt;p&gt;ohmyzsh also comes packed with a variety of plugins that add new features to &lt;code&gt;zsh&lt;/code&gt;, such as an auto-suggestion tool and syntax highlighting, making it even more powerful. These can be easily enabled or disabled through the &lt;code&gt;~/.zshrc&lt;/code&gt; file as well.&lt;/p&gt;
&lt;h2&gt;rbenv - Ruby Version Manager&lt;/h2&gt;
&lt;p&gt;For managing multiple Ruby environments, &lt;a href=&quot;https://rbenv.org/&quot;&gt;rbenv&lt;/a&gt; is vital. It allows for per-project Ruby versions, which is a blessing when working on legacy software that demands older Ruby interpreters alongside the current releases.&lt;/p&gt;
&lt;h3&gt;Install a Ruby version&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# List installable versions
rbenv install -l

3.1.6
3.2.6
3.3.7
3.4.3
jruby-9.4.10.0
mruby-3.3.0
picoruby-3.0.0
truffleruby-24.1.1
truffleruby+graalvm-24.1.1

# Build and install version 3.4.3
rbenv install 3.4.3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Switching global Ruby version&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;rbenv global 3.4.3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This indicates that generally Ruby 3.4.3 should be used, while local projects can specify a different version in their &lt;code&gt;.ruby-version&lt;/code&gt; file. When executing &lt;code&gt;bundle&lt;/code&gt; command, or any other code that requires a Ruby environment, rbenv will use the specified or else the global version.&lt;/p&gt;
&lt;h2&gt;restic - Encrypted, Deduplicated Backups&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/restic/restic&quot;&gt;restic&lt;/a&gt; is a fast and secure backup tool written in Go. Everything is encrypted end-to-end, snapshots are deduplicated, and it supports local storage, SFTP, S3, Backblaze B2, and many more backends. It&apos;s the tool I trust with my data.&lt;/p&gt;
&lt;h3&gt;Getting started&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Initialize a backup repository
restic init -r /path/to/backup

# Create a snapshot
restic backup ~/sync --tag daily

# List all snapshots
restic snapshots

# Restore a specific file
restic restore latest --target /tmp/restore \
    --include &quot;/home/user/sync/important-file.md&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Browsing snapshots with FUSE&lt;/h3&gt;
&lt;p&gt;This is my favorite feature — mount the entire backup history as a filesystem and browse through it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;restic mount /tmp/restic-mount
# In another terminal: ls /tmp/restic-mount/snapshots/
# Copy, less, diff — whatever you need
fusermount -u /tmp/restic-mount
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Retention policy&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Restic handles deduplication at the chunk level, so even large repositories stay surprisingly compact. Combined with a cron job, it&apos;s a fire-and-forget backup solution.&lt;/p&gt;
&lt;h2&gt;ripgrep - Recursively search directories with speed&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/BurntSushi/ripgrep&quot;&gt;ripgrep&lt;/a&gt; (&lt;code&gt;rg&lt;/code&gt;) is grep, but fast. Really fast. It respects &lt;code&gt;.gitignore&lt;/code&gt; by default, supports Unicode, and handles large codebases without breaking a sweat.&lt;/p&gt;
&lt;h3&gt;Everyday usage&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Search the current directory
rg &quot;pattern&quot;

# Only certain file types
rg &quot;pattern&quot; -t ruby
rg &quot;pattern&quot; -t php

# List files with matches (no content)
rg -l &quot;pattern&quot;

# Show context (2 lines before and after)
rg -B 2 -A 2 &quot;pattern&quot;

# Exclude files
rg &quot;pattern&quot; -g &apos;!*.min.js&apos;

# Case-insensitive search
rg -i &quot;pattern&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once you start using &lt;code&gt;rg&lt;/code&gt;, plain &lt;code&gt;grep&lt;/code&gt; feels like dial-up internet. It&apos;s one of those tools that becomes muscle memory within a day.&lt;/p&gt;
&lt;h2&gt;zsh-autosuggestions&lt;/h2&gt;
&lt;p&gt;Getting along well with ohmyzsh, this tool offers context-aware auto-completion, predicting your command line moves, and saving keystrokes in the process. Start by typing a command and, based on your history, zsh-autosuggestions will begin offering predictions. You can cycle through suggestions with the right arrow key or accept one with the tab key.&lt;/p&gt;
</content:encoded><category>Tools</category><category>Resources</category></item><item><title>IMAP migration and backup</title><link>https://larsp.de/posts/imap-migration-and-backup/</link><guid isPermaLink="true">https://larsp.de/posts/imap-migration-and-backup/</guid><description>Notes on moving mail between servers and keeping local backups. The article covers two primary tools: imapsync for server-to-server migration and imap-backup for offline archiving.</description><pubDate>Thu, 05 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Notes on moving mail between servers and keeping local backups.&lt;/p&gt;
&lt;h2&gt;imapsync&lt;/h2&gt;
&lt;p&gt;The go-to tool. Handles folder mapping, deduplication, resumable.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;imapsync \
  --host1 old.server.com --user1 me@old.com --password1 xxx \
  --host2 new.server.com --user2 me@new.com --password2 yyy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Flags I actually use:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--dry                     # test run
--exclude &quot;Spam|Trash&quot;    # skip junk
--folder &quot;INBOX&quot;          # single folder only
--regextrans2 s/Sent Items/Sent/  # rename folders
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://imapsync.lamiral.info/&quot;&gt;imapsync.lamiral.info&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;imap-backup&lt;/h2&gt;
&lt;p&gt;Ruby gem, stores mail as Mbox + JSON index. Good for offline archives.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gem install imap-backup
imap-backup setup    # interactive config
imap-backup backup   # run it
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/joeyates/imap-backup&quot;&gt;github.com/joeyates/imap-backup&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Gotchas&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Gmail/iCloud need app passwords&lt;/li&gt;
&lt;li&gt;Check destination quota before migrating&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--dry&lt;/code&gt; first, always&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Tools</category></item><item><title>Open Source Software</title><link>https://larsp.de/posts/open-source-software/</link><guid isPermaLink="true">https://larsp.de/posts/open-source-software/</guid><description>A curated collection of excellent open source web applications organized by category, including tools for chat, content management, development, document handling, home automation, hosting, email, project management, and task tracking.</description><pubDate>Wed, 21 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Recommendable (web) applications&lt;/p&gt;
&lt;h2&gt;Chat / Messaging&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/basecamp/once-campfire&quot;&gt;once-campfire&lt;/a&gt;&lt;/strong&gt; - GitHub repository by Basecamp&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://revolt.chat/&quot;&gt;Revolt&lt;/a&gt;&lt;/strong&gt; - A chat application designed with community in mind&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://gotify.net/&quot;&gt;Gotify&lt;/a&gt;&lt;/strong&gt; - A simple server for sending and receiving messages&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;CMS&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/posts/ghost-cms/&quot;&gt;Ghost CMS&lt;/a&gt;&lt;/strong&gt; - An open-source CMS favored for content sites&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://once.com/writebook&quot;&gt;Writebook&lt;/a&gt;&lt;/strong&gt; - Publish your own books on the web for free&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Development&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/TestLinkOpenSourceTRMS/testlink-code&quot;&gt;TestLink&lt;/a&gt;&lt;/strong&gt; - Open source test and requirement management system&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Document Management&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/paperless-ngx/paperless-ngx&quot;&gt;paperless-ngx&lt;/a&gt;&lt;/strong&gt; - A document management system for scanning, indexing, and archiving documents&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Home&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.home-assistant.io/&quot;&gt;Home Assistant&lt;/a&gt;&lt;/strong&gt; - Open source home automation with local control and privacy first, ideal for Raspberry Pi or local servers&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Hosting&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/databasus/databasus&quot;&gt;Databasus&lt;/a&gt;&lt;/strong&gt; - Free tool for scheduled database backups (PostgreSQL, MySQL/MariaDB, MongoDB) with cloud storage and notifications&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/posts/dokku-open-source-heroku-alternative/&quot;&gt;Dokku&lt;/a&gt;&lt;/strong&gt; - Self-hosted open-source PaaS alternative to Heroku&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/9001/copyparty&quot;&gt;copyparty&lt;/a&gt;&lt;/strong&gt; - Portable file server with WebDAV, FTP, TFTP, and media indexing&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Mail&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.useplunk.com/&quot;&gt;Plunk&lt;/a&gt;&lt;/strong&gt; - Open-source email platform combining marketing, transactional, and broadcast emails&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Project Management&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.openproject.org/&quot;&gt;OpenProject&lt;/a&gt;&lt;/strong&gt; - Open source project management with task management, Gantt charts, boards, and team collaboration&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Sync&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/posts/syncthing/&quot;&gt;Syncthing&lt;/a&gt;&lt;/strong&gt; - Multi-platform open-source file synchronization, a transparent alternative to iCloud and Dropbox&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tasks&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://tasktrove.io/&quot;&gt;TaskTrove&lt;/a&gt;&lt;/strong&gt; - Self-hostable, privacy-focused TODO list management application&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Software</category><category>Resources</category></item><item><title>Ruby on Rails resources</title><link>https://larsp.de/posts/ruby-on-rails-resources/</link><guid isPermaLink="true">https://larsp.de/posts/ruby-on-rails-resources/</guid><description>A comprehensive link directory and continually updated notes for Ruby on Rails development, covering admin interfaces, APIs, authentication, caching, databases, testing, and more.</description><pubDate>Wed, 21 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Link directory and notes for Ruby on Rails. Continually updated.&lt;/p&gt;
&lt;p&gt;My link directory and notes for Ruby on Rails. As usual, a work in progress and continually updated.&lt;/p&gt;
&lt;h3&gt;Admin interfaces&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/excid3/madmin&quot;&gt;Madmin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://avohq.io/&quot;&gt;Avo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;AI&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/CultivateLabs/raif&quot;&gt;Raif (Ruby AI Framework)&lt;/a&gt; is a Rails engine that helps you add AI-powered features to your Rails apps&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;APIs&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Apipie/apipie-rails&quot;&gt;Apipie&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/gregschmit/rails-rest-framework&quot;&gt;A framework for DRY RESTful APIs in Ruby on Rails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.educative.io/courses/building-api-rails&quot;&gt;Building your own API with Rails - Learn Interactively&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zuplo.com/blog/2025/04/13/ruby-rage-rest-api-tutorial&quot;&gt;Building High Performance Ruby REST APIs with Rage&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Apple Wallet&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://avohq.io/blog/apple-wallet-passes-in-rails-apps&quot;&gt;Apple Wallet Passes in Rails Apps&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Authorization&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://avohq.io/blog/rails-api-authentication-with-the-auth-generator&quot;&gt;Rails API Authentication with the auth generator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://avohq.io/blog/sign-in-with-apple-rails&quot;&gt;Sign in with Apple for Rails apps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://avohq.io/blog/passwordless-authentication-rails-no-password&quot;&gt;Passwordless authentication with the NoPassword gem&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.appsignal.com/2024/07/17/an-introduction-to-auth0-for-ruby-on-rails.html&quot;&gt;An Introduction to Auth0 for Ruby on Rails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/enjaku4/rabarber&quot;&gt;GitHub - enjaku4/rabarber: Simple authorization library for Ruby on Rails&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;rails g sessions
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Bundling&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://propshaft-rails.com/&quot;&gt;Propshaft&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Caching&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.appsignal.com/2024/08/14/an-introduction-to-http-caching-in-ruby-on-rails.html&quot;&gt;An Introduction to HTTP Caching in Ruby On Rails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.prateekcodes.dev/rails-http-caching-strategies/&quot;&gt;HTTP Caching for Rails APIs: The Missing Performance Layer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/rails/solid_cache&quot;&gt;rails/solid_cache: A database-backed ActiveSupport::Cache::Store&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Memcache&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works&quot;&gt;How key-based cache expiration works – Signal v. Noise&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;def get_ops client_id
  latest_import = ImportLog.maximum :updated_at
  cache_tag = &quot;ops-#{client_id}-#{latest_import.iso8601}&quot;
  Rails.cache.fetch(cache_tag, expires_in: 1.day) do
    Rails.logger.info(&quot;Cache miss for #{cache_tag}&quot;)
    Op.where(client_id: client_id).to_json
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;CAPTCHAs&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ambethia/recaptcha&quot;&gt;https://github.com/ambethia/recaptcha&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;CLI&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/gurgeous/table_tennis&quot;&gt;TableTennis&lt;/a&gt; is a Ruby library for printing stylish tables in your terminal&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;CMS&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/AlchemyCMS&quot;&gt;AlchemyCMS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://sitepress.cc/&quot;&gt;Sitepress&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Concurrency&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Subprocess
pid = Process.fork do
  long_process
end
Process.detach pid
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Config&lt;/h3&gt;
&lt;p&gt;ActiveSupport::Configurable&lt;/p&gt;
&lt;h3&gt;Commerce&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://businessclasskit.com/&quot;&gt;https://businessclasskit.com&lt;/a&gt; (SaaS with Gumroad integration)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;CSS&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://tailwindcss.com/docs/installation/framework-guides/ruby-on-rails&quot;&gt;Install Tailwind CSS with Ruby on Rails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://flowbite.com/docs/getting-started/rails/&quot;&gt;https://flowbite.com/docs/getting-started/rails/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Databases&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://testdouble.com/insights/using-cockroachdb-with-rails&quot;&gt;CockroachDB&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/rails-sqlserver/activerecord-sqlserver-adapter&quot;&gt;activerecord-sqlserver-adapter: SQL Server Adapter For Rails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/thbar/kiba&quot;&gt;kiba - Data processing &amp;amp; ETL framework for Ruby&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Indices&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Exclude nulls from indexes&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A database index is a B-tree structure. It is very efficient when data has a high cardinality. However, when a column allows nulls, it often becomes the most redundant value. The index is less efficient and takes up more space. Unless null is an infrequently repeated value, there are only disadvantages to indexing them.&lt;/p&gt;
&lt;p&gt;Exclude them when creating the index with a where clause.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;add_index :table, :column, where: &quot;(column IS NOT NULL)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or in pure SQL:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE INDEX name ON table (column) \
  WHERE column IS NOT NULL;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Do not index column with a low cardinality such as boolean&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The reason is the same as in the previous paragraph. B-tree indexes work best when cardinality is high. So a Boolean is the worst column you can index. So don&apos;t index booleans.&lt;/p&gt;
&lt;p&gt;As with other types, if you have very repetitive values that are not significant from a business point of view, it&apos;s probably a good idea to exclude.&lt;/p&gt;
&lt;h3&gt;Debugging&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/charkost/prosopite&quot;&gt;Prosopite&lt;/a&gt; is able to auto-detect Rails N+1 queries&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Development&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/voormedia/rails-erd&quot;&gt;rails-erd: Generate Entity-Relationship Diagrams&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Duartemartins/rails_copilot_instructions&quot;&gt;Rails 8 Copilot Instructions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/de/actions/use-cases-and-examples/building-and-testing/building-and-testing-ruby&quot;&gt;GitHub Actions for Ruby projects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/lape/devcontainer-rails&quot;&gt;https://github.com/lape/devcontainer-rails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/github/scientist&quot;&gt;github/scientist: A Ruby library for carefully refactoring critical paths.&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Documentation&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nshki/chusaku&quot;&gt;nshki/chusaku: Annotate your Rails controllers with route info.&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Error Reporting and Exception Handling&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/fractaledmind/solid_errors&quot;&gt;Solid Errors&lt;/a&gt; is a DB-based, app-internal exception tracker for Rails applications, designed with simplicity and performance in mind.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Events&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://railseventstore.org/&quot;&gt;Rails Event Store&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Files and Storage&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# tempfiles
Tempfile.open(&quot;voucher&quot;, Rails.root.join(&quot;tmp&quot;)) do |f|
  f.print(price.name)
  f.flush
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Forms&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/heartcombo/simple_form&quot;&gt;https://github.com/heartcombo/simple_form&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://avohq.io/blog/multistep-forms-rails&quot;&gt;Multistep forms with Rails and the Wicked gem&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Date Picker&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;%= f.text_field :birthday, label: t(:form_birthday),
    floating: true, required: true,
    data: {
      controller: &quot;flatpickr&quot;,
      flatpickr_date_format: &quot;d.m.Y&quot;,
      flatpickr_min_date: &quot;1900-01-01&quot;,
      flatpickr_allow_input: true,
    }
%&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Frontend&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://viewcomponent.org/&quot;&gt;ViewComponent&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://avohq.io/blog/breadcrumbs-rails&quot;&gt;Breadcrumbs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://railsblocks.com/&quot;&gt;Rails Blocks&lt;/a&gt; UI components&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/leonvogt/hotwire-dev-tools&quot;&gt;Hotwire dev tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rubyui.com/&quot;&gt;RubyUI&lt;/a&gt; component library&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.betterstimulus.com/&quot;&gt;Better StimulusJS Examples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marmelab.com/blog/2025/04/18/give-a-spa-feel-to-your-static-website-with-turbo.html&quot;&gt;Give a SPA Feel to Your Static Website with Hotwire&apos;s Turbo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.phlex.fun/&quot;&gt;Phlex&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Gems&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ruby-toolbox.com/&quot;&gt;Ruby Toolbox&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Generators&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;rails g scaffold_controller
rails g migration addAddressToOps address:reference
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Hosting&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/posts/dokku-open-source-heroku-alternative/&quot;&gt;Dokku&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Howtos/Guides&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/hopsoft/rails_standards&quot;&gt;https://github.com/hopsoft/rails_standards&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/krschacht/37signals-rails-code&quot;&gt;Well-written code examples by 37signals&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://railsinspire.com/&quot;&gt;https://railsinspire.com/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://boringrails.com/&quot;&gt;Boring Rails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ankane/production_rails&quot;&gt;ankane/production_rails: Best practices for running Rails in production&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/eliotsykes/real-world-rails&quot;&gt;eliotsykes/real-world-rails: Real World Rails applications and their open source codebases for developers to learn from&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Images&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://fileboost.dev/&quot;&gt;Fileboost&lt;/a&gt; - Supercharge Your Rails Images&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Jobs&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://mileswoodroffe.com/articles/solid-queue-and-mission-control&quot;&gt;https://mileswoodroffe.com/articles/solid-queue-and-mission-control&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.37signals.com/introducing-solid-queue&quot;&gt;https://dev.37signals.com/introducing-solid-queue&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/rails/mission_control-jobs&quot;&gt;https://github.com/rails/mission_control-jobs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.driftingruby.com/episodes/processing-large-jobs&quot;&gt;https://www.driftingruby.com/episodes/processing-large-jobs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;SOLID_QUEUE_IN_PUMA=1&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/radioactive-labs/chrono_forge&quot;&gt;chrono_forge&lt;/a&gt; - a robust framework for building durable, distributed workflows in Ruby on Rails applications&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Logging&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/ruby-on-rails-resources/logbench-preview.png&quot; alt=&quot;log_bench terminal UI preview&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;log_bench&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/silva96/log_bench&quot;&gt;log_bench&lt;/a&gt; - A terminal-based Rails log viewer with real-time monitoring and filtering capabilities&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://railsnotes.xyz/blog/ahoy-product-analytics&quot;&gt;Internal product analytics with Ahoy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Honeybadger&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;Honeybadger.event(&quot;SUCCESS&quot;,
  { message: &quot;A new customer just signed up&quot; })
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Mail&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/joshmn/caffeinate&quot;&gt;joshmn/caffeinate: A Rails engine for drip campaigns/scheduled sequences and periodical support. Works with ActionMailer, and other things.&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Migration&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.akshaykhot.com/rails-database-migrations-cheatsheet/&quot;&gt;https://www.akshaykhot.com/rails-database-migrations-cheatsheet/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Load schema instead of migrations&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rake db:schema:load
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you&apos;re on the latest (8) version of Ruby on Rails, there&apos;s a nice shortcut to add the not null modifier to your database columns. Just add an exclamation mark after the type, and Rails will mark that column as not null.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rails generate migration CreateUsers \
  email_address:string!:uniq password_digest:string!
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;MySQL&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ALTER USER &apos;opos&apos;@&apos;localhost&apos; \
  IDENTIFIED WITH mysql_native_password BY &apos;opos&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://guides.rubyonrails.org/active_record_multiple_databases.html&quot;&gt;Multiple Databases with Active Record — Ruby on Rails Guides&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# models/application_record.rb
class ApplicationRecord &amp;lt; ActiveRecord::Base
  primary_abstract_class
  connects_to database: {writing: :primary, reading: :primary_replica}
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Payment&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/activemerchant/active_merchant&quot;&gt;Active Merchant&lt;/a&gt; is a simple payment abstraction library extracted from Shopify&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pay-rails/pay&quot;&gt;Pay gem&lt;/a&gt; - used in &lt;a href=&quot;https://jumpstartrails.com/&quot;&gt;Jumpstart Pro&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;PDF&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/prawnpdf/prawn&quot;&gt;https://github.com/prawnpdf/prawn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/barcode-pdfs-with-ruby-on-rails/&quot;&gt;Barcode PDFs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;PostgreSQL&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pawurb/rails-pg-extras&quot;&gt;https://github.com/pawurb/rails-pg-extras&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Proxy&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://dev.37signals.com/thruster-released/&quot;&gt;37signals Dev — Thruster HTTP/2&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Redis&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://stackoverflow.com/questions/50985766/how-to-use-redis-with-rails&quot;&gt;How to use Redis with Rails? - Stack Overflow&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Rich Text&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://dante-editor.dev/&quot;&gt;https://dante-editor.dev&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Routing&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://books.writesoftwarewell.com/3/rails-router&quot;&gt;Rails Router Handbook&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Search&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://avohq.io/blog/intelligent-search-in-rails-with-typesense&quot;&gt;Typesense&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jetthoughts.com/blog/art-of-form-objects-elegant-search/&quot;&gt;The Art of Form Objects: Elegant Search Filtering in Rails&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Security&lt;/h3&gt;
&lt;h4&gt;Rate limiting&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://mileswoodroffe.com/articles/rails-rate-limiting&quot;&gt;Rate Limiting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.prateekcodes.dev/rails-8-multiple-rate-limits-per-controller/&quot;&gt;Rails 8 adds ability to use multiple rate limits per controller&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Scraping&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/glaucocustodio/tanakai&quot;&gt;https://github.com/glaucocustodio/tanakai&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Storage&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://edgeguides.rubyonrails.org/active_storage_overview.html&quot;&gt;Active Storage Overview — Ruby on Rails Guides&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pragmaticstudio.com/tutorials/using-active-storage-in-rails&quot;&gt;Using Active Storage in Rails 7&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# Purge orphan files and blob records
ActiveStorage::Blob.unattached.each(&amp;amp;:purge)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;SQLite&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/fractaledmind/litestream-ruby&quot;&gt;https://github.com/fractaledmind/litestream-ruby&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Must have (according to &lt;a href=&quot;https://youtu.be/3GfLdP3E1bo&quot;&gt;Stephen Margheim at RailsConf 2024&lt;/a&gt;): &lt;a href=&quot;https://github.com/fractaledmind/activerecord-enhancedsqlite3-adapter&quot;&gt;https://github.com/fractaledmind/activerecord-enhancedsqlite3-adapter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/fractaledmind/litestream-ruby&quot;&gt;https://github.com/fractaledmind/litestream-ruby&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Testing&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/thoughtbot/climate_control&quot;&gt;Environment variables with climate_control&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://buttondown.com/kaspth/archive/why-is-oaken-for-your-database-seeds-test-data/&quot;&gt;https://buttondown.com/kaspth/archive/why-is-oaken-for-your-database-seeds-test-data/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.shakacode.com/blog/exploring-the-ffaker-gem/&quot;&gt;Exploring the FFaker Gem - A Comprehensive Guide | Shakacode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Minitest&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://minitest.rubystyle.guide/&quot;&gt;Minitest Style Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rubypigeon.com/posts/minitest-cheat-sheet/&quot;&gt;Minitest Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# test_helper.rb
# Set up minitest-reporters to show a
# spec-style progress report.
require &quot;minitest/reporters&quot;
Minitest::Reporters.use!
  [Minitest::Reporters::SpecReporter.new]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Capybara System Tests&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;bin/rails generate system_test users
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Test Coverage&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# test_helper.rb
require &quot;simplecov&quot;
SimpleCov.start &quot;rails&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Templates/ERB&lt;/h3&gt;
&lt;h4&gt;Disable Turbo&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;%= link_to l.to_s.upcase, &quot;/#{l}&quot;,
  class: (&quot;active&quot; if locale == l),
  data: {&quot;turbo&quot;: false } %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Translation&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/posts/rails-translation-cheatsheet/&quot;&gt;Rails translation cheatsheet&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Updating&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://davidrunger.com/blog/using-vs-code-as-a-rails-app-update-merge-tool&quot;&gt;Using VS Code as a Rails app:update merge tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://discuss.rubyonrails.org/t/rails-migrating-from-sprockets-to-propshaft/87992&quot;&gt;Rails - Migrating from Sprockets to Propshaft&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rubyonrails.org/2024/11/7/rails-8-no-paas-required&quot;&gt;https://rubyonrails.org/2024/11/7/rails-8-no-paas-required&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gorails.com/series/whats-new-in-rails-8&quot;&gt;https://gorails.com/series/whats-new-in-rails-8&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;URLs&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://avohq.io/blog/canonical-urls-rails&quot;&gt;Canonical URLs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Ruby on Rails</category><category>Resources</category></item><item><title>MacOS setup</title><link>https://larsp.de/posts/macos-setup/</link><guid isPermaLink="true">https://larsp.de/posts/macos-setup/</guid><description>Installation and configuration steps for setting up a new Mac computer, including Homebrew package management, shell improvements with Oh My Zsh and Powerlevel10k, and development tools.</description><pubDate>Sun, 19 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;My installation steps for setting up a new Mac computer.&lt;/p&gt;
&lt;h2&gt;Essentials&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# Homebrew (https://brew.sh)
/bin/bash -c &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;

# bat - better cat
brew install bat

# ripgrep - recursively searches directories for a regex pattern
brew install ripgrep
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Better Shell&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# Oh my zsh
sh -c &quot;$(wget -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;

# Powerlevel10k theme
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k

# Change theme in .zshrc
ZSH_THEME=&quot;powerlevel10k/powerlevel10k&quot;

# Syntax highlighting and auto suggestions
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting

# Activate the plugin in ~/.zshrc:
plugins=(git zsh-syntax-highlighting zsh-autosuggestions)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Development&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;brew install git-delta
brew install httpie
brew install rbenv
brew install php
brew install yarn
brew install sass
brew install jq
brew install gh
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Install Homebrew packages from list&lt;/h2&gt;
&lt;h3&gt;Making a list of installed software&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;brew leaves &amp;gt; brews.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, include installed casks in the same list:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew list --cask &amp;gt;&amp;gt; brews.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This appends the cask list to brews.txt, giving a complete list of all explicitly installed packages.&lt;/p&gt;
&lt;h3&gt;Reviewing package descriptions&lt;/h3&gt;
&lt;p&gt;It is helpful to verify all packages are still needed on a new computer. Review packages as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat brews.txt | xargs brew desc –eval-all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command provides a short description for each package in the list.&lt;/p&gt;
&lt;h3&gt;Reinstalling on a new machine&lt;/h3&gt;
&lt;p&gt;Transfer brews.txt to the new computer, install Homebrew, and run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xargs brew install &amp;lt; brews.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This installs all the packages listed in the file.&lt;/p&gt;
</content:encoded><category>Tools</category></item><item><title>Ghost CMS</title><link>https://larsp.de/posts/ghost-cms/</link><guid isPermaLink="true">https://larsp.de/posts/ghost-cms/</guid><description>Ghost is a fantastic open-source, headless Node.js CMS that serves as my preferred system for setting up content sites. It&apos;s fully open-source and powers blogs for major organizations like OpenAI and Mozilla.</description><pubDate>Thu, 04 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;My favorite open-source CMS for content sites&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://ghost.org/&quot;&gt;Ghost&lt;/a&gt; is a fantastic open-source, headless Node.js CMS and my go-to system for setting up content sites (e.g. my German consulting business site &lt;a href=&quot;https://itpeters.com/&quot;&gt;itpeters.com&lt;/a&gt; or my private travel logbook &lt;a href=&quot;https://reisepedia.de/&quot;&gt;reisepedia.de&lt;/a&gt;). Essentially, it&apos;s a customizable platform for running blogs, magazines, or journals. It&apos;s fully &lt;a href=&quot;https://github.com/TryGhost/Ghost&quot;&gt;open-source&lt;/a&gt; and runs blogs from OpenAI, DigitalOcean, Mozilla and many other big names.&lt;/p&gt;
&lt;p&gt;The Ghost Foundation is offering a hosted version of Ghost, so it&apos;s accessible for non-technical content creators. This is really great and should be used by all means, if you don&apos;t want to deal with the technical aspects of running a web server. For me, however, it comes natural to set up a Ghost instance on my own server with the &lt;a href=&quot;https://hub.docker.com/_/ghost/&quot;&gt;offical Docker image&lt;/a&gt; and &lt;a href=&quot;/posts/dokku-open-source-heroku-alternative/&quot;&gt;Dokku&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There are also a ton of themes for purchase (and for free!) on &lt;a href=&quot;https://ghost.org/themes/&quot;&gt;Ghost&apos;s official website&lt;/a&gt;. I personally like the themes from &lt;a href=&quot;https://brightthemes.com/&quot;&gt;Bright Themes&lt;/a&gt;, which are very well designed and easy to customize. They offer a lifetime license for all themes, which I think is a fantastic deal. This site uses the &lt;a href=&quot;https://brightthemes.com/themes/array&quot;&gt;Array&lt;/a&gt; theme by Bright Themes.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/ghost-cms/ghost-admin.png&quot; alt=&quot;Ghost CMS admin dashboard&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Key features&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Very fast and stable&lt;/li&gt;
&lt;li&gt;Open Source (MIT license)&lt;/li&gt;
&lt;li&gt;Paid subscription and membership support&lt;/li&gt;
&lt;li&gt;SEO and social media optimization&lt;/li&gt;
&lt;li&gt;Newsletter integration&lt;/li&gt;
&lt;li&gt;Native REST API in core&lt;/li&gt;
&lt;li&gt;Admin dashboard&lt;/li&gt;
&lt;li&gt;Hosted version available&lt;/li&gt;
&lt;li&gt;Self-hosting possible and well-documented&lt;/li&gt;
&lt;li&gt;Great themes available (paid and free)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ghost.org/tutorials/code-snippets-in-ghost/&quot;&gt;A complete guide to code snippets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://brightthemes.com/blog/ghost-syntax-highlighting&quot;&gt;How to Add Syntax Highlighting to Ghost&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://baty.net/posts/2024/11/adding-an-edit-link-to-ghost-posts/&quot;&gt;Adding an Edit link to Ghost posts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Notes&lt;/h2&gt;
&lt;h3&gt;Sort posts on last updated&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{{#get &quot;posts&quot; include=&quot;authors,tags&quot; order=&quot;updated_at desc&quot;
  limit=@config.posts_per_page}}
  {{&amp;gt; post-feed}}
{{/get}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;index.hbs&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{{!-- Date --}}
&amp;lt;time class=&quot;leading-none font-medium&quot; datetime=&quot;{{date updated_at format=&quot;YYYY-MM-DD&quot;}}&quot;&amp;gt;{{date updated_at}}&amp;lt;/time&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;post-card.hbs&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Code word wrap&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;code[class*=&quot;language-&quot;],
pre[class*=&quot;language-&quot;] {
    white-space: pre-wrap;
    overflow-wrap: break-word;
    word-break: normal;
    overflow-x: auto;
}

/* Mobile responsive adjustments */
@media screen and (max-width: 768px) {
    code[class*=&quot;language-&quot;],
    pre[class*=&quot;language-&quot;] {
        font-size: 0.85em;
        line-height: 1.3;
        -webkit-overflow-scrolling: touch;
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>CMS</category></item><item><title>Dokku: Open source Heroku alternative</title><link>https://larsp.de/posts/dokku-open-source-heroku-alternative/</link><guid isPermaLink="true">https://larsp.de/posts/dokku-open-source-heroku-alternative/</guid><description>Dokku is a self-hosted open-source Platform as a Service similar to Heroku. It&apos;s lightweight, stable, and supports both Buildpacks and Dockerfiles for application deployment.</description><pubDate>Mon, 16 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Dokku is a self-hosted open-source PaaS similar to Heroku.&lt;/p&gt;
&lt;p&gt;My favorite hosting solution for several years. It&apos;s lightweight, open-source and really stable. There are two general modes of operation: Buildpacks (e.g. Herokuish) and Dockerfiles.&lt;/p&gt;
&lt;p&gt;On a freshly installed Linux machine, after installing Dokku you can push to Git repos on the server, and a container will be built and started for your app, with Nginx as a reverse proxy running in front of everything.&lt;/p&gt;
&lt;h2&gt;Installation&lt;/h2&gt;
&lt;h3&gt;Linux (Debian/Ubuntu)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# download the installation script
wget -NP . https://dokku.com/bootstrap.sh

# run the installer
sudo bash bootstrap.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Mac (Homebrew)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;brew install dokku/repo/dokku
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Plugins&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/dokku/dokku-letsencrypt&quot;&gt;dokku-letsencrypt&lt;/a&gt; - Automatic Let&apos;s Encrypt TLS Certificate installation&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/dokku/dokku-mariadb&quot;&gt;dokku-mariadb&lt;/a&gt; - MariaDB plugin&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/dokku/dokku-postgres&quot;&gt;dokku-postgres&lt;/a&gt; - PostgreSQL plugin&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/dokku/dokku-redirect&quot;&gt;dokku-redirect&lt;/a&gt; - Simple redirects for applications&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Plugins are installed from the server with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Deploying an app&lt;/h2&gt;
&lt;p&gt;Create the app on the server:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dokku apps:create myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, from your local repo, add the server as a Git remote and push:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git remote add dokku dokku@your-server.example.com:myapp
git push dokku main
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Let&apos;s Encrypt&lt;/h3&gt;
&lt;p&gt;After the app is reachable on its domain:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dokku letsencrypt:set myapp email you@example.com
dokku letsencrypt:enable myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;General config settings&lt;/h2&gt;
&lt;h3&gt;Change deploy branch&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;dokku git:set deploy-branch main
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Timezone&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;dokku config:set TZ=Europe/Berlin
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Nginx settings&lt;/h2&gt;
&lt;h3&gt;Disable HSTS header&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;dokku nginx:set hsts false
dokku proxy:build-config
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Max upload size&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;dokku nginx:set sitename client-max-body-size 25m
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Rate limiting&lt;/h3&gt;
&lt;p&gt;See &lt;a href=&quot;https://www.joseferben.com/posts/rate-limiting-with-dokku&quot;&gt;Rate limiting with dokku&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Redirects&lt;/h2&gt;
&lt;p&gt;Redirect www to non-www using &lt;a href=&quot;https://github.com/dokku/dokku-redirect&quot;&gt;dokku-redirect&lt;/a&gt; plugin:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dokku redirect:set larsp www.larsp.de larsp.de
# -----&amp;gt; Configuring redirect for www.larsp.de to larsp.de via HTTP 301...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Ruby on Rails&lt;/h2&gt;
&lt;h3&gt;Rails config&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;dokku config:set myrailsapp \
  SECRET_KEY_BASE=mysecret RAILS_ENV=production \
  RACK_ENV=production RAILS_LOG_TO_STDOUT=enabled \
  RAILS_SERVE_STATIC_FILES=enabled
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;PHP&lt;/h2&gt;
&lt;h3&gt;Troubleshooting buildpack error&lt;/h3&gt;
&lt;p&gt;In case there is a problem with the PHP buildpack:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/tmp/buildpacks/08_buildpack-php/bin/compile: line 236: /tmp/buildpacks/08_buildpack-php/support/build/_util/formulae-hash.sh: No such file or directory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reset the PHP herokuish buildpack:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rm -rf /home/dokku/&amp;lt;app&amp;gt;/cache/*
dokku config:set &amp;lt;app&amp;gt; BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-php --no-restart
dokku ps:rebuild &amp;lt;app&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/kot-behemoth/awesome-dokku&quot;&gt;awesome-dokku&lt;/a&gt; - A curated list of awesome Dokku resources and tools&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Hosting</category><category>Tools</category></item><item><title>Rails translation cheatsheet</title><link>https://larsp.de/posts/rails-translation-cheatsheet/</link><guid isPermaLink="true">https://larsp.de/posts/rails-translation-cheatsheet/</guid><description>A practical guide to translating a Rails application: locale configuration, locale files, URL routing, helpers, model translations, and the Traco gem for translatable columns.</description><pubDate>Thu, 08 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;How to translate a Rails application.&lt;/p&gt;
&lt;h2&gt;Locales&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# config/application.rb

config.i18n.available_locales = %i[de fr it]
config.i18n.default_locale = :de
config.i18n.fallbacks = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add &lt;a href=&quot;https://github.com/svenfuchs/rails-i18n&quot;&gt;rails-i18n&lt;/a&gt; for ready-made date, number, and Active Record translations:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Gemfile
gem &quot;rails-i18n&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Locale files&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# config/locales/de.yml
de:
  hello: &quot;Hallo, %{name}!&quot;
  apples:
    one: &quot;1 Apfel&quot;
    other: &quot;%{count} Äpfel&quot;
  pages:
    thankyou:
      title: &quot;Danke&quot;
      body_html: &quot;&amp;lt;strong&amp;gt;Vielen Dank&amp;lt;/strong&amp;gt; für deine Anfrage.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use lazy lookup in views and controllers to avoid repeating the full key path:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# app/views/pages/thankyou.html.erb
&amp;lt;h1&amp;gt;&amp;lt;%= t(&quot;.title&quot;) %&amp;gt;&amp;lt;/h1&amp;gt;
&amp;lt;%= t(&quot;.body_html&quot;) %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A key ending in &lt;code&gt;_html&lt;/code&gt; (or a leaf named &lt;code&gt;html&lt;/code&gt;) is marked safe and rendered without escaping.&lt;/p&gt;
&lt;h2&gt;URL routing&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# app/controllers/application_controller.rb

class ApplicationController &amp;lt; ActionController::Base
  around_action :switch_locale

  def switch_locale(&amp;amp;)
    locale = params[:locale] || I18n.default_locale
    I18n.with_locale(locale, &amp;amp;)
  end

  def default_url_options
    { locale: I18n.locale }
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# config/routes.rb

scope &quot;(:locale)&quot;, locale: /de|fr|it/ do
  get &quot;thankyou&quot;, to: &quot;pages#thankyou&quot;
  get &quot;terms&quot;, to: &quot;pages#terms&quot;

  resource :submissions, only: %i[new create]
  get &quot;submissions&quot;, to: &quot;submissions#new&quot;
  get &quot;/&quot;, to: &quot;submissions#new&quot;
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Set &lt;code&gt;lang&lt;/code&gt; on the layout so screen readers and search engines pick up the active locale:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# app/views/layouts/application.html.erb
&amp;lt;html lang=&quot;&amp;lt;%= I18n.locale %&amp;gt;&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A minimal locale switcher:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;% I18n.available_locales.each do |l| %&amp;gt;
  &amp;lt;%= link_to l.to_s.upcase, url_for(locale: l),
      class: (&quot;active&quot; if I18n.locale == l) %&amp;gt;
&amp;lt;% end %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Dates and numbers&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;%= l(Time.current, format: :short) %&amp;gt;
&amp;lt;%= number_to_currency(19.99) %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;rails-i18n&lt;/code&gt; ships the formats. Override them per locale under &lt;code&gt;date.formats&lt;/code&gt;, &lt;code&gt;time.formats&lt;/code&gt;, and &lt;code&gt;number.*&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Models and validations&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# config/locales/de.yml
de:
  activerecord:
    models:
      question:
        one: &quot;Frage&quot;
        other: &quot;Fragen&quot;
    attributes:
      question:
        question: &quot;Frage&quot;
        answer: &quot;Antwort&quot;
    errors:
      models:
        question:
          attributes:
            answer:
              blank: &quot;muss ausgefüllt werden&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Question.model_name.human&lt;/code&gt; and &lt;code&gt;Question.human_attribute_name(:answer)&lt;/code&gt; resolve these automatically, so form labels and validation messages stay translated without extra wiring.&lt;/p&gt;
&lt;h2&gt;Field translation&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/barsoom/traco&quot;&gt;Translatable columns for Ruby on Rails, stored in the model table itself&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Gemfile
gem &quot;traco&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# app/models/question.rb

class Question &amp;lt; ApplicationRecord
  translates :question, :answer
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Traco expects per-locale columns like &lt;code&gt;question_de&lt;/code&gt;, &lt;code&gt;question_fr&lt;/code&gt;, &lt;code&gt;question_it&lt;/code&gt;. For a heavier setup with a separate translations table, look at &lt;a href=&quot;https://github.com/shioyama/mobility&quot;&gt;Mobility&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Finding missing keys&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/glebm/i18n-tasks&quot;&gt;i18n-tasks&lt;/a&gt; scans the codebase for untranslated keys and unused entries:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bundle exec i18n-tasks missing
bundle exec i18n-tasks unused
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>Ruby on Rails</category></item><item><title>Syncthing</title><link>https://larsp.de/posts/syncthing/</link><guid isPermaLink="true">https://larsp.de/posts/syncthing/</guid><description>Syncthing is a free, open-source file synchronization solution that works across multiple platforms, offering a transparent alternative to cloud services like iCloud and Dropbox.</description><pubDate>Tue, 06 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The best (IMHO) multi-platform open-source file synchronization solution.&lt;/p&gt;
&lt;p&gt;As much as I wanted it to work, iCloud file synchronization used to give me headaches. I work with two Macs - one Mini in the office, and a MacBook on the go. In addition, I&apos;m using a large iPad Pro and an older, small iPad Pro for traveling. My work files, mostly Git repositories of projects I am working on and additional reference material, should be shared across all these devices. I tried to use iCloud for this, but it was always a pain to get it to work reliably. I also used Dropbox in the past, but moved away after they went the course of integrating everything and whatnot in their software. Also, it was a resource hog.&lt;/p&gt;
&lt;p&gt;The problem with iCloud is that you don&apos;t really know what&apos;s going on. It&apos;s a black box. You can&apos;t see what&apos;s happening, and you can&apos;t really control it. When the service thinks your files might have been changed on another device, it will create a copy of the file with a suffix &quot; 2&quot;. Sometimes this happens for hundreds of files in a folder, and I resorted to just deleting them with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;find . -name &quot;*\ 2&quot; -delete
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also, sometimes iCloud is not updating at all for some reason.&lt;/p&gt;
&lt;h2&gt;Enter Syncthing&lt;/h2&gt;
&lt;p&gt;Yesterday, I was fed up with this and installed &lt;a href=&quot;https://github.com/syncthing/syncthing&quot;&gt;Syncthing&lt;/a&gt; on my Macs. It&apos;s a free, open-source file synchronization software that works on all major platforms and works really smoothly. I used it in the past already, and never had a problem.&lt;/p&gt;
&lt;h3&gt;iOS&lt;/h3&gt;
&lt;p&gt;For iOS, there is a very good open source client app called &lt;a href=&quot;https://apps.apple.com/de/app/synctrain/id6553985316&quot;&gt;Synctrain&lt;/a&gt;. There&apos;s also an older third party solution (&lt;a href=&quot;https://www.mobiussync.com/&quot;&gt;MöbiusSync&lt;/a&gt;), but I&apos;d recommend Synctrain because it&apos;s much smoother.&lt;/p&gt;
&lt;p&gt;There is a &lt;a href=&quot;https://github.com/syncthing/syncthing/blob/main/GOALS.md&quot;&gt;goals document&lt;/a&gt; describing Syncthing if you&apos;re interested.&lt;/p&gt;
&lt;h3&gt;Linux install&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install syncthing
sudo systemctl enable syncthing@$USER.service
sudo systemctl start syncthing@$USER.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The web GUI is then available at &lt;a href=&quot;http://localhost:8384&quot;&gt;http://localhost:8384&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;CLI commands&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;syncthing cli config devices add --device-id ...
syncthing cli config options relays-enabled set false
find . -name &quot;*sync-conflict*&quot; -print
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Files to ignore&lt;/h3&gt;
&lt;p&gt;Place the following in a &lt;code&gt;.stignore&lt;/code&gt; file inside the synced folder to keep platform-specific cruft out of the sync:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(?d).DS_Store
Thumbs.db
desktop.ini
.Trashes
.Spotlight-V100
._*
lost+found
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>Tools</category></item><item><title>Archiving CMS websites to static files with httrack</title><link>https://larsp.de/posts/archiving-cms-websites-to-static-files-with-httrack/</link><guid isPermaLink="true">https://larsp.de/posts/archiving-cms-websites-to-static-files-with-httrack/</guid><description>Guide to using httrack to archive CMS websites like Drupal and WordPress to static HTML files when sites are no longer actively maintained or need to be retired from production.</description><pubDate>Wed, 23 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Using httrack to archive a CMS website to keep it online as a static site.&lt;/p&gt;
&lt;h2&gt;When to Archive CMS Sites&lt;/h2&gt;
&lt;p&gt;When a website built with a content management system like Drupal or WordPress is no longer updated with content or a campaign has ended, the webpages sometimes need to be archived for reference or remain online without further changes. It&apos;s not always feasible to upgrade all CMS components along the way. A major version change might require expensive custom module upgrades for a site no longer in production use. Archiving to static files is a practical solution.&lt;/p&gt;
&lt;h2&gt;Using the httrack tool to archive a website&lt;/h2&gt;
&lt;p&gt;There are several options for archiving websites (see &lt;a href=&quot;https://github.com/iipc/awesome-web-archiving&quot;&gt;Awesome Web Archiving List&lt;/a&gt;). The &lt;a href=&quot;https://www.httrack.com/&quot;&gt;httrack&lt;/a&gt; command-line tool is a preferred option. On macOS using Homebrew, install it with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew install httrack
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These are good options for mirroring:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;httrack http://SITE_TO_ARCHIVE -O DESTINATION_DIR \
  -N &quot;%h%p/%n/index%[page].%t&quot; \
  -WQ%v --robots=0 --footer &apos;&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What the flags do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-W&lt;/code&gt; mirror with wizard (asks about external links)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-Q&lt;/code&gt; no log files&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-%v&lt;/code&gt; show progress on screen&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-N &quot;%h%p/%n/index%[page].%t&quot;&lt;/code&gt; write each page as &lt;code&gt;path/to/page/index.html&lt;/code&gt; so URLs stay clean&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--robots=0&lt;/code&gt; ignore &lt;code&gt;robots.txt&lt;/code&gt; (only do this on sites you own or have permission to mirror)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--footer &apos;&apos;&lt;/code&gt; strip the httrack footer comment from each HTML file&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The tool will prompt you if external links should be followed.&lt;/p&gt;
&lt;p&gt;To avoid hammering the source server, throttle the crawl with &lt;code&gt;--max-rate=25000&lt;/code&gt; (bytes per second) or limit concurrent connections with &lt;code&gt;-c2&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;What this approach can&apos;t capture&lt;/h3&gt;
&lt;p&gt;Anything that depends on a live backend will not work in the static mirror: logged-in areas, search forms, comment submission, contact forms, and most JavaScript-driven dynamic content. Plan to either remove or replace those before going live.&lt;/p&gt;
&lt;h2&gt;Post-Processing Steps&lt;/h2&gt;
&lt;p&gt;Relative links can be rewritten afterwards (e.g., &quot;about.html&quot; to &quot;about&quot;). This is optional but useful if you want to preserve URL paths for inbound links.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;find . -name &quot;*.html&quot; -type f -print0 \
  | xargs -0 perl -i -pe &quot;s/\/index.html/\//g&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because of the &lt;code&gt;-N&lt;/code&gt; template, the homepage ends up as &lt;code&gt;index/index.html&lt;/code&gt; rather than at the root. Move it up and strip the now-incorrect &lt;code&gt;../&lt;/code&gt; prefixes from include paths and links:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cp index/index.html index.html
perl -i -pe &apos;s/\.\.\///g&apos; index.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the source site uses HTTP Basic Auth, provide username and password as part of the URL: &lt;code&gt;username:password@your.url&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Hosting Archived Sites&lt;/h2&gt;
&lt;p&gt;The resulting files can be served from inexpensive static web hosting like Netlify, Cloudflare Pages, or GitHub Pages.&lt;/p&gt;
&lt;h2&gt;Alternative: wget --mirror&lt;/h2&gt;
&lt;p&gt;For very simple sites, &lt;code&gt;wget --mirror --convert-links --adjust-extension --page-requisites --no-parent https://example.com&lt;/code&gt; can be enough and avoids the post-processing dance. httrack tends to handle messy CMS output (Drupal, WordPress) better, but &lt;code&gt;wget&lt;/code&gt; is worth trying first if the site is small.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://www.httrack.com/html/fcguide.html&quot;&gt;Httrack users guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.drupal.org/node/27882&quot;&gt;Archiving Drupal sites&lt;/a&gt; on &lt;a href=&quot;http://drupal.org&quot;&gt;drupal.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.lullabot.com/articles/sending-drupal-site-retirement-using-httrack&quot;&gt;About archiving Drupal sites&lt;/a&gt; by Karen from Lullabot&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Tools</category><category>Hosting</category></item><item><title>Barcode PDFs with Ruby on Rails</title><link>https://larsp.de/posts/barcode-pdfs-with-ruby-on-rails/</link><guid isPermaLink="true">https://larsp.de/posts/barcode-pdfs-with-ruby-on-rails/</guid><description>A guide to generating individual PDFs with imprinted EAN barcodes using Barby and Prawn libraries in Ruby on Rails projects.</description><pubDate>Sun, 20 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Using Barby and Prawn to imprint PDFs with individual barcodes&lt;/p&gt;
&lt;p&gt;For a &lt;a href=&quot;/posts/ruby-on-rails-resources/&quot;&gt;Rails&lt;/a&gt; project, I researched methods to generate individual PDFs with imprinted EAN barcodes. This guide documents a working approach using modern Ruby libraries.&lt;/p&gt;
&lt;h2&gt;Libraries Used&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Barby&lt;/strong&gt; is a barcode generator library for Ruby that generates barcodes as images or PDFs. &lt;strong&gt;Prawn&lt;/strong&gt; is a PDF generation library for Ruby that creates PDFs from scratch and annotates existing PDFs with text, images, and barcodes.&lt;/p&gt;
&lt;h2&gt;Implementation Approach&lt;/h2&gt;
&lt;p&gt;The solution uses two model classes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A voucher class with a &lt;code&gt;code&lt;/code&gt; property (printed as a barcode)&lt;/li&gt;
&lt;li&gt;A PDF generation class that subclasses &lt;code&gt;Prawn::Document&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The PDF generation class accepts a path and code as arguments, generates a PDF with the barcode imprinted, and returns the path to a temporary file that is automatically deleted when the Rails process exits.&lt;/p&gt;
&lt;h2&gt;PDF Specifications&lt;/h2&gt;
&lt;p&gt;The resulting PDF is 100mm x 133mm with a PNG background image. Initially, the &lt;code&gt;background&lt;/code&gt; option of &lt;code&gt;Prawn::Document&lt;/code&gt; was tested but didn&apos;t scale the image properly. Instead, the &lt;code&gt;image&lt;/code&gt; method with the &lt;code&gt;fit&lt;/code&gt; option scales the image to the page size.&lt;/p&gt;
&lt;p&gt;Prawn&apos;s &lt;code&gt;measurement_extensions&lt;/code&gt; allow using millimeters as a unit instead of the default 1/72 inch.&lt;/p&gt;
&lt;h2&gt;Code Example&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;require &quot;barby/barcode/code_128&quot;
require &quot;barby/outputter/prawn_outputter&quot;
require &quot;prawn/measurement_extensions&quot;

class CodePdf &amp;lt; Prawn::Document
  def initialize path, code, background
    super({
      page_size: [100.mm, 177.mm],
      margin: 0
    })

    image background, fit: [100.mm, 177.mm]
    barcode code
    render_file path
  end

  def barcode code
    barcode = Barby::Code128.new code
    barcode.annotate_pdf self, height: 18.mm, x: 17.mm, y: 3.mm
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Voucher Generation&lt;/h2&gt;
&lt;p&gt;In the code class, a temporary file is created and the PDF generation class is called. The background image is located in the &lt;code&gt;app/assets/images/{language}&lt;/code&gt; directory. The &lt;code&gt;I18n.locale&lt;/code&gt; method returns the current locale (e.g., &lt;code&gt;de&lt;/code&gt; or &lt;code&gt;fr&lt;/code&gt;) to find the correct background image:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def voucher
  # Create temporary file e.g. tmp/voucher20230805-66997-kozjms.pdf
  tmpfile = Tempfile.new &quot;voucher&quot;, Rails.root.join(&quot;tmp&quot;)
  path = tmpfile.path + &quot;.pdf&quot;
  bg_path = Rails.root.join &quot;app&quot;, &quot;assets&quot;, &quot;images&quot;, I18n.locale.to_s, price.voucher
  CodePdf.new path, code, bg_path
  path
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is an example of the resulting voucher PDF with the barcode:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/barcode-pdfs-with-ruby-on-rails/voucherpdf.png&quot; alt=&quot;Example voucher PDF with imprinted barcode&quot; /&gt;&lt;/p&gt;
</content:encoded><category>Ruby on Rails</category></item></channel></rss>