<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2026-04-22T08:53:25+00:00</updated><id>/feed.xml</id><title type="html">Goodbran</title><subtitle>Writing, publishing, and sharing notes. A personal blog about coding, music, and creativity.</subtitle><entry><title type="html">Welcome to the New Goodbran Blog</title><link href="/2026/04/21/welcome-to-the-new-goodbran-blog/" rel="alternate" type="text/html" title="Welcome to the New Goodbran Blog" /><published>2026-04-21T04:00:00+00:00</published><updated>2026-04-21T04:00:00+00:00</updated><id>/2026/04/21/welcome-to-the-new-goodbran-blog</id><content type="html" xml:base="/2026/04/21/welcome-to-the-new-goodbran-blog/"><![CDATA[<p>The Goodbran blog has been rebuilt from the ground up using <a href="https://jekyllrb.com/">Jekyll</a> — a static site generator that transforms plain text into static websites and blogs.</p>

<h2 id="why-jekyll">Why Jekyll?</h2>

<p>Moving from a Rails-based content system to Jekyll offers several advantages:</p>

<ul>
  <li><strong>Speed</strong>: Static pages load incredibly fast</li>
  <li><strong>Security</strong>: No database or server-side code means fewer attack vectors</li>
  <li><strong>Simplicity</strong>: Write in Markdown, deploy to any static host</li>
  <li><strong>Version Control</strong>: All content lives in Git</li>
</ul>

<h2 id="mozilla-protocol-design-system">Mozilla Protocol Design System</h2>

<p>The new design is inspired by <a href="https://protocol.mozilla.org/">Mozilla Protocol</a> — a design system that emphasizes:</p>

<ul>
  <li>Clean, readable typography with Zilla Slab for headings</li>
  <li>Firefox blue accents (#0060DF) for calls-to-action</li>
  <li>Card-based layouts for content organization</li>
  <li>Accessibility-first approach</li>
</ul>

<h2 id="whats-next">What’s Next</h2>

<p>Over the coming weeks, I’ll be migrating the existing blog posts from the Rails application. This includes:</p>

<ul>
  <li>Technical tutorials on Rails and deployment</li>
  <li>Music notation guides using LilyPond</li>
  <li>Piano scores and sheet music</li>
</ul>

<p>Stay tuned for more content!</p>

<hr />

<p><em>This blog is open source. Find a bug or want to suggest an improvement? <a href="https://github.com/goodbran/goodbran.github.io">Open an issue on GitHub</a>.</em></p>]]></content><author><name></name></author><summary type="html"><![CDATA[Introducing the new Jekyll-powered blog with Mozilla Protocol design system.]]></summary></entry><entry><title type="html">KidsUI: A Small UI Kit for Kids</title><link href="/2026/04/20/kidsui-a-small-ui-kit-for-kids/" rel="alternate" type="text/html" title="KidsUI: A Small UI Kit for Kids" /><published>2026-04-20T04:00:00+00:00</published><updated>2026-04-20T04:00:00+00:00</updated><id>/2026/04/20/kidsui-a-small-ui-kit-for-kids</id><content type="html" xml:base="/2026/04/20/kidsui-a-small-ui-kit-for-kids/"><![CDATA[<blockquote>
  <p><a href="https://openkids.github.io/kidsui/">KidsUI</a> is a small UI kit for kid-friendly web projects. I built it because I keep finding small reasons to make little things for my daughter, her kindergarten, and the teachers who sometimes reach out for help.</p>
</blockquote>

<p>I did not set out to make a design system. I just kept needing the same kind of parts.</p>

<p>A few little projects for my girl in kindergarten. A small page or tool when one of her teachers reaches out for help. And a few simple web apps I want to build later, partly for fun and partly for educational use.</p>

<p>So I pulled those pieces into one place and called it KidsUI.</p>

<h2 id="why-this-exists">Why this exists</h2>

<p>The goal is not completeness. The goal is a small set of components that already feel right for children: friendly, clear, a little playful, and easy to drop into tiny projects without dragging in a whole frontend stack.</p>

<h2 id="why-web-components-and-motion">Why web components and motion</h2>

<p>The stack is web components with motion. I do not want the kit bound to any one library.</p>

<p>Not React. Not Vue. Not even my usual favorite, ViewComponent.</p>

<p>If a project is plain HTML, Rails with Stimulus, or some other stack entirely, the UI pieces should still work. That portability matters more to me here than framework convenience.</p>

<p>Motion is part of the language too. Kids notice movement immediately. Used lightly, it makes an interface feel alive and welcoming instead of static and dry.</p>

<p>That is the whole idea behind KidsUI: small components, small projects, and a softer place to start building simple web apps for kids.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I built a small UI kit for kid-friendly web projects because I keep needing cheerful, simple components for my daughter, her kindergarten, and a few educational apps I want to make later.]]></summary></entry><entry><title type="html">HomeAway: A Small CLI for Switching Wi-Fi Modes</title><link href="/2026/04/12/homeaway-cli-switching-wifi-modes/" rel="alternate" type="text/html" title="HomeAway: A Small CLI for Switching Wi-Fi Modes" /><published>2026-04-12T04:00:00+00:00</published><updated>2026-04-12T04:00:00+00:00</updated><id>/2026/04/12/homeaway-cli-switching-wifi-modes</id><content type="html" xml:base="/2026/04/12/homeaway-cli-switching-wifi-modes/"><![CDATA[<blockquote>
  <p><a href="https://github.com/GoodBran/homeaway">HomeAway</a> is a tiny macOS CLI that flips my Wi-Fi between manual and DHCP mode. It exists because I got tired of digging through System Settings every time I left home.</p>
</blockquote>

<p>At home, my setup is a little strange but very effective.</p>

<p>I keep a side router around specifically for traffic that needs to get past the Great Firewall. When I am home, I point my Mac at that router with manual Wi-Fi settings and let it handle the first proxy layer. From there I can add a second VPN on macOS, like NordVPN, if I want another layer on top.</p>

<p>Outside, that setup falls apart. The side router is not with me, so manual mode is useless. I need DHCP just to get normal internet access. And without that first router-based proxy layer, NordVPN is usually not an option either. I can still use Clash or another local proxy for basic GFW bypass, but the network setup on the Mac has to change first.</p>

<p>That small switch between home and away turned into a recurring nuisance: open settings, find Wi-Fi, flip from manual to DHCP or back, recheck router and DNS values, then hope I did not leave something half-configured.</p>

<p>So I made HomeAway.</p>

<h2 id="what-it-does">What it does</h2>

<p>HomeAway is not a full network manager. It is a thin wrapper around the one thing I actually keep doing.</p>

<ul>
  <li>Detect the Wi-Fi interface</li>
  <li>Switch between manual mode and DHCP</li>
  <li>Pick an available IP when switching back to manual</li>
  <li>Optionally clear DNS when returning to DHCP</li>
</ul>

<p>Install it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gem <span class="nb">install </span>homeaway
</code></pre></div></div>

<p>Run it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>homeaway
</code></pre></div></div>

<p>On first run it asks for the router address, DNS server, and whether DHCP mode should clear DNS settings. After that, it just remembers.</p>

<h2 id="why-this-exists">Why this exists</h2>

<p>The point is not sophistication. The point is removing friction from a real routine.</p>

<p>I know exactly when I want manual mode: at home, with the side router. I know exactly when I want DHCP: everywhere else. That should be a one-command decision, not a small trip through macOS settings.</p>

<p>That is all HomeAway does. Small tool, specific job, less clicking.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[At home I use a side router and manual Wi-Fi settings. Outside I need DHCP. HomeAway turns that repetitive macOS network shuffle into one command.]]></summary></entry><entry><title type="html">Basic Math Symbols for Primary and Middle School Kids</title><link href="/2026/04/05/basic-math-symbols-for-kids/" rel="alternate" type="text/html" title="Basic Math Symbols for Primary and Middle School Kids" /><published>2026-04-05T04:00:00+00:00</published><updated>2026-04-05T04:00:00+00:00</updated><id>/2026/04/05/basic-math-symbols-for-kids</id><content type="html" xml:base="/2026/04/05/basic-math-symbols-for-kids/"><![CDATA[<p>Math symbols are the shorthand of school math. Kids do not need all of them at once. They need the right ones at the right time.</p>

<p>This is a practical list of the symbols most students meet in primary school and middle school, grouped by grade band and by category. Each symbol is clickable to learn more on Wikipedia.</p>

<p>This post was shaped by the broader symbol reference from Math Vault: <a href="https://mathvault.ca/hub/higher-math/math-symbols/">Compendium of Mathematical Symbols</a>.</p>

<hr />

<h2 id="grades-1-3">Grades 1-3</h2>

<p>At this stage, kids mostly need counting, comparison, and the four basic operations.</p>

<hr />

<h3 id="number-sentences">Number Sentences</h3>

<p><strong><a href="https://en.wikipedia.org/wiki/Equals_sign">Equal sign</a></strong> (=)</p>

<p>Means the values on both sides are the same. Three plus two equals five:</p>

<p>[3 + 2 = 5]</p>

<p><strong><a href="https://en.wikipedia.org/wiki/Less-than_sign">Less than</a></strong> (&lt;)</p>

<p>Means the first number is smaller. Four is less than seven:</p>

<p>[4 &lt; 7]</p>

<p><strong><a href="https://en.wikipedia.org/wiki/Greater-than_sign">Greater than</a></strong> (&gt;)</p>

<p>Means the first number is larger. Nine is greater than six:</p>

<p>[9 &gt; 6]</p>

<p>These three carry a lot of early math. If a student can read them comfortably, word problems start to feel less mysterious.</p>

<hr />

<h3 id="basic-operations">Basic Operations</h3>

<p><strong><a href="https://en.wikipedia.org/wiki/Plus_and_minus_signs">Plus sign</a></strong> (+)</p>

<p>Means add.</p>

<p>[2 + 5 = 7]</p>

<p><strong><a href="https://en.wikipedia.org/wiki/Plus_and_minus_signs">Minus sign</a></strong> (−)</p>

<p>Means subtract.</p>

<p>[9 - 4 = 5]</p>

<p><strong><a href="https://en.wikipedia.org/wiki/Multiplication_sign">Multiplication sign</a></strong> ($\times$)</p>

<p>Means multiply.</p>

<p>[3 \times 4 = 12]</p>

<p><strong><a href="https://en.wikipedia.org/wiki/Division_sign">Division sign</a></strong> ($\div$)</p>

<p>Means divide.</p>

<p>[12 \div 3 = 4]</p>

<p>Students also see the multiplication sign written as a dot ($\cdot$) in some books, but the times sign is the one most kids meet first.</p>

<hr />

<h3 id="place-value-and-grouping">Place Value And Grouping</h3>

<p><strong><a href="https://en.wikipedia.org/wiki/Bracket#Parentheses">Parentheses</a></strong> ( )</p>

<p>Mean do this part first.</p>

<p>[(2 + 3) \times 4 = 20]</p>

<p><strong><a href="https://en.wikipedia.org/wiki/Comma">Comma</a></strong> (,)</p>

<p>Separates numbers in a list.</p>

<p>[2, 4, 6, 8]</p>

<p><strong><a href="https://en.wikipedia.org/wiki/Decimal_separator">Decimal point</a></strong> (.)</p>

<p>Separates whole numbers from parts. Three and a half:</p>

<p>[3.5]</p>

<p>Parentheses are worth teaching early. They help kids see that math is not just left to right button pushing.</p>

<hr />

<h2 id="grades-4-5">Grades 4-5</h2>

<p>Now the work gets broader: fractions, decimals, measurement, and geometry show up more often.</p>

<hr />

<h3 id="fractions-and-division">Fractions And Division</h3>

<p><strong><a href="https://en.wikipedia.org/wiki/Fraction">Fraction</a></strong> ($\frac{1}{2}$)</p>

<p>Shows parts of a whole. One half means one part out of two equal parts:</p>

<p>[\frac{1}{2}]</p>

<p><strong><a href="https://en.wikipedia.org/wiki/Vinculum_(symbol)">Fraction bar</a></strong> or slash (/)</p>

<p>Can also mean division.</p>

<p>[6 / 2 = 3]</p>

<p>Fractions are often the moment when symbols start to feel like a new language. A little repetition helps a lot.</p>

<hr />

<h3 id="decimals-money-and-percents">Decimals, Money, And Percents</h3>

<p><strong><a href="https://en.wikipedia.org/wiki/Percent_sign">Percent sign</a></strong> ($\%$)</p>

<p>Means "out of 100." Twenty-five percent equals twenty-five hundredths:</p>

<p>[25\% = \frac{25}{100}]</p>

<p>By this point, students should start seeing that $0.5$, $\frac{1}{2}$, and $50\%$ are different ways to say the same amount.</p>

<hr />

<h3 id="geometry-symbols">Geometry Symbols</h3>

<p><strong><a href="https://en.wikipedia.org/wiki/Angle">Angle symbol</a></strong> ($\angle$)</p>

<p>Marks an angle. The angle at point $B$ formed by points $A$, $B$, and $C$:</p>

<p>[\angle ABC]</p>

<p><strong><a href="https://en.wikipedia.org/wiki/Degree_symbol">Degree symbol</a></strong> ($^{\circ}$)</p>

<p>Measures angles. Ninety degrees is a right angle:</p>

<p>[90^{\circ}]</p>

<p><strong><a href="https://en.wikipedia.org/wiki/Parallel_(geometry)">Parallel</a></strong> ($\parallel$)</p>

<p>Means lines that never meet.</p>

<p>[a \parallel b]</p>

<p><strong><a href="https://en.wikipedia.org/wiki/Perpendicular">Perpendicular</a></strong> ($\perp$)</p>

<p>Means lines that meet at a right angle.</p>

<p>[m \perp n]</p>

<p>Two especially useful geometry facts for this age:</p>

<ul>
  <li>
    <p>A <a href="https://en.wikipedia.org/wiki/Right_angle">right angle</a> is $90^{\circ}$.</p>
  </li>
  <li>
    <p>Perpendicular lines meet to make a right angle.</p>
  </li>
</ul>

<hr />

<h3 id="measurement-and-approximation">Measurement And Approximation</h3>

<p><strong><a href="https://en.wikipedia.org/wiki/Approximation">Approximation symbol</a></strong> ($\approx$)</p>

<p>Means "about equal to." Three point one four is close to pi:</p>

<p>[3.14 \approx \pi]</p>

<p>This is a nice symbol for helping kids see that some values are exact and some are close enough for the job.</p>

<hr />

<h2 id="grades-6-8">Grades 6-8</h2>

<p>Middle school math adds variables, exponents, ratios, inequalities, and basic statistics.</p>

<hr />

<h3 id="variables-and-expressions">Variables And Expressions</h3>

<p><strong><a href="https://en.wikipedia.org/wiki/Variable_(mathematics)">Variable</a></strong> ($x$, $y$, $n$)</p>

<p>Stands for an unknown number. If two $x$ plus five equals three, then $x$ equals negative one:</p>

<p>[2x + 5 = 3]</p>

<p>[x = -1]</p>

<p>This is where students learn that a letter in math is not a fancy decoration. It stands in for a number.</p>

<hr />

<h3 id="powers-and-roots">Powers And Roots</h3>

<p><strong><a href="https://en.wikipedia.org/wiki/Exponentiation">Exponent</a></strong> ($x^2$)</p>

<p>Means multiply $x$ by itself. Five squared equals twenty-five:</p>

<p>[5^2 = 25]</p>

<p><strong><a href="https://en.wikipedia.org/wiki/Cube_(algebra)">Cube</a></strong> ($x^3$)</p>

<p>Means $x$ multiplied by itself three times. Two cubed equals eight:</p>

<p>[2^3 = 8]</p>

<p><strong><a href="https://en.wikipedia.org/wiki/Square_root">Square root</a></strong> ($\sqrt{x}$)</p>

<p>Asks what number times itself gives $x$. The square root of forty-nine equals seven because seven times seven equals forty-nine:</p>

<p>[\sqrt{49} = 7]</p>

<p>[7 \times 7 = 49]</p>

<hr />

<h3 id="ratios-rates-and-proportions">Ratios, Rates, And Proportions</h3>

<p><strong><a href="https://en.wikipedia.org/wiki/Ratio">Ratio</a></strong> ($:$)</p>

<p>Compares two amounts. Two parts to three parts:</p>

<p>[2:3]</p>

<p><strong><a href="https://en.wikipedia.org/wiki/Proportionality_(mathematics)">Proportionality</a></strong> ($\propto$)</p>

<p>Means "is proportional to," though this usually shows up later in middle school or early high school.</p>

<p>For most students in this band, ratio language matters more than the fancy notation. The fraction form does most of the work.</p>

<hr />

<h3 id="inequalities">Inequalities</h3>

<p><strong><a href="https://en.wikipedia.org/wiki/Inequality_(mathematics)">Less than or equal to</a></strong> ($\le$)</p>

<p>Means the first number is smaller or the same.</p>

<p>[x \le 10]</p>

<p><strong><a href="https://en.wikipedia.org/wiki/Inequality_(mathematics)">Greater than or equal to</a></strong> ($\ge$)</p>

<p>Means the first number is larger or the same.</p>

<p>[y \ge 2]</p>

<p><strong><a href="https://en.wikipedia.org/wiki/Equals_sign#Not_equal">Not equal to</a></strong> ($\ne$)</p>

<p>Means two values are different.</p>

<p>[x \ne 4]</p>

<p>This is the point where students stop solving only one-answer equations and start describing sets of answers.</p>

<hr />

<h3 id="coordinate-plane-and-graphing">Coordinate Plane And Graphing</h3>

<p><strong><a href="https://en.wikipedia.org/wiki/Cartesian_coordinate_system">Coordinate pair</a></strong> ($(x, y)$)</p>

<p>Names a point on a grid. Three units right and four units up:</p>

<p>[(3, 4)]</p>

<p><strong><a href="https://en.wikipedia.org/wiki/Delta_(letter)">Delta</a></strong> ($\Delta$)</p>

<p>Means change. Delta $y$ means change in $y$:</p>

<p>[\Delta y]</p>

<p>Students may not love graphing at first, but these symbols keep showing up, so it is worth making them familiar.</p>

<hr />

<h3 id="probability-and-statistics">Probability And Statistics</h3>

<p><strong><a href="https://en.wikipedia.org/wiki/Mean">Mean</a></strong> ($\overline{x}$)</p>

<p>Represents the average of a set of numbers.</p>

<p><strong><a href="https://en.wikipedia.org/wiki/Probability">Probability</a></strong> ($P(A)$)</p>

<p>Means the chance of event $A$ happening. The probability of heads equals one half:</p>

<p>[P(\text{heads}) = 0.5]</p>

<p>These are not always introduced formally in every middle school class, but they are common enough to recognize.</p>

<hr />

<h2 id="the-small-set-worth-memorizing-first">The Small Set Worth Memorizing First</h2>

<p>If a student only memorizes a short list, start here:</p>

<p>Addition and subtraction: +, −, $\times$, $\div$</p>

<p>Comparisons: =, &lt;, &gt;</p>

<p>Fractions and percents: $\frac{1}{2}$, ., $\%$</p>

<p>Geometry basics: $\angle$, $^{\circ}$, $\parallel$, $\perp$</p>

<p>Algebra basics: $x$, $x^2$, $\sqrt{x}$</p>

<p>Inequalities: $\le$, $\ge$, $\ne$</p>

<p>That set covers a surprising amount of school math.</p>

<hr />

<h2 id="a-good-way-to-teach-them">A Good Way To Teach Them</h2>

<p>Do not teach symbols as a vocabulary quiz. Teach them inside real problems.</p>

<p>For example:</p>

<ul>
  <li>
    <p>Three plus four equals seven teaches addition and equality together.</p>
  </li>
  <li>
    <p>Five is less than eight teaches comparison with a concrete fact.</p>
  </li>
  <li>
    <p>One half of ten is five teaches a symbol and an idea at the same time.</p>
  </li>
  <li>
    <p>$x$ plus two equals nine teaches that a letter can hold a missing value.</p>
  </li>
</ul>

<p>Kids usually learn symbols faster when the symbol solves a problem they already care about.</p>

<hr />

<h2 id="final-thought">Final Thought</h2>

<p>Most math symbols are not hard. They are just unfamiliar.</p>

<p>Once a student learns to read &lt;, $\sqrt{x}$, or $\angle ABC$ without pausing, math gets lighter. Less decoding. More thinking.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[A simple guide to the math symbols kids see most often, grouped by grade level and category. Each symbol links to Wikipedia for deeper learning.]]></summary></entry><entry><title type="html">X API, POSSE, and the Cost of Automation</title><link href="/2026/04/05/x-api-posse-cost-automation/" rel="alternate" type="text/html" title="X API, POSSE, and the Cost of Automation" /><published>2026-04-05T04:00:00+00:00</published><updated>2026-04-05T04:00:00+00:00</updated><id>/2026/04/05/x-api-posse-cost-automation</id><content type="html" xml:base="/2026/04/05/x-api-posse-cost-automation/"><![CDATA[<p>Most newsletter setups start with "sign up for Mailchimp." My POSSE experiment started with "how hard could posting to X be?" Turns out, pretty hard – or at least, surprisingly expensive.</p>

<h2 id="posse"><a href="https://indieweb.org/POSSE#">POSSE</a></h2>

<p>POSSE stands for <strong>Publish (on your) Own Site, Syndicate Elsewhere</strong>. The idea is simple: you own the canonical copy of your content, but you push copies to wherever your readers are.</p>

<p>The appeal is obvious. Your blog is yours. The URLs are yours. But your friends aren't all going to subscribe to your RSS feed. So you syndicate. Write once, publish everywhere.</p>

<p>I wanted to build it into my Rails blog. Write a post, hit publish, and have it automatically post a summary with a link to X. Classic POSSE.</p>

<h2 id="the-x-api-today">The X API Today</h2>

<p>X's <a href="https://docs.x.com/overview">developer documentation</a> used to say "get started with a free API key." Now it says "purchase API credits."</p>

<p>The current model is <strong>pay-per-usage</strong>. You buy credits upfront. Different endpoints cost different amounts. There's a 24-hour deduplication window – request the same post twice in a day, pay once. The free tier is effectively gone for new developers.</p>

<p>For someone who posts occasionally – say, a few times a month – the economics don't work. You're buying credits upfront for a trickle of posts. The dream of automatic POSSE-to-X died quickly.</p>

<h2 id="why-i-get-it">Why I Get It</h2>

<p>AI-generated content is cheap to produce. Ungodly amounts of it can be generated for pennies. If the API were free, X would be flooded with AI posts, bot replies, and automated engagement farming.</p>

<p>Nobody wants to use a platform where everything might be AI-generated. X is essentially saying: programmatic access costs money. This filters out low-value automation. It makes spam uneconomical.</p>

<p>The pricing isn't about extracting revenue from legitimate developers. It's a defense against the AI flood. Rate limits add another layer – even with credits, you can't fire off unlimited requests. Billing blocks abuse; rate limits protect infrastructure.</p>

<h2 id="paid-api-free-app">Paid API, Free App</h2>

<p>The website and official apps are free. The API costs money. How?</p>

<p>X's apps use first-party OAuth credentials that the gateway recognizes as trusted. Third-party developers get different keys. The gateway inspects every request: first-party passes through, third-party gets metered and billed. Same API, different treatment based on the key.</p>

<p>Could you extract the app's keys and impersonate it? Technically possible, but X layers defenses: certificate pinning, code obfuscation, device attestation, behavioral detection. The goal isn't perfect security – it's making the legitimate path easier than the adversarial one.</p>

<p>This pattern is spreading. OpenAI charges for API tokens while ChatGPT the app is free. Reddit has paid API tiers. Google Maps has for years. The model is: humans through the app, free; machines through the API, metered.</p>

<h2 id="what-now">What Now</h2>

<p>I'll keep posting to X manually when I feel like it. The POSSE dream lives on for platforms with friendlier APIs – Mastodon, Bluesky, RSS, webmentions.</p>

<p>A platform that costs a little to automate is better than one that's free to drown in AI slop. Sometimes the right technical decision is the one that makes the easy thing expensive.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I wanted to auto-syndicate blog posts to X. Then I found out every API request costs money. Here's why that might actually be fine.]]></summary></entry><entry><title type="html">Moving Assets to Cloudflare R2</title><link href="/2026/04/04/moving-assets-to-cloudflare-r2/" rel="alternate" type="text/html" title="Moving Assets to Cloudflare R2" /><published>2026-04-04T04:00:00+00:00</published><updated>2026-04-04T04:00:00+00:00</updated><id>/2026/04/04/moving-assets-to-cloudflare-r2</id><content type="html" xml:base="/2026/04/04/moving-assets-to-cloudflare-r2/"><![CDATA[<p>Binary files in Git are a trap. They bloat your repo, slow clones, and create false promises of version control. When you change one pixel in a 5MB image, Git stores another 5MB. Do this enough and your repository becomes a burden.</p>

<p>My sheet music project was heading there. Each piano score generated five files: SVG, PDF, MIDI, MP3, and the LilyPond source. Plus a bird guessing game with 13 bird photos and 8 audio clips. 32 files, 33MB, all checked into Git.</p>

<p>Cloudflare R2 fixes this. Zero egress fees, S3-compatible API, cheap storage. Here's the setup.</p>

<h2 id="the-architecture">The architecture</h2>

<table>
  <thead>
    <tr>
      <th>Environment</th>
      <th>Files served from</th>
      <th>URL base</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Development</td>
      <td>
<code>public/music/</code> and <code>public/projects/</code>
</td>
      <td><code>/music</code></td>
    </tr>
    <tr>
      <td>Production</td>
      <td>R2 bucket</td>
      <td><code>https://cdn.goodbran.com/music</code></td>
    </tr>
  </tbody>
</table>

<p>Same code, different sources. Rails environment determines where files come from.</p>

<h2 id="uploading-with-rclone">Uploading with rclone</h2>

<p>R2 uses S3-compatible API. rclone speaks that natively.</p>

<p>Configure once:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rclone config
<span class="c"># Choose "Amazon S3", enter your R2 access key and secret</span>
<span class="c"># Endpoint: https://&lt;account_id&gt;.r2.cloudflarestorage.com</span>
</code></pre></div></div>

<p>Then sync:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rclone <span class="nb">sync </span>public/music/ r2:goodbran/music/
rclone <span class="nb">sync </span>public/projects/ r2:goodbran/projects/
</code></pre></div></div>

<p>I added rake tasks for convenience:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails assets:sync_music      <span class="c"># Sync sheet music</span>
rails assets:sync_projects   <span class="c"># Sync project files</span>
rails assets:sync_all        <span class="c"># Both</span>
</code></pre></div></div>

<h2 id="environment-aware-urls">Environment-aware URLs</h2>

<p>The <code>Content::Sheet</code> model generates asset URLs. In production, they point to the CDN. In development, they point to local files.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Content::Sheet</span> <span class="o">&lt;</span> <span class="no">Perron</span><span class="o">::</span><span class="no">Resource</span>
  <span class="no">CDN_BASE</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">env</span><span class="p">.</span><span class="nf">production?</span> <span class="p">?</span> 
    <span class="s2">"https://cdn.goodbran.com/music"</span> <span class="p">:</span> <span class="s2">"/music"</span>

  <span class="k">def</span> <span class="nf">pdf_url</span>
    <span class="s2">"</span><span class="si">#{</span><span class="no">CDN_BASE</span><span class="si">}</span><span class="s2">/</span><span class="si">#{</span><span class="n">pdf_filename</span><span class="si">}</span><span class="s2">"</span> <span class="k">if</span> <span class="n">pdf_filename</span><span class="p">.</span><span class="nf">present?</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>No configuration files, no environment variables. The code knows where it lives.</p>

<h2 id="removing-from-git">Removing from Git</h2>

<p>After syncing to R2, I removed the assets from Git tracking:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Update .gitignore</span>
<span class="nb">echo</span> <span class="s2">"/public/music/*"</span> <span class="o">&gt;&gt;</span> .gitignore
<span class="nb">echo</span> <span class="s2">"/public/projects/*"</span> <span class="o">&gt;&gt;</span> .gitignore

<span class="c"># Remove from tracking but keep local files</span>
git <span class="nb">rm</span> <span class="nt">-r</span> <span class="nt">--cached</span> public/music/
git <span class="nb">rm</span> <span class="nt">-r</span> <span class="nt">--cached</span> public/projects/
</code></pre></div></div>

<p>Local files stay for development. Fresh clones get an empty <code>public/music/</code> directory. Run <code>rails lilypond:compile</code> or <code>rclone sync</code> to populate it.</p>

<h2 id="the-workflow-now">The workflow now</h2>

<p><strong>Adding new sheet music:</strong></p>

<ol>
  <li>Create <code>.ly</code> file in <code>app/content/lilypond/</code>
</li>
  <li>Run <code>rails lilypond:compile[new-piece]</code>
</li>
  <li>Files appear in <code>public/music/</code> — works immediately in dev</li>
  <li>Run <code>rails assets:sync_music</code> — now on CDN for production</li>
</ol>

<p><strong>The bird game works the same way.</strong> The <code>ProjectsController</code> builds URLs based on environment, feeding JSON to a Stimulus controller that plays sounds and shows images.</p>

<h2 id="cost-and-limits">Cost and limits</h2>

<p>R2's free tier: 10GB storage, 10 million reads per month. My entire asset collection is under 35MB. It costs nothing.</p>

<p>Even if it grows to 1GB, that's $0.015/month for storage. Egress is free. The only risk is going over 10 million monthly reads, which would cost $0.36 per million after that. For a personal site, unreachable.</p>

<h2 id="summary">Summary</h2>

<ul>
  <li>33MB removed from Git repository</li>
  <li>Deploys are faster (fewer files to copy)</li>
  <li>CDN serves assets globally</li>
  <li>Local development still works offline</li>
  <li>Cost: $0</li>
</ul>

<p>The repo contains source files. The CDN serves generated files. Separation of concerns, finally.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Stop committing MP3s and PDFs to Git. Serve them from R2 instead—zero egress fees, local development still works, and your repo shrinks by 30MB.]]></summary></entry><entry><title type="html">Discovering the Mutopia Project</title><link href="/2026/04/03/discovering-the-mutopia-project/" rel="alternate" type="text/html" title="Discovering the Mutopia Project" /><published>2026-04-03T04:00:00+00:00</published><updated>2026-04-03T04:00:00+00:00</updated><id>/2026/04/03/discovering-the-mutopia-project</id><content type="html" xml:base="/2026/04/03/discovering-the-mutopia-project/"><![CDATA[<p>I stumbled onto the <a href="https://www.mutopiaproject.org/">Mutopia Project</a> while looking for trustworthy sheet music sources. Their entire collection is open source on <a href="https://github.com/MutopiaProject/MutopiaProject">GitHub</a>.</p>

<p>It is exactly what I needed: a volunteer-run archive of public domain scores, all typeset in LilyPond and freely licensed. No sketchy PDFs from questionable corners of the internet. Just clean, engrave-ready source files.</p>

<h2 id="why-it-matters-for-learning">Why it matters for learning</h2>

<p>When you are learning piano, finding good beginner material is harder than it should be. Most free sheet music online is either:</p>

<ul>
  <li>Low-quality scans with blurry notation</li>
  <li>Copyrighted material in a legal gray zone</li>
  <li>Watermarked previews trying to sell you something</li>
</ul>

<p>The Mutopia Project is none of that. Every piece is verified public domain, professionally typeset, and available as a <code>.ly</code> source file.</p>

<h2 id="what-i-found">What I found</h2>

<p>The first piece I grabbed was the <strong>Minuet in G Major (BWV Anh. 114)</strong> from the Anna Magdalena Bach notebook. It is the perfect beginner piece: simple hand positions, clear melody, just enough ornamentation to teach mordents.</p>

<p>The source file compiled cleanly. One <code>rake</code> task later I had PDF, SVG, MIDI, and audio rendered with my Grand Piano soundfont.</p>

<h2 id="the-workflow">The workflow</h2>

<p>The beautiful part is that Mutopia scores integrate directly into a code-based pipeline:</p>

<ol>
  <li>Download the <code>.ly</code> file</li>
  <li>Run the LilyPond compiler</li>
  <li>Get web-ready assets: SVG for display, PDF for printing, audio for playback</li>
</ol>

<p>Because the source is plain text, you can version it, diff it, and review changes like any other code file.</p>

<h2 id="whats-available">What's available</h2>

<p>The collection spans centuries: Bach minuets, Burgmüller etudes, Clementi sonatinas, Beethoven dances. Most of the standard beginner repertoire is there, waiting to be compiled.</p>

<p>For anyone building a piano learning tool or just collecting repertoire, Mutopia is a goldmine. Free, legal, and built for the same source-code mindset that makes LilyPond appealing in the first place.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[A free library of public domain sheet music that fits perfectly into a code-first workflow.]]></summary></entry><entry><title type="html">From LilyPond to Playable Piano</title><link href="/2026/03/31/from-lilypond-to-playable-piano/" rel="alternate" type="text/html" title="From LilyPond to Playable Piano" /><published>2026-03-31T04:00:00+00:00</published><updated>2026-03-31T04:00:00+00:00</updated><id>/2026/03/31/from-lilypond-to-playable-piano</id><content type="html" xml:base="/2026/03/31/from-lilypond-to-playable-piano/"><![CDATA[<p>The score pipeline used to stop at display. Today, it goes all the way to sound. A single LilyPond file now compiles into sheet music (SVG, PDF), raw performance data (MIDI), and playable web audio (MP3, OGG) you can listen to right in the browser.</p>

<h2 id="expanding-the-output">Expanding the output</h2>

<p>The flow remains simple: write LilyPond, compile it, publish the results. But the output is much wider. The build task now emits five artifacts for each piece:</p>

<ul>
  <li>
<code>SVG</code> for inline display on the page</li>
  <li>
<code>PDF</code> for printing</li>
  <li>
<code>MIDI</code> for raw performance instructions</li>
  <li>
<code>MP3</code> for broad browser support</li>
  <li>
<code>OGG</code> for an open audio format</li>
</ul>

<p>A score page is no longer just something to look at. It is something to read, download, and hear.</p>

<h2 id="finding-the-sound">Finding the sound</h2>

<p>LilyPond can write MIDI, but MIDI is not audio. It is a set of instructions: play this note, at this time, with this instrument.</p>

<p>To turn that into sound, the build now pipes the MIDI through <code>fluidsynth</code> with a piano SoundFont, then hands the result to <code>ffmpeg</code> for <code>MP3</code> and <code>OGG</code> encoding.</p>

<p>That small change shifts everything. The pipeline feels complete. Text in, sheet music out, audio out.</p>

<h2 id="piano-first">Piano first</h2>

<p>I do not need a hundred synthetic instruments. I just need one decent piano.</p>

<p>Our setup leans hard into that constraint. The score asks for <code>acoustic grand</code>, the local build uses a dedicated piano SoundFont, and the audio path is tuned for that single instrument instead of general MIDI convenience.</p>

<p>It is still a simple system. But it is simple in the right direction.</p>

<h2 id="closing-the-loop">Closing the loop</h2>

<p>The score page keeps the engraved notation, adds fast download links for the new files, and includes a plain native browser audio player.</p>

<p>That is the part I appreciate most. The same text source file now serves three jobs simultaneously:</p>

<ol>
  <li>Readable source in the repository</li>
  <li>Engraved sheet music on the page</li>
  <li>Playable piano audio in the browser</li>
</ol>

<p>It is not a massive feature on paper. But it completes the experience. Write the music. Compile it. See it. Hear it.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[We now compile LilyPond into sheet music, MIDI, MP3, and OGG, then play it right on the score page.]]></summary></entry><entry><title type="html">Adding Newsletter Subscriptions</title><link href="/2026/03/30/adding-newsletter-subscriptions/" rel="alternate" type="text/html" title="Adding Newsletter Subscriptions" /><published>2026-03-30T04:00:00+00:00</published><updated>2026-03-30T04:00:00+00:00</updated><id>/2026/03/30/adding-newsletter-subscriptions</id><content type="html" xml:base="/2026/03/30/adding-newsletter-subscriptions/"><![CDATA[<p>Most newsletter setups start with "sign up for Mailchimp." This one starts with <code>rails g model</code>. If you already run a Rails app, everything you need for a proper double opt-in newsletter is already in the box. Here's how ours works.</p>

<h2 id="the-subscriber-lifecycle">The subscriber lifecycle</h2>

<p>A subscriber goes through three states: pending, confirmed, and unsubscribed. One table tracks all three with two timestamp columns:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">create_table</span> <span class="ss">:subscribers</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span>
  <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span>
  <span class="n">t</span><span class="p">.</span><span class="nf">datetime</span> <span class="ss">:confirmed_at</span>
  <span class="n">t</span><span class="p">.</span><span class="nf">datetime</span> <span class="ss">:unsubscribed_at</span>
  <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:source</span>
  <span class="n">t</span><span class="p">.</span><span class="nf">timestamps</span>
<span class="k">end</span>

<span class="n">add_index</span> <span class="ss">:subscribers</span><span class="p">,</span> <span class="s2">"lower(email)"</span><span class="p">,</span> <span class="ss">unique: </span><span class="kp">true</span>
</code></pre></div></div>

<p>No <code>status</code> enum, no state machine gem. A <code>nil</code> <code>confirmed_at</code> means pending. A present <code>confirmed_at</code> with a <code>nil</code> <code>unsubscribed_at</code> means active. Both present means they left. The scopes write themselves:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">scope</span> <span class="ss">:pending</span><span class="p">,</span>   <span class="o">-&gt;</span> <span class="p">{</span> <span class="n">where</span><span class="p">(</span><span class="ss">confirmed_at: </span><span class="kp">nil</span><span class="p">,</span> <span class="ss">unsubscribed_at: </span><span class="kp">nil</span><span class="p">)</span> <span class="p">}</span>
<span class="n">scope</span> <span class="ss">:confirmed</span><span class="p">,</span> <span class="o">-&gt;</span> <span class="p">{</span> <span class="n">where</span><span class="p">.</span><span class="nf">not</span><span class="p">(</span><span class="ss">confirmed_at: </span><span class="kp">nil</span><span class="p">)</span> <span class="p">}</span>
<span class="n">scope</span> <span class="ss">:active</span><span class="p">,</span>    <span class="o">-&gt;</span> <span class="p">{</span> <span class="n">confirmed</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">unsubscribed_at: </span><span class="kp">nil</span><span class="p">)</span> <span class="p">}</span>
</code></pre></div></div>

<p>The functional <code>lower(email)</code> index catches duplicates regardless of casing. Combined with <code>create_or_find_by!</code>, re-subscribing the same address is a no-op instead of an error.</p>

<h2 id="double-opt-in-with-signed-ids">Double opt-in with signed IDs</h2>

<p>The confirmation flow uses Rails' built-in signed IDs — no token column, no custom crypto:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Generating the confirmation link</span>
<span class="n">confirm_newsletter_url</span><span class="p">(</span>
  <span class="ss">token: </span><span class="n">subscriber</span><span class="p">.</span><span class="nf">signed_id</span><span class="p">(</span><span class="ss">purpose: :newsletter_confirm</span><span class="p">,</span> <span class="ss">expires_in: </span><span class="mi">7</span><span class="p">.</span><span class="nf">days</span><span class="p">)</span>
<span class="p">)</span>

<span class="c1"># Verifying it</span>
<span class="n">subscriber</span> <span class="o">=</span> <span class="no">Subscriber</span><span class="p">.</span><span class="nf">find_signed!</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:token</span><span class="p">],</span> <span class="ss">purpose: :newsletter_confirm</span><span class="p">)</span>
<span class="n">subscriber</span><span class="p">.</span><span class="nf">mark_confirmed!</span>
</code></pre></div></div>

<p><code>signed_id</code> encodes the record's ID with a purpose and expiry into a tamper-proof token. Rails handles the signing, verification, and expiration. The token lives in the URL, so there's nothing to store.</p>

<p>The same pattern works for unsubscribe links, just with a different purpose:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Subscriber</span><span class="p">.</span><span class="nf">find_signed!</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:token</span><span class="p">],</span> <span class="ss">purpose: :newsletter_unsubscribe</span><span class="p">)</span>
</code></pre></div></div>

<p>One mechanism, two use cases.</p>

<h2 id="sending-newsletters">Sending newsletters</h2>

<p>Newsletters are campaigns. Each one is a database row with a subject, an HTML body, and a plain-text body. A rake task ties everything together:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bin/rails newsletter:send <span class="nv">SUBJECT</span><span class="o">=</span><span class="s2">"Monthly note"</span> <span class="nv">MARKDOWN_FILE</span><span class="o">=</span><span class="s2">"path/to/issue.md"</span>
</code></pre></div></div>

<p>The task reads a Markdown file, converts it to HTML with Kramdown, strips tags for the text version, and enqueues a delivery job for every active subscriber:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">campaign</span> <span class="o">=</span> <span class="no">NewsletterCampaign</span><span class="p">.</span><span class="nf">create_from_markdown_file!</span><span class="p">(</span><span class="n">subject</span><span class="p">:,</span> <span class="n">file_path</span><span class="p">:)</span>

<span class="no">Subscriber</span><span class="p">.</span><span class="nf">active</span><span class="p">.</span><span class="nf">find_each</span> <span class="k">do</span> <span class="o">|</span><span class="n">subscriber</span><span class="o">|</span>
  <span class="no">NewsletterMailer</span><span class="p">.</span><span class="nf">issue</span><span class="p">(</span><span class="n">subscriber</span><span class="p">,</span> <span class="n">campaign</span><span class="p">).</span><span class="nf">deliver_later</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Each email gets its own unsubscribe link. The <code>List-Unsubscribe</code> and <code>List-Unsubscribe-Post</code> headers tell email clients to show an unsubscribe button natively — Gmail, Apple Mail, and others will render it without you building any UI on their end.</p>

<h2 id="why-not-a-third-party-service">Why not a third-party service?</h2>

<p>For a personal blog with a modest list, the answer is simplicity and cost. The goal is to minimize the budget for this blog system as much as possible. No API keys to rotate, no webhook endpoints to maintain, and no monthly bill that scales with your subscriber count. The entire subscription system is about 150 lines of application code. Rails' <code>deliver_later</code> handles background delivery, and Amazon SES (Simple Email Service) handles the actual sending for pennies.</p>

<p>If the list grows to thousands and you need analytics, A/B testing, or deliverability optimization — sure, migrate to a dedicated service then. But starting with your own gives you something no SaaS does: complete understanding of every line in the stack, and a monthly bill that rounds to zero.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[A double opt-in newsletter for a Rails blog. Two models, two mailers, one rake task, and no third-party service.]]></summary></entry><entry><title type="html">Music Notation as Source Code</title><link href="/2026/03/28/music-notation-as-source-code/" rel="alternate" type="text/html" title="Music Notation as Source Code" /><published>2026-03-28T04:00:00+00:00</published><updated>2026-03-28T04:00:00+00:00</updated><id>/2026/03/28/music-notation-as-source-code</id><content type="html" xml:base="/2026/03/28/music-notation-as-source-code/"><![CDATA[<p>I am very new to music, so what struck me about LilyPond was how much it feels like code.</p>

<p><a href="https://lilypond.org/">LilyPond</a> takes a plain text file and produces engraved sheet music.</p>

<h2 id="the-text-file-is-the-score">The text file is the score</h2>

<p>Instead of dragging notes around a canvas, you describe the music directly:</p>

<pre><code class="language-lilypond">\relative c' {
  \key c \major
  \time 4/4
  c4 e g e |
  d4 f a f |
}
</code></pre>

<p>Even without knowing much music, I can read the shape of this. Key, time signature, notes, rhythm – all as text.</p>

<p>If you spend the day in a code editor, this feels familiar. You write a file, compile it, and get an artifact. It just happens to be music.</p>

<h2 id="why-it-fits">Why it fits</h2>

<p>What makes LilyPond interesting is that the source stays readable and diffable. You can review changes, track revisions, and keep the <code>.ly</code> file in the same repository as the rest of your project.</p>

<p>The output matters too. LilyPond gives you professional-looking results without nudging everything by hand.</p>

<h2 id="where-it-went">Where it went</h2>

<p>I started with a small experiment and quickly liked the workflow. A rake task compiles <code>.ly</code> files into SVG and PDF, so it fits naturally into a codebase.</p>

<p>Text file in, engraved score out, automated build. Exactly the kind of workflow I want.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[LilyPond turns plain text into engraved sheet music, and that makes it feel like code.]]></summary></entry></feed>