<rss version="2.0">
  <channel>
    <title>Willow-Camp on enumerator.dev</title>
    <link>https://enumerator.dev/t/willow-camp/</link>
    <description></description>
    
    <language>en-CA</language>
    
    <lastBuildDate>Tue, 07 Oct 2025 11:25:05 +0000</lastBuildDate>
    
    <item>
      <title>willow.camp Updates - October 7, 2025</title>
      <link>https://enumerator.dev/willow-camp-updates-october-7-2025/</link>
      <pubDate>Tue, 07 Oct 2025 11:25:05 +0000</pubDate>
      <guid>https://enumerator.dev/willow-camp-updates-october-7-2025/</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve spent most of my willow.camp work on finding a new backend language. But I&amp;rsquo;ve still updated the Rails app! Here&amp;rsquo;s what&amp;rsquo;s changed since my last update.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Moved HTML storage out of the &lt;code&gt;post&lt;/code&gt; table and into &lt;code&gt;ActionText&lt;/code&gt;. It was this deep dive that made me question using Rails. I went down a long rabbit hole, finding a good text editor and then realized this work felt more like building Ikea furniture than I wanted. I&amp;rsquo;m more interested in having access to the application end-to-end than plugging parts together. Anyway, in the meantime, your posts are now stored in an ActionText table.&lt;/li&gt;
&lt;li&gt;Prevented email enumeration attacks by returning an ambiguous error on sign-up. This is my pet peeve with Devise, and I am a bit embarrassed I let this live in production for so long. Previously, the sign-up form would indicate if an email address had already been taken. Since I can&amp;rsquo;t guarantee people use unique passwords, this means an attacker could use a leaked email/password list to access accounts that use poor password hygiene. No more! I&amp;rsquo;ve done my best to give ambiguous, helpful messages when something is wrong on the signup page.&lt;/li&gt;
&lt;li&gt;Updated dependencies! A whole bunch of Ruby Gems and npm packages got minor version bumps.&lt;/li&gt;
&lt;li&gt;Fixed CI errors with vips. This is minor but tests were failing in CI because vips wasn&amp;rsquo;t installed on the machine.&lt;/li&gt;
&lt;li&gt;Cleaned up the Hanami directory. There was a day when I was convinced Hanami was the next framework for willow.camp, and I started a migration in its own directory.&lt;/li&gt;
&lt;li&gt;Cleaned up image things I was trying out with images and media handling.&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>willow.camp: Next</title>
      <link>https://enumerator.dev/willow-camp-next/</link>
      <pubDate>Tue, 07 Oct 2025 01:26:24 +0000</pubDate>
      <guid>https://enumerator.dev/willow-camp-next/</guid>
      <description>&lt;p&gt;I have spent the past few weeks figuring out the future of willow.camp.&lt;/p&gt;
&lt;p&gt;It turns out I love making it, and I love that a handful of people are using it.&lt;/p&gt;
&lt;p&gt;I built willow.camp in Rails because I could prototype quickly without having to think about things like auth, logging, or building forms. Rails was great for that.&lt;/p&gt;
&lt;p&gt;Now that I have a version of willow.camp I like, I want to be more hands-on with what I&amp;rsquo;m building, and I find that I&amp;rsquo;ve spent more time working around the framework and various libraries than I have creating the things I want.&lt;/p&gt;
&lt;p&gt;Learning other people&amp;rsquo;s code isn&amp;rsquo;t particularly enjoyable.&lt;/p&gt;
&lt;p&gt;Aside: Rails and Ruby have also had a public meltdown lately, and I&amp;rsquo;m tired of feeling like a puny developer watching giants fight and say evil things.&lt;/p&gt;
&lt;h2 id=&#34;finding-the-next-tech-stack&#34;&gt;Finding the Next Tech Stack&lt;/h2&gt;
&lt;p&gt;This was a long journey.&lt;/p&gt;
&lt;p&gt;I started with &lt;a href=&#34;https://hanamirb.org/&#34;&gt;Hanami&lt;/a&gt;. It is still Ruby, and the community is much friendlier. Hanami didn&amp;rsquo;t work for me. It felt like I kept bumping up against the framework, telling me I was doing it wrong. The community is friendly, but the framework is very rigid about &amp;ldquo;the right way&amp;rdquo; of doing things.&lt;/p&gt;
&lt;p&gt;Then I hopped over to &lt;a href=&#34;https://crystal-lang.org/&#34;&gt;Crystal&lt;/a&gt; and tried &lt;a href=&#34;https://luckyframework.org/&#34;&gt;Lucky&lt;/a&gt;, &lt;a href=&#34;https://amberframework.org/&#34;&gt;Amber&lt;/a&gt;, and &lt;a href=&#34;https://martenframework.com/&#34;&gt;Marten&lt;/a&gt;. They were all okay to work with, but didn&amp;rsquo;t strike my fancy.&lt;/p&gt;
&lt;p&gt;Lucky was the easiest to get going. I had the data models for willow.camp up and running with a couple of rough-looking views in a few hours.&lt;/p&gt;
&lt;p&gt;Marten&amp;rsquo;s &amp;ldquo;apps&amp;rdquo; are nice to work with (think slices in Hanami). But the framework doesn&amp;rsquo;t natively support subdomain routing, and I didn&amp;rsquo;t want to fight that design.&lt;/p&gt;
&lt;p&gt;Crystal is also very slow to compile. A straightforward Lucky app took over 30 seconds on my M1 to compile! Who has that kind of time when you need to compile multiple times an hour?&lt;/p&gt;
&lt;p&gt;Finally, the community. The Lucky devs are really active, which is nice. But Crystal itself seems to have a fragmented community. When I interacted with people, I didn&amp;rsquo;t really see someone like me in any of the channels.&lt;/p&gt;
&lt;h3 id=&#34;what-i-want-in-a-language-and-framework&#34;&gt;What I Want in A Language and Framework&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;An active, welcoming community that builds trust.&lt;/li&gt;
&lt;li&gt;Helpful error messages from the language.&lt;/li&gt;
&lt;li&gt;Good documentation.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That&amp;rsquo;s about it. I don&amp;rsquo;t need huge framework features. I need a language with a helpful design and friendly people.&lt;/p&gt;
&lt;h2 id=&#34;the-next-willowcamp-tech-stack&#34;&gt;The Next willow.camp Tech Stack&lt;/h2&gt;
&lt;p&gt;Once I realized that I valued community and friendly docs more than I valued framework features, I found &lt;a href=&#34;https://gleam.run&#34;&gt;Gleam&lt;/a&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Backend:
&lt;ul&gt;
&lt;li&gt;Gleam with the &lt;a href=&#34;https://github.com/gleam-wisp/wisp&#34;&gt;Wisp Framework&lt;/a&gt; on a Postgres database.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Frontend:
&lt;ul&gt;
&lt;li&gt;So far, I plan to use &lt;a href=&#34;https://hexdocs.pm/nakai/index.html&#34;&gt;Nakai&lt;/a&gt; for server-side rendered HTML and HTMX or a similar library for interactive components.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Oh, and I&amp;rsquo;ve already written a Gleam package! willow.camp needs an accurate domain name parser, so I wrote &lt;a href=&#34;https://github.com/cassiascheffer/psl&#34;&gt;psl&lt;/a&gt; to parse domains using the &lt;a href=&#34;https://publicsuffix.org/&#34;&gt;Public Suffix List&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;the-next-willowcamp-features&#34;&gt;The Next willow.camp Features&lt;/h2&gt;
&lt;p&gt;I have a long list of features I want to build for willow.camp, and it&amp;rsquo;s these features that inspired me to change the language and framework.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A WYSYIG editor that supports markdown. This is the best of both worlds and opens willow.camp up to non-technical writers.&lt;/li&gt;
&lt;li&gt;An image library via &lt;a href=&#34;https://openverse.org/&#34;&gt;openverse&lt;/a&gt; with an image editor UI.&lt;/li&gt;
&lt;li&gt;Email newsletters! I&amp;rsquo;m excited about this one. You will be able to collect a list of subscribers and send blog posts to your distribution list.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://atproto.com/&#34;&gt;ATProto&lt;/a&gt; and &lt;a href=&#34;https://www.w3.org/TR/activitypub/&#34;&gt;ActivityPub&lt;/a&gt; support.&lt;/li&gt;
&lt;li&gt;An RSS reader so you can follow blogs without a social profile.&lt;/li&gt;
&lt;li&gt;Private bookmarks with notes and highlights.&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>willow.camp Updates September 21, 2025</title>
      <link>https://enumerator.dev/willow-camp-updates-september-21-2025/</link>
      <pubDate>Sun, 21 Sep 2025 12:58:00 +0000</pubDate>
      <guid>https://enumerator.dev/willow-camp-updates-september-21-2025/</guid>
      <description>&lt;p&gt;Quiet on the updates this week. I could rename &amp;ldquo;Major Features&amp;rdquo; to &amp;ldquo;Major Worries&amp;rdquo; because I&amp;rsquo;ve spent more time worrying about how images should work than testing them with real users!&lt;/p&gt;
&lt;p&gt;I enabled image uploads on the &lt;a href=&#34;github.com/avo-hq/marksmith&#34;&gt;marksmith&lt;/a&gt; text editor this week, and a few people tested it out for me. Turns out marksmith doesn&amp;rsquo;t work how I thought it would with images. Image uploads are still enabled, but I&amp;rsquo;m going back to the drawing board with the editor UI and media management.&lt;/p&gt;
&lt;h2 id=&#34;major-features&#34;&gt;Major Features&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Image storage and processing functionality with Active Storage integration, direct uploads, and automatic image processing&lt;/li&gt;
&lt;li&gt;I heard you like willow.camp. How about deleting willow.camp? You can now delete your blog and account if you&amp;rsquo;d like. This will delete all your data and is irreversible. The option is there if you need it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;improvements&#34;&gt;Improvements&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Enhanced navigation to only show blog title or subdomain in menu&lt;/li&gt;
&lt;li&gt;Updated testing configuration for quieter pre-commit output&lt;/li&gt;
&lt;li&gt;Removed social_share_image functionality&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;security--dependencies&#34;&gt;Security &amp;amp; Dependencies&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Updated JavaScript dependencies&lt;/li&gt;
&lt;li&gt;Updated Ruby gem dependencies&lt;/li&gt;
&lt;li&gt;Added new license file&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>How Much does it Cost to Run willow.camp?</title>
      <link>https://enumerator.dev/how-much-does-it-cost-to-run-willow-camp/</link>
      <pubDate>Sun, 21 Sep 2025 12:21:00 +0000</pubDate>
      <guid>https://enumerator.dev/how-much-does-it-cost-to-run-willow-camp/</guid>
      <description>&lt;p&gt;&lt;img
    src=&#34;https://enumerator.dev/images/how-much-does-it-cost-to-run-willow-camp_hu_ba1bc2c0460fdb3e.webp&#34;
    srcset=&#34;https://enumerator.dev/images/how-much-does-it-cost-to-run-willow-camp_hu_5330240592c623c1.webp 576w, https://enumerator.dev/images/how-much-does-it-cost-to-run-willow-camp_hu_8d21ea327229791c.webp 864w, https://enumerator.dev/images/how-much-does-it-cost-to-run-willow-camp_hu_ba1bc2c0460fdb3e.webp 1152w&#34; sizes=&#34;(max-width: 36rem) 100vw, 36rem&#34;
    width=&#34;1152&#34;
    height=&#34;767&#34;
    loading=&#34;lazy&#34;
    decoding=&#34;async&#34; alt=&#34;Photo by StellrWeb on Unsplash&#34;&gt;&lt;/p&gt;
&lt;p&gt;I recently enabled image uploads in the marksmith text editor on willow.camp. I&amp;rsquo;ve been hesitant to add media management to willow.camp for two reasons: it is hard to get the user experience right, media management adds extra cost.&lt;/p&gt;
&lt;p&gt;I am being cost-conscious on willow.camp because it doesn&amp;rsquo;t make any money. This is a passion project, and I&amp;rsquo;m having lots of fun doing it. But the more fun I have, the more it might cost.&lt;/p&gt;
&lt;p&gt;It turns out adding storage is &lt;em&gt;way cheaper&lt;/em&gt; than I thought. I would have to have a whole lot of media and a few really busy willow.camp sites to start getting charged more for data transfer fees.&lt;/p&gt;
&lt;p&gt;I will eventually build a pricing plan for willow.camp to cover costs for active sites. In the meantime, if you want to support willow.camp, you can send me a tip on &lt;a href=&#34;https://ko-fi.com/cassiascheffer&#34;&gt;https://ko-fi.com/cassiascheffer&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;claude-code&#34;&gt;Claude Code&lt;/h2&gt;
&lt;p&gt;I include Claude Code in here because it is part of my development process. I&amp;rsquo;m on the Max plan, and I use it enough that it pays off.&lt;/p&gt;
&lt;p&gt;In the past 30 days, I&amp;rsquo;ve used CAD $188.49 of tokens, and I even took a week off in the woods.&lt;/p&gt;
&lt;p&gt;I use Claude Code to scaffold ideas, experiment with features, and improve test coverage. I&amp;rsquo;ve been able to build willow.camp so quickly because Claude Code is pretty good at scaffolding a blog in Rails.&lt;/p&gt;
&lt;h2 id=&#34;august-breakdown&#34;&gt;August Breakdown&lt;/h2&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;service&lt;/th&gt;
          &lt;th&gt;fee (CAD)&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;DigitalOcean Droplet (1 vcpu, 1 gb memory)&lt;/td&gt;
          &lt;td&gt;$8.25&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;DigitalOcean Postgres ( 1gb ram, 10 gb disk)&lt;/td&gt;
          &lt;td&gt;$20.82&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Hatchbox.io Deployments&lt;/td&gt;
          &lt;td&gt;$14.13&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Claude Code Max&lt;/td&gt;
          &lt;td&gt;$158.2&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Honeybadger Error Tracking&lt;/td&gt;
          &lt;td&gt;$0&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Total&lt;/td&gt;
          &lt;td&gt;$201.40&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Total without Claude Code&lt;/td&gt;
          &lt;td&gt;$43.20&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;september-estimate&#34;&gt;September Estimate&lt;/h2&gt;
&lt;p&gt;Here is my estimate for September based on today&amp;rsquo;s exchange rate.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;service&lt;/th&gt;
          &lt;th&gt;fee (CAD)&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;DigitalOcean Droplet (1 vCPU, 1 GB memory)&lt;/td&gt;
          &lt;td&gt;$8.26&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;DigitalOcean Postgres ( 1 GB RAM, 10 GB disk)&lt;/td&gt;
          &lt;td&gt;$20.87&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;DigitalOcean Spaces (half a month)&lt;/td&gt;
          &lt;td&gt;$3.44&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Hatchbox.io Deployments&lt;/td&gt;
          &lt;td&gt;$14.13&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Claude Code Max&lt;/td&gt;
          &lt;td&gt;$192.84&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Honeybadger Error Tracking&lt;/td&gt;
          &lt;td&gt;$0&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Total&lt;/td&gt;
          &lt;td&gt;$239.54&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Total without Claude Code&lt;/td&gt;
          &lt;td&gt;$46.70&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;p&gt;Photo by &lt;a href=&#34;https://unsplash.com/@stellrweb?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash&#34;&gt;&lt;a href=&#34;https://unsplash.com/@stellrweb?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash&#34;&gt;StellrWeb&lt;/a&gt;&lt;/a&gt; on &lt;a href=&#34;https://unsplash.com/photos/white-canon-cash-register-djb1whucfBY?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash&#34;&gt;&lt;a href=&#34;https://unsplash.com/photos/white-canon-cash-register-djb1whucfBY?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash&#34;&gt;Unsplash&lt;/a&gt;&lt;/a&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>willow.camp Updates September 7 2025</title>
      <link>https://enumerator.dev/willow-camp-updates-september-7-2025/</link>
      <pubDate>Mon, 08 Sep 2025 00:20:35 +0000</pubDate>
      <guid>https://enumerator.dev/willow-camp-updates-september-7-2025/</guid>
      <description>&lt;p&gt;It&amp;rsquo;s been a busy month! I took some time off for a much-needed vacation and forgot to post about willow.camp. I&amp;rsquo;ve worked on some fun features in the past few weeks.&lt;/p&gt;
&lt;h2 id=&#34;new-features&#34;&gt;New Features&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Favicons now use &lt;a href=&#34;https://openmoji.org/&#34;&gt;OpenMoji&lt;/a&gt; emojis! I think these are adorable icons. I have a few qualms with how they fit in the willow.camp design (they&amp;rsquo;re all a little bottom-heavy), but it&amp;rsquo;s not a deal breaker.&lt;/li&gt;
&lt;li&gt;New favicon picker! You can now search for an emoji by name to pick for your blog.&lt;/li&gt;
&lt;li&gt;Now you can have two blogs! You can click on the profile icon and add a second blog. I&amp;rsquo;m going to use this to document my garden and remind myself what I want to do next year.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I also fiddled around with auto-generating OG images for each blog. I wrote a script that uses your OpenMoji favicon and your blog&amp;rsquo;s colours to create an Open Graph image for your blog.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what the tiled image looks like for enumerator.dev.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://enumerator.dev/tiled_1F3D5_1200x630.png&#34; alt=&#34;willow.camp tile&#34;&gt;&lt;/p&gt;
&lt;p&gt;Ultimately, the best approach for images is to enable users to upload them instead of doing some clever auto-generation. I&amp;rsquo;m going to put this on my project board and figure out how to do it. Since storage costs me, I might need to change the license on willow.camp and start charging for image uploads.&lt;/p&gt;
&lt;p&gt;Here is the Claude-generated summary of the last few weeks.&lt;/p&gt;
&lt;h2 id=&#34;summary---august-17---september-7-2025&#34;&gt;Summary - August 17 - September 7, 2025&lt;/h2&gt;
&lt;h3 id=&#34;major-features&#34;&gt;Major Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Multi-Blog Support&lt;/strong&gt;: Complete implementation of &amp;ldquo;Two Blogs per User&amp;rdquo; feature (&lt;a href=&#34;https://github.com/cassiascheffer/willow_camp/commit/a33fdaa9eec42e4f9e046653886963ce2a986dcc&#34;&gt;#85&lt;/a&gt;) - Major architectural change allowing users to have multiple blogs instead of a single-tenant structure&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenMoji Favicon System&lt;/strong&gt;: Implemented comprehensive emoji favicon selector with search functionality using OpenMoji library (&lt;a href=&#34;https://github.com/cassiascheffer/willow_camp/commit/2d13fd2e318339b77cf0068a0dd0b945896e44bd&#34;&gt;2d13fd2e&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;improvements&#34;&gt;Improvements&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OG Image Generation&lt;/strong&gt;: Added automated Open Graph image generation scripts (&lt;a href=&#34;https://github.com/cassiascheffer/willow_camp/commit/29ba2359fdd5b761d0cf995fbc3f8153e24a5c3f&#34;&gt;29ba2359&lt;/a&gt;, &lt;a href=&#34;https://github.com/cassiascheffer/willow_camp/commit/290f237256be0ff7f1ab7ddc100cbb82eef751d5&#34;&gt;290f2372&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>willow.camp Updates August 16, 2025</title>
      <link>https://enumerator.dev/willow-camp-updates-august-16-2025/</link>
      <pubDate>Sat, 16 Aug 2025 12:32:00 +0000</pubDate>
      <guid>https://enumerator.dev/willow-camp-updates-august-16-2025/</guid>
      <description>&lt;p&gt;It felt like a quiet week on willow.camp but somehow I still got lots done.&lt;/p&gt;
&lt;p&gt;Aside from the release notes below, I&amp;rsquo;m focusing my energy on getting proper favicons for willow.camp blogs.&lt;/p&gt;
&lt;p&gt;My first version of this will use &lt;a href=&#34;https://openmoji.org/&#34;&gt;OpenMoji&lt;/a&gt; emojis for favicons. I like their simple design principles and especially like that they are CC-BY-SA 4.0, similar to willow.camp&amp;rsquo;s source code.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how I think I&amp;rsquo;ll approach this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Include OpenMoji assets in the willow.camp source code.&lt;/li&gt;
&lt;li&gt;Use their &lt;a href=&#34;https://github.com/hfg-gmuend/openmoji/blob/master/data/openmoji.json&#34;&gt;source map&lt;/a&gt; to populate a choices.js dropdown on the settings page.&lt;/li&gt;
&lt;li&gt;Then save the Unicode reference to the database and use that to fetch the correct favicon files when rendering a page.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;OpenMoji provides pngs in two sizes, so I&amp;rsquo;ll need to resize them for the best cross-device compatibility.&lt;/p&gt;
&lt;p&gt;Okay, on to this week&amp;rsquo;s summary!&lt;/p&gt;
&lt;h2 id=&#34;improvements&#34;&gt;Improvements&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Migrated system tests from Selenium to Cuprite for better performance and reliability&lt;/li&gt;
&lt;li&gt;Optimized &lt;code&gt;ReservedWords&lt;/code&gt; with &lt;code&gt;Set&lt;/code&gt; data structure for faster lookups&lt;/li&gt;
&lt;li&gt;Added post-deploy script to send deploy messages to Discord&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;security--dependencies&#34;&gt;Security &amp;amp; Dependencies&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Updated Rails from 8.0.2 to 8.0.2.1&lt;/li&gt;
&lt;li&gt;Updated ActiveRecord from 8.0.2 to 8.0.2.1&lt;/li&gt;
&lt;li&gt;Updated Pagy from 9.3.5 to 9.4.0&lt;/li&gt;
&lt;li&gt;Updated solid_cable from 3.0.11 to 3.0.12&lt;/li&gt;
&lt;li&gt;Updated Honeybadger from 6.0.4 to 6.0.5&lt;/li&gt;
&lt;li&gt;Updated Jbuilder from 2.13.0 to 2.14.1&lt;/li&gt;
&lt;li&gt;Updated CommonMarker from 2.3.1 to 2.3.2&lt;/li&gt;
&lt;li&gt;Updated Lefthook from 1.12.2 to 1.12.3&lt;/li&gt;
&lt;li&gt;Updated GitHub Actions dependencies (actions/checkout v4→v5, browser-actions/setup-chrome v1→v2)&lt;/li&gt;
&lt;li&gt;Implemented subdomain validation and bot protection in &lt;code&gt;Rack::Attack&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Added user agent and IP logging to Honeybadger error context to better understand bot behaviour&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;bug-fixes&#34;&gt;Bug Fixes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Fixed deprecated &lt;code&gt;:unprocessable_entity&lt;/code&gt; status code (replaced with &lt;code&gt;:unprocessable_content&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Temporarily disabled auto-save functionality. This isn&amp;rsquo;t a bug fix. Sorry! I want to get to fixing this soon.&lt;/li&gt;
&lt;li&gt;Remove social_share_image JavaScript and HTML. I am going to sit on this a bit. I want this to be rendered server-side instead of in the browser. Once I integrate OpenMoji, this will be much easier because I&amp;rsquo;ll have png assets available on the server.&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>End To End Tests with Minitest and Rails</title>
      <link>https://enumerator.dev/end-to-end-tests-with-minitest-and-rails/</link>
      <pubDate>Wed, 13 Aug 2025 22:27:26 +0000</pubDate>
      <guid>https://enumerator.dev/end-to-end-tests-with-minitest-and-rails/</guid>
      <description>&lt;p&gt;willow.camp has a small system test suite that runs on CI. It used to use the out-of-the-box settings that come with Rails: Capybara, Selenium, etc.&lt;/p&gt;
&lt;p&gt;I noticed a flaky test on CI, which launched a journey to replace Selenium with Cuprite.&lt;/p&gt;
&lt;h2 id=&#34;fixing-the-flaky-test&#34;&gt;Fixing the Flaky Test&lt;/h2&gt;
&lt;p&gt;I worked through the flaky test with Claude Code, and as Claude was running the tests, I noticed this pop-up from Chrome.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A data breach on a site or app exposed your password&amp;hellip;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I don&amp;rsquo;t use Chrome very often, so this jumped out at me. Of course, I use the oh-so-secret password &amp;ldquo;password&amp;rdquo; in my test suite, and Chrome flagged it as unsafe.&lt;/p&gt;
&lt;p&gt;Disabling password leak detection fixed this for me.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;browser_options: { &amp;#34;disable-features&amp;#34;: &amp;#34;PasswordLeakDetection&amp;#34; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But I still got timeouts on CI that looked like the tests were running before the browser was ready.&lt;/p&gt;
&lt;p&gt;Waiting for CI is a bit painful. You can see my hilariously bad Git history with Claude&amp;rsquo;s gleeful overconfidence:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;* 0c514d1 - nosandbox (15 minutes ago) &amp;lt;Cassia Scheffer&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;* 57ac55b - bump timeout to 30 sec for ci (21 minutes ago)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;* 9d6e86e - rewirite cuprite/capybara configs (26 minutes ago)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;* 39f3238 - fix: simplify Cuprite configuration following best practices (10 hours ago)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;* 68bfd53 - fix: simplify Cuprite configuration and fix CI timeout issues (24 hours ago)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;* cebfae7 - fix: resolve Cuprite websocket timeout issues in CI (24 hours ago)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;* dc288b7 - fix: make Cuprite configuration more robust for CI environments (24 hours ago)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;* e5bb4df - fix: increase Cuprite timeout and add CI-specific settings for GitHub Acti&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;* cb36eaa - feat: migrate system tests from Selenium to Cuprite (35 hours ago)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;* 39f0ff1 - disable selenium chrome password detection (35 hours ago)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;that-looks-bad-cassia-why-would-you-let-claude-do-that&#34;&gt;That Looks Bad, Cassia. Why Would You Let Claude Do That!?&lt;/h3&gt;
&lt;p&gt;Because Claude is faster at making mistakes than I am, and I am tired of waiting for CI. Making mistakes quickly means speedier feedback. So I sent Claude off on a fool&amp;rsquo;s errand to help me learn from the mistakes I would otherwise have made myself.&lt;/p&gt;
&lt;p&gt;In the meantime, I read the &lt;a href=&#34;https://evilmartians.com/chronicles/system-of-a-test-setting-up-end-to-end-rails-testing#dockerizing-system-tests&#34;&gt;Evil Martians article&lt;/a&gt; about better system tests.&lt;/p&gt;
&lt;h2 id=&#34;using-cuprite-in-rails-with-minitest&#34;&gt;Using Cuprite in Rails with Minitest&lt;/h2&gt;
&lt;p&gt;The Evil Martians article on system tests uses RSpec, but willow.camp uses Minitest. Here&amp;rsquo;s what I had to change to make it work with Minitest.&lt;/p&gt;
&lt;h3 id=&#34;1-precompileassets-becomes-a-module-and-gets-called-when-setup-runs&#34;&gt;1. PrecompileAssets becomes a module and gets called when setup runs.&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rb&#34; data-lang=&#34;rb&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Precompile assets before running tests to avoid timeouts.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Do not precompile if webpack-dev-server is running (NOTE: MUST be launched with RAILS_ENV=test)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;module&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;PrecompileAssets&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;setup&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# Check if we&amp;#39;re running system tests by looking at the test files being loaded&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;running_system_tests&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;caller&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;any?&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;line&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;line&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;include?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;test/system&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;||&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;no&#34;&gt;ARGV&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;any?&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;arg&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;arg&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;include?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;test/system&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;||&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;no&#34;&gt;ENV&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;RAILS_TEST_TYPE&amp;#34;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;system&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;unless&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;running_system_tests&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nb&#34;&gt;puts&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;se&#34;&gt;\n&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;🚀️️  No system test selected. Skip assets compilation.&lt;/span&gt;&lt;span class=&#34;se&#34;&gt;\n&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;puts&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;se&#34;&gt;\n&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;🐢  Precompiling assets.&lt;/span&gt;&lt;span class=&#34;se&#34;&gt;\n&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;original_stdout&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;vg&#34;&gt;$stdout&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;clone&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;start&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;Time&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;current&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;begin&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;vg&#34;&gt;$stdout&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;reopen&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;no&#34;&gt;File&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;no&#34;&gt;File&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;::&lt;/span&gt;&lt;span class=&#34;no&#34;&gt;Constants&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;::&lt;/span&gt;&lt;span class=&#34;no&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;w&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nb&#34;&gt;require&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;rake&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;no&#34;&gt;Rails&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;application&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;load_tasks&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;no&#34;&gt;Rake&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;::&lt;/span&gt;&lt;span class=&#34;no&#34;&gt;Task&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;assets:precompile&amp;#34;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;].&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;invoke&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;ensure&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;vg&#34;&gt;$stdout&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;reopen&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;original_stdout&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nb&#34;&gt;puts&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Finished in &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;#{&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;no&#34;&gt;Time&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;current&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;start&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;round&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt; seconds&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Run the setup when this file is loaded&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;no&#34;&gt;PrecompileAssets&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;setup&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;defined?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;no&#34;&gt;Rails&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;2-betterrailssystemtests-becomes-a-moduel&#34;&gt;2. BetterRailsSystemTests Becomes a Moduel&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rb&#34; data-lang=&#34;rb&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;module&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;BetterRailsSystemTests&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;c1&#34;&gt;# Make failure screenshots compatible with multi-session setup.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;take_screenshot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;super&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;unless&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;Capybara&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;last_used_session&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;no&#34;&gt;Capybara&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;using_session&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;no&#34;&gt;Capybara&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;last_used_session&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;super&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;3-modules-are-included-in-applicationsystemtestcase&#34;&gt;3. Modules are included in ApplicationSystemTestCase&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rb&#34; data-lang=&#34;rb&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;require&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;test_helper&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;require&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;capybara/cuprite&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;require_relative&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;system/system_helper&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;ApplicationSystemTestCase&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;ActionDispatch&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;::&lt;/span&gt;&lt;span class=&#34;no&#34;&gt;SystemTestCase&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;kp&#34;&gt;include&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;BetterRailsSystemTests&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;kp&#34;&gt;include&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;CupriteHelpers&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;n&#34;&gt;driven_by&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;Capybara&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;javascript_driver&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;setup&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;super&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# Use JS driver always&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# Store original host for cleanup&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;vi&#34;&gt;@original_host&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;Rails&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;application&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;default_url_options&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;ss&#34;&gt;:host&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# Make urls in mailers contain the correct server host.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# This is required for testing links in emails (e.g., via capybara-email).&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;no&#34;&gt;Rails&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;application&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;default_url_options&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;ss&#34;&gt;:host&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;Capybara&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;server_host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;teardown&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# Restore original host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;no&#34;&gt;Rails&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;application&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;default_url_options&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;ss&#34;&gt;:host&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;vi&#34;&gt;@original_host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;super&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;flakiness-still-included-on-ci&#34;&gt;Flakiness Still Included on CI&lt;/h2&gt;
&lt;p&gt;I still see a bit of system test flakiness on CI, but far less than before. I am running on the default GitHub Actions runner, which is pretty small. So there&amp;rsquo;s a good chance that the flakiness is due to low resources on the machine. So far, tests have consistently passed when I run these on my M1 MacBook Air.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll dig into these flaky tests another day! But this looks like an improvement overall to me!&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>willow.camp Release Notes August 9, 2025</title>
      <link>https://enumerator.dev/willow-camp-release-notes-august-9-2025/</link>
      <pubDate>Sat, 09 Aug 2025 20:51:00 +0000</pubDate>
      <guid>https://enumerator.dev/willow-camp-release-notes-august-9-2025/</guid>
      <description>&lt;p&gt;Really quiet week for willow.camp this week.&lt;/p&gt;
&lt;p&gt;I updated a few dependencies and upgraded the server it runs on. Everything&amp;rsquo;s been running smoothly!&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also been working with an LLM to learn &lt;a href=&#34;https://hanamirb.org/&#34;&gt;Hanami&lt;/a&gt; and might eventually move the backend over to Hanami. I&amp;rsquo;d lose a lot of &amp;ldquo;batteries included&amp;rdquo; stuff switching to Hanami, but I like the philosophy they have.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s been nice working on something the LLM doesn&amp;rsquo;t know well. It often drifts off into Rails land, and I have to learn the right pattern to correct the LLM. I think I learn faster this way because it&amp;rsquo;s easier to be skeptical of the LLM than it is of my own mistakes.&lt;/p&gt;
&lt;p&gt;Please have a look at my first pass at the migration at the &lt;a href=&#34;https://github.com/cassiascheffer/willow_camp_2&#34;&gt;willow_camp_2&lt;/a&gt; repo.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>willow.camp Release Notes Week Ending August 2, 2025</title>
      <link>https://enumerator.dev/willow-camp-release-notes-week-ending-august-2-2025/</link>
      <pubDate>Sat, 02 Aug 2025 12:43:00 +0000</pubDate>
      <guid>https://enumerator.dev/willow-camp-release-notes-week-ending-august-2-2025/</guid>
      <description>&lt;p&gt;It was a quiet week for willow.camp. The biggest thing I did was to test out generating Open Graph images using a browser canvas. I also improved logging for Honeybadger Insights, which will help me find performance issues.&lt;/p&gt;
&lt;h2 id=&#34;beta-features&#34;&gt;Beta Features&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Added a beta feature for auto-generating Open Graph images for RSS feeds and social media. I&amp;rsquo;m still figuring out how I want this to work before releasing it for general use.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;improvements&#34;&gt;Improvements&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Added in-memory caching to the blog index page.&lt;/li&gt;
&lt;li&gt;Site map generation performance optimizations and test coverage.&lt;/li&gt;
&lt;li&gt;Added a few more Rack::Attack configurations for noisy bots.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;infrastructure--monitoring&#34;&gt;Infrastructure &amp;amp; Monitoring&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Logging Refactor to work better with Honeybadger Insights.&lt;/li&gt;
&lt;li&gt;Marksmith Editor: Fixed scroll position issues during autosave.&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Testing A New willow.camp Feature</title>
      <link>https://enumerator.dev/testing-a-new-willow-camp-feature/</link>
      <pubDate>Wed, 30 Jul 2025 13:12:00 +0000</pubDate>
      <guid>https://enumerator.dev/testing-a-new-willow-camp-feature/</guid>
      <description>&lt;p&gt;I don&amp;rsquo;t like competing for clicks. But I do like people reading my blog.&lt;/p&gt;
&lt;p&gt;If you landed here form LinkedIn, you probably saw a colorful image showing the title of this post. Where is that image?! It&amp;rsquo;s in the Open Graph tags in the &lt;code&gt;head&lt;/code&gt; of this page.&lt;/p&gt;
&lt;p&gt;When I launched willow.camp, I intentionally kept social-media style features out of it. willow.camp doesn&amp;rsquo;t track views (aside from what&amp;rsquo;s needed for performance and bug monitoring), and doesn&amp;rsquo;t have likes or comments.&lt;/p&gt;
&lt;p&gt;But it was missing something.&lt;/p&gt;
&lt;p&gt;When I shared posts to LinkedIn, my posts would show up as plain text. Despite having no analytics, I get the feeling that some people miss my posts because they are overwhelmed by a sea of images.&lt;/p&gt;
&lt;p&gt;So here I am. Writing a social media feature for willow.camp that is in beta (meaning I am the beta tester!) If you are using willow.camp, I&amp;rsquo;ll release this feature once I&amp;rsquo;ve put the right polish on it.&lt;/p&gt;
&lt;p&gt;I added a canvas to the post editing interface that grabs the theme colours, favicon, and title to create an Open Graph image that LinkedIn can see. This is the first time I have image storage.&lt;/p&gt;
&lt;p&gt;The images are stored locally on the server, so I&amp;rsquo;ll run out of space one day. But for now, I&amp;rsquo;m going to test this out and polish up the user experience before making it more broadly available.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>willow.camp Release Notes Week Ending July 26, 2025</title>
      <link>https://enumerator.dev/willow-camp-release-notes-week-ending-july-26-2025/</link>
      <pubDate>Sat, 26 Jul 2025 12:20:00 +0000</pubDate>
      <guid>https://enumerator.dev/willow-camp-release-notes-week-ending-july-26-2025/</guid>
      <description>&lt;p&gt;Here&amp;rsquo;s everything I did on willow.camp this week. It blows my mind how much I can get done when I&amp;rsquo;m having fun! I work on this app for about an hour every morning and evening.&lt;/p&gt;
&lt;h2 id=&#34;major-features-added&#34;&gt;Major Features Added&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Featured posts functionality with toggle UI&lt;/li&gt;
&lt;li&gt;Tap/click to edit a tag on the tags index page&lt;/li&gt;
&lt;li&gt;Terms of Service page&lt;/li&gt;
&lt;li&gt;Rack-attack integration for rate limiting and security (blocking PHP requests, credential probes)&lt;/li&gt;
&lt;li&gt;Added no-index option to settings page for search engine control&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;uiux-improvements&#34;&gt;UI/UX Improvements&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Replaced inline SVG icons with heroicons gem&lt;/li&gt;
&lt;li&gt;Enhanced mobile navigation by moving dashboard tabs to the hamburger menu&lt;/li&gt;
&lt;li&gt;Updated tag UI with circular icon buttons&lt;/li&gt;
&lt;li&gt;Replaced default Rails error pages with custom DaisyUI designs&lt;/li&gt;
&lt;li&gt;Improved post actions UI with a dropdown menu and a modal delete confirmation&lt;/li&gt;
&lt;li&gt;Added &amp;ldquo;Save Draft&amp;rdquo; and &amp;ldquo;Publish&amp;rdquo; buttons instead of a &amp;ldquo;Published&amp;rdquo; checkbox&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;performance--testing&#34;&gt;Performance &amp;amp; Testing&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Optimized Dashboard::TokensControllerTest (1.97s → 0.31s)&lt;/li&gt;
&lt;li&gt;Optimized slow mermaid test by reducing markdown content in tests&lt;/li&gt;
&lt;li&gt;Added Bullet gem for N+1 query detection&lt;/li&gt;
&lt;li&gt;Fixed N+1 query in API posts controller&lt;/li&gt;
&lt;li&gt;Re-enabled parallel test runs&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;code-quality&#34;&gt;Code Quality&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Removed RuboCop entirely and only use StandardRB for code style&lt;/li&gt;
&lt;li&gt;Added standard-rails gem for enhanced Rails linting&lt;/li&gt;
&lt;li&gt;Updated Brakeman from 7.0.2 to 7.1.0&lt;/li&gt;
&lt;li&gt;Merged willow_camp_cli history into cli/ subdirectory&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;security--seo&#34;&gt;Security &amp;amp; SEO&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Enhanced request logging with user agent and subdomain info&lt;/li&gt;
&lt;li&gt;Added CC license to GitHub repo&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;bug-fixes&#34;&gt;Bug Fixes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Fixed favicon controller error&lt;/li&gt;
&lt;li&gt;Fixed mobile tag editing card UI updates&lt;/li&gt;
&lt;li&gt;Fixed OG published_time tag for draft posts&lt;/li&gt;
&lt;li&gt;Prevented mobile auto-capitalization for domain inputs&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Use Precomputed Hashes Instead of BCrypt in Test</title>
      <link>https://enumerator.dev/use-precomputed-hashes-instead-of-bcrypt-in-test/</link>
      <pubDate>Mon, 21 Jul 2025 00:55:00 +0000</pubDate>
      <guid>https://enumerator.dev/use-precomputed-hashes-instead-of-bcrypt-in-test/</guid>
      <description>&lt;p&gt;I asked Claude to find the slowest test file again after my success &lt;a href=&#34;https://enumerator.dev/speeding-up-willow-camp-s-post_test-rb-by-99&#34;&gt;earlier today&lt;/a&gt; and Claude found a call to BCrypt in my fixtures that was gobbling up time.&lt;/p&gt;
&lt;p&gt;I need user records all over the place in my test and each new user is a new call to encrypt a password.&lt;/p&gt;
&lt;p&gt;Here is Claude&amp;rsquo;s summary, with my Grammarly edits:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Problem: The &lt;code&gt;PreviewsControllerTest#test_should_show_preview_for_own_unpublished_post&lt;/code&gt; test was taking 2.95 seconds due to BCrypt password hashing being computed dynamically in fixtures with &lt;code&gt;&amp;lt;%= BCrypt::Password.create(&amp;quot;password&amp;quot;) %&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Solution: Replaced the dynamic BCrypt computation with a pre-computed hash &lt;code&gt;$2a$12$dapdHZiOTxAL8OT9yNI6e.hhTjRhWgXq7kx737/xf8tBfQbMD2bi&lt;/code&gt; for all user fixtures.&lt;/p&gt;
&lt;p&gt;Result: The slowest test now runs in ~0.25-0.52 seconds (82% improvement) and all tests pass. The overall test suite performance has been significantly improved, as this optimization affects all tests that use user fixtures.&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
    <item>
      <title>willow.camp Release Notes Week of Jul 19</title>
      <link>https://enumerator.dev/willow-camp-release-notes-week-of-jul-19/</link>
      <pubDate>Sun, 20 Jul 2025 18:13:00 +0000</pubDate>
      <guid>https://enumerator.dev/willow-camp-release-notes-week-of-jul-19/</guid>
      <description>&lt;h2 id=&#34;features-added&#34;&gt;Features Added&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Live markdown preview functionality using &amp;ldquo;Preview Post&amp;rdquo; button at the bottom. This is better than the Marksmith &amp;ldquo;Preview&amp;rdquo; beside &amp;ldquo;Write&amp;rdquo; because it shows the rendered site.&lt;/li&gt;
&lt;li&gt;Automatic dark mode detection for Marksmith editor which is better than before but still needs work&lt;/li&gt;
&lt;li&gt;Enhanced tag selection using the Choices.js library. Yay! I learned something about importing and CSS variables here. I&amp;rsquo;ll write a post on this soon.&lt;/li&gt;
&lt;li&gt;API documentation link added to settings page.&lt;/li&gt;
&lt;li&gt;Post footer feature with conditional display (hidden on about page). Find this on your settings page.&lt;/li&gt;
&lt;li&gt;Tag functionality with tag counts and improved UI. I still want to work on this a bit.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thanks to &lt;a href=&#34;https://wondering.willow.camp/&#34;&gt;Mark&lt;/a&gt; for a few of these suggestions!&lt;/p&gt;
&lt;h2 id=&#34;bug-fixes&#34;&gt;Bug Fixes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Draft post handling - Skip OG published_time tag for draft posts and allow nil dates&lt;/li&gt;
&lt;li&gt;Colour fixes for UI elements&lt;/li&gt;
&lt;li&gt;Landing page navigation - Fixed redirect behaviour when logged-in users visit the home page&lt;/li&gt;
&lt;li&gt;Production optimization - Conditionally preload the app only in the production environment, fixes dev env hanging sometimes.&lt;/li&gt;
&lt;li&gt;JSON error handling - Return 404 for JSON requests to non-existent resources&lt;/li&gt;
&lt;li&gt;Accessibility updates for blog interface and voiceover support. &lt;a href=&#34;https://letterbird.co/cassia&#34;&gt;Feedback please!&lt;/a&gt; I&amp;rsquo;m new to a11y and would love to know how to make this better.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;performance--maintenance&#34;&gt;Performance &amp;amp; Maintenance&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Test optimization - &lt;a href=&#34;https://enumerator.dev/speeding-up-willow-camp-s-post_test-rb-by-99&#34;&gt;Reduced markdown content in slow Mermaid tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Parallel testing is temporarily disabled for stability (on my machine ☹️)&lt;/li&gt;
&lt;li&gt;Security update - Brakeman updated from 7.0.2 to 7.1.0&lt;/li&gt;
&lt;li&gt;Code quality - Multiple linting fixes applied&lt;/li&gt;
&lt;li&gt;Documentation - Added CLAUDE.md project instructions&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Speeding Up willow.camp&#39;s post_test.rb by 99%</title>
      <link>https://enumerator.dev/speeding-up-willow-camp-s-post_test-rb-by-99/</link>
      <pubDate>Sun, 20 Jul 2025 14:05:00 +0000</pubDate>
      <guid>https://enumerator.dev/speeding-up-willow-camp-s-post_test-rb-by-99/</guid>
      <description>&lt;p&gt;I just asked Claude to speed up the slowest test file in willow.camp. post_test.rb was taking over 1 second to execute. For a tiny Ruby app, that&amp;rsquo;s a long time.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what Claude did. I was not expecting such a simple fix!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Test: test_should_detect_mermaid_diagrams_and_set_has_mermaid_diagrams_flag in test/models/post_test.rb:82&lt;/p&gt;
&lt;p&gt;Issue: The test was using an unnecessarily large markdown string that required
expensive processing through the Commonmarker library.&lt;/p&gt;
&lt;p&gt;Optimization: Reduced the test markdown from a multi-section document to a minimal string that still tests the same functionality:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Before: Large heredoc with headers, multiple code blocks, and text content&lt;/li&gt;
&lt;li&gt;After: Simple &amp;ldquo;&lt;code&gt;mermaid\ngraph TD\n  A --&amp;gt; B\n&lt;/code&gt;&amp;rdquo; string
Performance Improvement:&lt;/li&gt;
&lt;li&gt;Before: 1.31 seconds&lt;/li&gt;
&lt;li&gt;After: 0.01 seconds&lt;/li&gt;
&lt;li&gt;Improvement: 99%+ faster (130x speed improvement)
The optimization maintains full test coverage while dramatically reducing execution time by eliminating unnecessary markdown processing overhead.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;The original test was designed to evaluate a full-fledged Markdown document that was about 10 lines long. I didn&amp;rsquo;t need that in this case because I just needed to test that my custom &lt;code&gt;mermaid&lt;/code&gt; code block worked.&lt;/p&gt;
&lt;p&gt;Thanks Claude!&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>How to Migrate Data From One Postgres Instance to Another</title>
      <link>https://enumerator.dev/how-to-migrate-data-from-one-postgres-instance-to-another/</link>
      <pubDate>Thu, 03 Jul 2025 00:57:00 +0000</pubDate>
      <guid>https://enumerator.dev/how-to-migrate-data-from-one-postgres-instance-to-another/</guid>
      <description>&lt;p&gt;My first version of willow.camp &lt;a href=&#34;https://enumerator.dev/should-i-use-a-managed-postgres-instance&#34;&gt;ran a Postgres instance on the same container as the app&lt;/a&gt;. Before going to production, I had to migrate the data from the local Postgres instance to the managed instance.&lt;/p&gt;
&lt;p&gt;I had a whopping 50-some records in the database. So, of course,  could have used the &lt;a href=&#34;https://github.com/cassiascheffer/willow_camp_cli&#34;&gt;willow_camp_cli&lt;/a&gt; to migrate them, but I wanted to do it the hard way. Because sometimes the hard way is fun. At least, it&amp;rsquo;s fun when it&amp;rsquo;s much lower stakes than working on a large production app.&lt;/p&gt;
&lt;p&gt;Here is how I migrated the data from one database to another using an SSH tunnel, &lt;code&gt;pg_dump&lt;/code&gt; and &lt;code&gt;pg_restore&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;maintenance-mode&#34;&gt;Maintenance Mode&lt;/h2&gt;
&lt;p&gt;I had all of two real-life users when I did this. So I didn&amp;rsquo;t need maintenance mode, but I went ahead and used it anyway. hatchbox has a maintenance mode toggle in the UI that I turned on.&lt;/p&gt;
&lt;h2 id=&#34;set-up-a-new-user&#34;&gt;Set Up a New User&lt;/h2&gt;
&lt;p&gt;Digital Ocean sets up a managed database with a root user. I wanted to create an app-specific user with access to the necessary databases.&lt;/p&gt;
&lt;p&gt;I connected to the managed database using psql and created a user, granting the necessary permissions.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;PGPASSWORD&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;CLUSTER_PASSWORD psql &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  -U cluster_admin &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  -h digitaloceanhost.com &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  -p &lt;span class=&#34;m&#34;&gt;25060&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  -d defaultdb
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;CREATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;USER&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;willow-camp&amp;#34;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;WITH&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;PASSWORD&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;NEW_PASSWORD&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;-- Grant connect permission to the database
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;GRANT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;CONNECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ON&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;DATABASE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;defaultdb&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TO&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;willow-camp&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;-- Grant usage on the schema
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;GRANT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;USAGE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ON&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SCHEMA&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;public&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TO&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;willow-camp&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;-- Grant table permissions (choose what you need)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;GRANT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;INSERT&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;UPDATE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;DELETE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ON&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ALL&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;TABLES&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;IN&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SCHEMA&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;public&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TO&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;willow-camp&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;-- Grant permissions on sequences (for auto-increment/serial columns)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;GRANT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;USAGE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ON&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ALL&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SEQUENCES&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;IN&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SCHEMA&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;public&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TO&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;willow-camp&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;-- Grant permissions on future tables (optional)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ALTER&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;DEFAULT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;PRIVILEGES&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;IN&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SCHEMA&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;public&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;GRANT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;INSERT&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;UPDATE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;DELETE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ON&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;TABLES&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TO&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;willow-camp&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ALTER&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;DEFAULT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;PRIVILEGES&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;IN&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SCHEMA&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;public&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;GRANT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;USAGE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ON&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SEQUENCES&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TO&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;willow-camp&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;export-data-from-the-old-database&#34;&gt;Export Data from the Old Database&lt;/h2&gt;
&lt;p&gt;To do this, I created an SSH tunnel to the hatchbox instance that hosted my app and database. Then, I used &lt;code&gt;pg_dump&lt;/code&gt; to export the data. &lt;code&gt;-L&lt;/code&gt; in the command below sets up port forwarding so that my local port &lt;code&gt;5432&lt;/code&gt; points to the hatchbox server where Postgres is running.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ssh -L 5432:localhost:5432 deploy@10.0.1.100
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then I created a pg_dump on my local machine through the tunnel. Note that &lt;code&gt;localhost:5432&lt;/code&gt; now points through the tunnel to the hatchbox database.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;pg_dump -Fc &lt;span class=&#34;s2&#34;&gt;&amp;#34;postgresql://app_user:PASSWORD@localhost:5432/myapp_production&amp;#34;&lt;/span&gt; &amp;gt; myapp_primary_20250625_185627.dump
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;import-data-to-the-new-database&#34;&gt;Import Data to the New Database&lt;/h2&gt;
&lt;p&gt;I made a common mistake here. A pg_dump file includes statements like &lt;code&gt;ALTER TABLE public.ar_internal_metadata OWNER TO user_a1b2c3d4e5f6;&lt;/code&gt;, which assigns ownership of a table to a user, but this user only existed in my hatchbox database.&lt;/p&gt;
&lt;p&gt;I worked around ownership issues by using the &lt;code&gt;--no-owner&lt;/code&gt; and &lt;code&gt;--no-priviliges&lt;/code&gt; options.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;PGPASSWORD&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;CLUSTER_PASSWORD pg_restore &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  -U cluster_admin &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  -h digitaloceanhost.com &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  -p &lt;span class=&#34;m&#34;&gt;25060&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  -d defaultdb &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  --no-owner &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  --no-privileges &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  -v &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  myapp_primary_20250625_185627.dump
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;verify-the-data&#34;&gt;Verify the Data&lt;/h2&gt;
&lt;p&gt;Once pg_restore had done its job, I connected to the new database again using the &lt;code&gt;psql&lt;/code&gt; command above and issued a few queries to check the data.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;relname&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;as&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;table_name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;n_live_tup&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;as&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;current_rows&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pg_stat_user_tables&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ORDER&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;BY&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;current_rows&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;DESC&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;connect-the-app-to-the-new-database&#34;&gt;Connect the App to the New Database&lt;/h2&gt;
&lt;p&gt;Finally, I updated the database environment variables in hatchbox, which rebooted the app. I performed a quick &lt;code&gt;rails console&lt;/code&gt; check to ensure the database was connected, then I took the app out of maintenance mode.&lt;/p&gt;
&lt;p&gt;Once the app was back up, I could see queries hitting the managed instance in the DigitalOcean UI, and I tore down the old database on my hatchbox server.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Should I use a Managed Postgres Instance?</title>
      <link>https://enumerator.dev/should-i-use-a-managed-postgres-instance/</link>
      <pubDate>Tue, 01 Jul 2025 13:04:00 +0000</pubDate>
      <guid>https://enumerator.dev/should-i-use-a-managed-postgres-instance/</guid>
      <description>&lt;p&gt;I asked myself this question while developing willow.camp and decided to use a managed instance before sharing willow.camp with the world. In this post, I’ll explain my thought process and how it changed as I finished off willow.camp’s blogging features.&lt;/p&gt;
&lt;h2 id=&#34;hatching-a-new-app&#34;&gt;Hatching a New App&lt;/h2&gt;
&lt;p&gt;Once I had a rough version of willow.camp coded up, I wanted to deploy it so that I could test features like subdomains, multiple users (real people, not just me and some database seeds), and eventually custom domains.&lt;/p&gt;
&lt;p&gt;I kicked off a new app on hatchbox.io using DigitalOcean. hatchbox wires everything up to deploy a Rails app off the &lt;code&gt;main&lt;/code&gt; branch of a GitHub repository. All I needed was a database, so I clicked “PostgreSQL” in hatchbox, so that my Rails app and database would run on the same server. In just a few clicks, willow.camp was up and running with 1 vCPU, 1 GB of RAM, and 25 GB of storage.&lt;/p&gt;
&lt;h2 id=&#34;going-to-production&#34;&gt;Going to Production&lt;/h2&gt;
&lt;p&gt;Once I had most of the features I wanted, I had to make a decision. What do I do if my server crashes? The environment and data are ephemeral. It is easy enough to get the web app up and running again, but I’d lose all the data in my database.&lt;/p&gt;
&lt;p&gt;My requirements for the database were:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Automated backups at a minimum in a daily interval. Despite making willow.camp for markdown, I end up saving drafts right in the UI.&lt;/li&gt;
&lt;li&gt;Access to backups for a limited period. I don’t need much more than two or three days of backups.&lt;/li&gt;
&lt;li&gt;Preferably, minimal configuration.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I had three options:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Take periodic snapshots of the server that I can restore in case of data loss. I didn’t see an automated way to take snapshots, so I dropped this from the list.&lt;/li&gt;
&lt;li&gt;Mount a storage volume: This would separate the storage from the container on which my app was running. This seemed like an okay option to me but I was also wary of managing a single server with the Rails app and Postgres.&lt;/li&gt;
&lt;li&gt;Use a managed Postgres instance: This met all my requirements, but it cost a bit more. I waffled on this and decided to go with it in the end.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;why-i-chose-a-managed-postgres-instance&#34;&gt;Why I Chose a Managed Postgres Instance&lt;/h2&gt;
&lt;p&gt;In the past, I worked on a production app where the app and database were hosted on the same server. It constantly felt like a precarious situation. The database and the app server were competing for resources.&lt;/p&gt;
&lt;p&gt;On willow.camp I only have 1 vCPU and 1 GB of RAM. I could upsize my droplet, but then I’d lose sleep over balancing resources between the two fundamental processes that run the app.&lt;/p&gt;
&lt;p&gt;willow.camp is a tiny app and can easily run with the Rails app and Database on the same small server. I wouldn’t &lt;em&gt;need&lt;/em&gt; to lose sleep over it, but I’d be thinking about it all the time.&lt;/p&gt;
&lt;p&gt;I needed peace of mind that the processes would keep running and my data would be safe. Scaling up a single instance to handle the app and database didn’t sound like a fun hobby for me. It certainly wasn’t a fun job when I worked on the production setup like this in the past.&lt;/p&gt;
&lt;p&gt;The resources on this instance are ideal for a small Rails app. With 1 vCPU and 1 GB of RAM, I can run 5 Puma threads. My response time is well under 200 milliseconds (often under 100ms), so it’ll be a while before I need to consider another CPU.&lt;/p&gt;
&lt;p&gt;Choosing a managed Postgres instance gave me peace of mind that my data was safe, my app server and my database had isolated resources, and I had the flexibility to change the app server without affecting the data. The extra $10 a month simplifies my tiny infrastructure and lets me focus on the fun parts of willow.camp.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Why “willow.camp”</title>
      <link>https://enumerator.dev/why-willow-camp/</link>
      <pubDate>Mon, 30 Jun 2025 12:25:59 +0000</pubDate>
      <guid>https://enumerator.dev/why-willow-camp/</guid>
      <description>&lt;p&gt;In my last post I forgot to explain the name “willow.camp.” It’s quite simple. We have a very cute dog named Willow and I thought I’d name this cute blogging service after him and “camp” was a great TLD that made me think of setting up camp and reading a good book.&lt;/p&gt;
&lt;p&gt;Like a base camp, willow.camp is a good place to stash your stuff. Right now you can stash your writing here. In the future, you can stash bookmarks and RSS feeds.&lt;/p&gt;
&lt;h2 id=&#34;the-naming-journey&#34;&gt;The Naming Journey&lt;/h2&gt;
&lt;p&gt;I really wanted willow.blog at first, but .blog was a bit to pricy for a side project. willow.page sounded nice but it was too similar to another great blogging service, pika.page.&lt;/p&gt;
&lt;p&gt;I spun the idea around for a while. I even got willow.camp deployed on hatchbox and it was still named willow.blog.&lt;/p&gt;
&lt;p&gt;I finally settled on willow.camp because I realized I wanted to make an RSS reader and bookmarking service too, and willow.camp sounded like a nice place to stash things.&lt;/p&gt;
&lt;p&gt;So, long story short, willow.camp is named after a cute dog named willow.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Introducing willow.camp</title>
      <link>https://enumerator.dev/introducing-willow-camp/</link>
      <pubDate>Sat, 28 Jun 2025 20:06:00 +0000</pubDate>
      <guid>https://enumerator.dev/introducing-willow-camp/</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been working on a side project and am finally ready to share it! My blog, enumerator.dev, is now hosted on my platform called &lt;a href=&#34;https://willow.camp&#34;&gt;willow.camp&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;willow.camp started because I wanted a platform that felt like me. I wanted a simple editing interface, the ability to import and export every post in markdown, and a pared-down reading interface.&lt;/p&gt;
&lt;p&gt;My blog was previously hosted on &lt;a href=&#34;https://ghost.org/&#34;&gt;Ghost&lt;/a&gt;, but the experience felt like an &amp;ldquo;influencer&amp;rdquo; blog to me. I&amp;rsquo;m a regular developer sharing the projects I work on, and I don&amp;rsquo;t need a big newsletter distribution list or metrics on open rates.&lt;/p&gt;
&lt;p&gt;I looked at &lt;a href=&#34;https://bearblog.dev/&#34;&gt;bearblog.dev&lt;/a&gt; and &lt;a href=&#34;https://pika.page/&#34;&gt;pika.page&lt;/a&gt; as alternatives to Ghost. They are both lovely platforms, and willow.camp is inspired by them, but they didn&amp;rsquo;t quite meet my needs. So I took the plunge and typed &lt;code&gt;rails new willow_camp&lt;/code&gt; in my terminal.&lt;/p&gt;
&lt;p&gt;Well, okay, I admit. First, I tried Elixir; then I read about doing it in Rust and tested out Hanami before realizing that I wanted to create something more than I wanted to learn a new language.&lt;/p&gt;
&lt;p&gt;Sometimes speed is about using a batteries-included framework more than it is about milliseconds of server render time.&lt;/p&gt;
&lt;h2 id=&#34;the-willowcamp-stack&#34;&gt;The willow.camp Stack&lt;/h2&gt;
&lt;p&gt;willow.camp is built on Rails 8, Turbo, Hotwire, Stimulus, and  DaisyUI. These libraries have made it easy to spin up scaffolding and prototype ideas. A considerable part of the initial commits in willow.camp are all &lt;code&gt;rails scaffold&lt;/code&gt; (not even AI!). As things took shape, I polished off features using &lt;a href=&#34;https://github.com/upstash/context7&#34;&gt;context7&lt;/a&gt; to pull docs into Zed hooked up to GitHub Copilot.&lt;/p&gt;
&lt;p&gt;So far, here are a few libraries I&amp;rsquo;ve used to develop willow.camp:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/mbleigh/acts-as-taggable-on&#34;&gt;acts-as-taggable-on&lt;/a&gt; for tagging posts.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/avo-hq/marksmith&#34;&gt;marksmith&lt;/a&gt; for a markdown editing UI.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/gjtorikian/commonmarker&#34;&gt;commonmarker&lt;/a&gt; for parsing and rendering markdown.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/waiting-for-dev/front_matter_parser&#34;&gt;front_matter_parser&lt;/a&gt; for parsing YAML front matter on markdown files.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/norman/friendly_id&#34;&gt;friendly_id&lt;/a&gt; for post slugs.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/ddnexus/pagy&#34;&gt;pagy&lt;/a&gt; for pagination.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/weppos/publicsuffix-ruby&#34;&gt;public_suffix&lt;/a&gt; for validating custom domains without a complex regex.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;hosting&#34;&gt;Hosting&lt;/h2&gt;
&lt;p&gt;willow.camp is hosted on a &lt;a href=&#34;https://www.digitalocean.com/&#34;&gt;DigitalOcean&lt;/a&gt; instance with a managed Postgres database. Deployments and configs are managed through &lt;a href=&#34;https://hatchbox.io/&#34;&gt;hatchbox.io&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;apm-errors-emails&#34;&gt;APM, Errors, Emails&lt;/h2&gt;
&lt;p&gt;I hooked up &lt;a href=&#34;https://www.scoutapm.com/&#34;&gt;Scout Monitoring&lt;/a&gt; for APM and &lt;a href=&#34;https://www.honeybadger.io/&#34;&gt;Honeybadger&lt;/a&gt; for error tracking. For now, I&amp;rsquo;m well below the usage limit for both, so I&amp;rsquo;m on the free plan. I&amp;rsquo;m happy with both of those, but I find the log management in Scout to be somewhat clunky on my tiny MacBook Air&amp;rsquo;s screen. I&amp;rsquo;m not sure I&amp;rsquo;d pay for the logging aspects of Scout, but everything else meets my needs.&lt;/p&gt;
&lt;p&gt;Password reset emails are sent through &lt;a href=&#34;https://loops.so/&#34;&gt;loops.so&lt;/a&gt;. Loops was straightforward to set up, and transactional emails are free.&lt;/p&gt;
&lt;h2 id=&#34;dashboard&#34;&gt;Dashboard&lt;/h2&gt;
&lt;p&gt;As mentioned above, the editing interface is &lt;a href=&#34;https://github.com/avo-hq/marksmith&#34;&gt;marksmith&lt;/a&gt;, an easy-to-use, plug-and-play markdown editor with a built-in preview feature. Posts are saved in both markdown and HTML formats. This makes exporting content through the API straightforward because Markdown is the base format in which posts are written and saved. HTML is only used for display.&lt;/p&gt;
&lt;h2 id=&#34;mermaid-diagrams&#34;&gt;Mermaid Diagrams&lt;/h2&gt;
&lt;p&gt;You can add a code block with &lt;code&gt;mermaid&lt;/code&gt; as the language, which will render a mermaid diagram on the page in place of the code block. Have a look at &lt;a href=&#34;https://enumerator.dev/how-rack-timeout-keeps-your-app-alive&#34;&gt;How Rack::Timeout Keeps Your App Alive&lt;/a&gt; for an example.&lt;/p&gt;
&lt;h2 id=&#34;auto-saving-posts&#34;&gt;Auto-saving Posts&lt;/h2&gt;
&lt;p&gt;I like to start a draft post, then walk away and think about it. While I enjoy writing in Obsidian, I often use my blog &amp;ldquo;drafts&amp;rdquo; as a place to store ideas.&lt;/p&gt;
&lt;p&gt;I added a stimulus controller that auto-saves draft posts. It checks if the post is &amp;ldquo;dirty&amp;rdquo; (i.e., changed since the last save) and saves it every 60 seconds. You can also hit Command/Control + S to save while you type.&lt;/p&gt;
&lt;p&gt;Auto-save is disabled on &amp;ldquo;published&amp;rdquo; posts because I wouldn&amp;rsquo;t want a half-edited post to be saved and published.&lt;/p&gt;
&lt;h2 id=&#34;no-image-hosting&#34;&gt;No Image Hosting&lt;/h2&gt;
&lt;p&gt;I don&amp;rsquo;t host images or static content aside from what&amp;rsquo;s in the Rails asset pipeline. This is intentional right now. I want to keep hosting complexity to a minimum, and I didn&amp;rsquo;t want to worry about storage services, mounting volumes, or data retention. For now, I&amp;rsquo;d like the server to be ephemeral.&lt;/p&gt;
&lt;h2 id=&#34;favicons-themes-subdomains--custom-domains&#34;&gt;Favicons, Themes, Subdomains, &amp;amp; Custom Domains&lt;/h2&gt;
&lt;p&gt;Of course, I spent the most time on the settings page.&lt;/p&gt;
&lt;p&gt;You can preview custom favicons and themes on the willow.camp home page by clicking the menu items in the top right. The themes come from DaisyUI. The favicon functionality still requires some refinement.&lt;/p&gt;
&lt;h3 id=&#34;how-willowcamp-sort-of-generates-favicons&#34;&gt;How willow.camp (sort of) Generates Favicons&lt;/h3&gt;
&lt;p&gt;Users can insert any emoji into the &amp;ldquo;favicon&amp;rdquo; settings form. The emoji is saved to their settings, and then when the page loads, it is rendered in in-memory canvases to generate the various sizes of favicons needed and insert them into the &lt;code&gt;head&lt;/code&gt; as an image in a URI scheme like so:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;link&lt;/span&gt; &lt;span class=&#34;na&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;image/png&amp;#34;&lt;/span&gt; &lt;span class=&#34;na&#34;&gt;rel&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;icon&amp;#34;&lt;/span&gt; &lt;span class=&#34;na&#34;&gt;sizes&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;16x16&amp;#34;&lt;/span&gt; &lt;span class=&#34;na&#34;&gt;href&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;data:image/png;base64,...lots of characters=&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This works for most cases, but since the image doesn&amp;rsquo;t exist until the JS renders it, some browsers may not display a favicon at all, and there are no Open Graph images available for link previews on social sites.&lt;/p&gt;
&lt;h4 id=&#34;better-favicons-in-the-future&#34;&gt;Better Favicons in the Future&lt;/h4&gt;
&lt;p&gt;I want to fix the favicon functionality. I think the best way to do this reliably is to pre-generate images for 10-20 emojis that users can pick from. This way, I only have to store a set number of assets on the server and can provide real links to favicon images.&lt;/p&gt;
&lt;p&gt;Another possibility is to render a canvas in the browser when the user selects the desired emoji. Then, when they update their settings, save the favicon as an attachment and generate the correct sizes on the server. This would provide flexibility to use any emoji, but it would also require hosting more static content, which I didn&amp;rsquo;t want to do yet.&lt;/p&gt;
&lt;h3 id=&#34;themes&#34;&gt;Themes&lt;/h3&gt;
&lt;p&gt;The DaisyUI themes are fun! I borrowed their theme picker HTML and used a Stimulus controller to give instant previews of each theme. Users&amp;rsquo; theme preferences are saved in the DB and rendered in the HTML.&lt;/p&gt;
&lt;p&gt;I experimented with allowing users to choose between a light and dark theme, but couldn&amp;rsquo;t get it to work to my satisfaction. &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme&#34;&gt;prefers-colour-scheme&lt;/a&gt; works great with DiasyUI&amp;rsquo;s default &lt;code&gt;light&lt;/code&gt; and &lt;code&gt;dark&lt;/code&gt;, but I didn&amp;rsquo;t see an easy way to pass a user&amp;rsquo;s preferences from the database into the CSS without compiling the assets on each request.&lt;/p&gt;
&lt;p&gt;I did have a light/dark functionality sort of working with a Stimulus controller and some data attributes in the HTML, but there was a brief flash of the default &lt;code&gt;light&lt;/code&gt; or &lt;code&gt;dark&lt;/code&gt; scheme before the chosen scheme was loaded.&lt;/p&gt;
&lt;p&gt;For now, you can pick one colour scheme for your willow.camp blog.&lt;/p&gt;
&lt;h3 id=&#34;subdomains-and-custom-domains&#34;&gt;Subdomains and Custom Domains&lt;/h3&gt;
&lt;p&gt;Thanks to hatchbox&amp;rsquo;s Caddy setup, subdomains and custom domains are served over HTTPS. Each blog has a subdomain like cassia.willow.camp and an optional custom domain like enumerator.dev. Users point a CNAME record to their subdomain to set up a custom domain name.&lt;/p&gt;
&lt;h2 id=&#34;seo&#34;&gt;SEO&lt;/h2&gt;
&lt;p&gt;It has been nearly a decade since I had to think about SEO. I&amp;rsquo;ve added &lt;a href=&#34;https://ogp.me/&#34;&gt;Open Graph&lt;/a&gt; tags to the pages, and every site gets a &lt;code&gt;sitemap.xml&lt;/code&gt; and &lt;code&gt;robots.txt&lt;/code&gt;. I tried to use semantic HTML as best I could and asked Claude to audit the pages.&lt;/p&gt;
&lt;h2 id=&#34;no-analytics&#34;&gt;No Analytics&lt;/h2&gt;
&lt;p&gt;I had plausible.io on my blog for a very brief period, but the analytics provided too much information for me. I just want to share what I write. I don&amp;rsquo;t need graphs.&lt;/p&gt;
&lt;p&gt;If you want to use willow.camp for your blog and want analytics, let me know, and I can add a feature to let you insert the code you need.&lt;/p&gt;
&lt;h2 id=&#34;accessibility&#34;&gt;Accessibility&lt;/h2&gt;
&lt;p&gt;I need to do another accessibility audit on willow.camp. I tried to ensure the HTML used aria labels that were clear and concise.&lt;/p&gt;
&lt;p&gt;I recently found &lt;a href=&#34;https://github.com/thoughtbot/capybara_accessibility_audit&#34;&gt;CapybaraAccessibilityAudit&lt;/a&gt;, which I&amp;rsquo;ll use to audit the dashboard and public-facing pages.&lt;/p&gt;
&lt;h2 id=&#34;about-page&#34;&gt;About Page&lt;/h2&gt;
&lt;p&gt;The &amp;ldquo;about&amp;rdquo; page is a bit of a hack. &amp;ldquo;Pages&amp;rdquo; and &amp;ldquo;Posts&amp;rdquo; are almost the same thing in the database. I used single table inheritance to create a single &amp;ldquo;about&amp;rdquo; page for each user. Every user always has one about page. If you delete your &amp;ldquo;about&amp;rdquo; page, a new draft page is created in the background.&lt;/p&gt;
&lt;p&gt;willow.camp doesn&amp;rsquo;t use pages otherwise. Everyone gets one &amp;ldquo;about&amp;rdquo; page.&lt;/p&gt;
&lt;h2 id=&#34;feeds-rss-atom-json&#34;&gt;Feeds: RSS, Atom, JSON&lt;/h2&gt;
&lt;p&gt;RSS and the &lt;a href=&#34;https://indieweb.org/&#34;&gt;indie web&lt;/a&gt; are having a comeback. I&amp;rsquo;ve been reading RSS on &lt;a href=&#34;https://feeeed.nateparrott.com/&#34;&gt;Feeeed&lt;/a&gt; and &lt;a href=&#34;https://feedbin.com/&#34;&gt;Feedbin&lt;/a&gt; lately, so proper feeds are essential to willow.camp.&lt;/p&gt;
&lt;p&gt;My pet peeve with &amp;ldquo;follow me on RSS&amp;rdquo; links is that most of the time, you end up downloading a file when you click on the link. That&amp;rsquo;s not helpful.&lt;/p&gt;
&lt;p&gt;I drew inspiration from &lt;a href=&#34;https://subscribeopenly.net/&#34;&gt;Subscribe Openly&lt;/a&gt; and created a helpful &amp;ldquo;Subscribe&amp;rdquo; page with clipboard helpers to copy the feed of your choice easily. Check it out: &lt;a href=&#34;https://enumerator.dev/subscribe&#34;&gt;enumerator.dev/subscribe&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;api-and-cli&#34;&gt;API and CLI&lt;/h2&gt;
&lt;p&gt;willow.camp comes with an &lt;a href=&#34;https://willow.camp/docs&#34;&gt;API&lt;/a&gt; and &lt;a href=&#34;https://github.com/cassiascheffer/willow_camp_cli&#34;&gt;CLI tool&lt;/a&gt; for managing content. I&amp;rsquo;ve used the CLI to import and format my content from Ghost.&lt;/p&gt;
&lt;p&gt;The CLI converts most of the HTML from Ghost correctly, but leaves some extraneous markdown that requires manual cleanup.&lt;/p&gt;
&lt;h2 id=&#34;todo-list&#34;&gt;Todo list&lt;/h2&gt;
&lt;p&gt;I have lots of things I still want to do on willow.camp. Here are a few things I have in mind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Write an &lt;a href=&#34;https://obsidian.md/&#34;&gt;Obsidian&lt;/a&gt; extension. I got Claude, Copilot, and Zed to build the CLI in Ruby. It does the job. However, what I&amp;rsquo;d like is a full-featured Obsidian extension that allows me to write, edit, publish, and sync my blog directly within Obsidian.&lt;/li&gt;
&lt;li&gt;Re-implement syntax highlighting in code blocks. I think I messed up code syntax highlighting somewhere along the way. I want to fix this.&lt;/li&gt;
&lt;li&gt;Fix the CLI tool&amp;rsquo;s gem publishing. Something borked here and I haven&amp;rsquo;t had a chance to fix it. If you want to use the latest CLI, you can build it from the repo for now.&lt;/li&gt;
&lt;li&gt;Add a manual &amp;ldquo;light&amp;rdquo; and &amp;ldquo;dark&amp;rdquo; toggle to toggle between a user&amp;rsquo;s preferred schemes. This wouldn&amp;rsquo;t use &lt;code&gt;prefers-color-scheme&lt;/code&gt; but would still allow light and dark mode.&lt;/li&gt;
&lt;li&gt;Disallow a light scheme to be paired with Dracula because Dracula lives in the dark.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;new-features&#34;&gt;New Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;I love RSS, and I mostly love most RSS readers. But like a blogging platform, I&amp;rsquo;ve had trouble finding one that fits me. I want to build an RSS reader functionality into willow.camp so you can read RSS in the same place you publish.&lt;/li&gt;
&lt;li&gt;Bookmarks! I love everything about &lt;a href=&#34;https://raindrop.io/&#34;&gt;raindrop.io&lt;/a&gt;, but I also like the challenge of building my own. So why not make willow.camp the super app of the indie web and add bookmarks? I&amp;rsquo;m joking here. I&amp;rsquo;d create a bookmarking feature for myself and hope that others will like it too.&lt;/li&gt;
&lt;li&gt;Maybe introduce distribution lists? I liked that people could get my posts by email on Ghost. I didn&amp;rsquo;t want to see the open rates and graphs. I might re-introduce email distribution with a more straightforward UI.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;thoughts-on-stimulus-hotwire-and-turbo&#34;&gt;Thoughts on Stimulus, Hotwire, and Turbo&lt;/h2&gt;
&lt;p&gt;Wow. I love this. Honestly, it makes me love developing web apps again. There is no heavy complexity between the frontend and backend. You don&amp;rsquo;t need to shift mindsets to start writing in a frontend framework.&lt;/p&gt;
&lt;p&gt;I didn&amp;rsquo;t expect to enjoy these features this much. However, I&amp;rsquo;m back to writing HTML that maps directly to what I see in the browser, and a few extra bits of syntax in &lt;code&gt;data-attribute&lt;/code&gt; fields make it a snappy and dynamic UI.&lt;/p&gt;
&lt;h2 id=&#34;willowcamp-is-free&#34;&gt;willow.camp is Free&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://willow.camp&#34;&gt;willow.camp&lt;/a&gt; is free to use. If people find it useful, I might introduce a paid tier to cover hosting costs.&lt;/p&gt;
&lt;p&gt;If you want a willow.camp blog, you can sign up and try it out.&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>