{"@attributes":{"version":"2.0"},"channel":{"title":"Jonathan Bennett","link":"https:\/\/jbennett.me\/","description":"Recent content on Jonathan Bennett","generator":"Hugo","language":"en-us","lastBuildDate":"Tue, 14 Apr 2026 00:00:00 +0000","item":[{"title":"My Attempt to Define Abstraction","link":"https:\/\/jbennett.me\/articles\/my-attempt-to-define-abstraction\/","pubDate":"Tue, 14 Apr 2026 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/my-attempt-to-define-abstraction\/","description":"<p>Listening to the <a href=\"https:\/\/overcast.fm\/+AAsAoKBBw6A\">code with jason podcast<\/a> with Joel Drapper and Jason asked for a definition of abstraction. Here is my attempt:<\/p>\n<p>Abstraction in composed of two things:<\/p>\n<ol>\n<li>Elimintation\/hiding of elements that are not required for communication\/understanding<\/li>\n<li>Communicating in a way and level that is appropriate for the remaining elements<\/li>\n<\/ol>\n<p>Joel used the example of \u201cclean your room\u201d. He did two things with this, he removed a million elements that are required but not core to the overall task (put away socks, make the bed, \u2026) and he communicated at higher level, the overall goal of the room being clean, not the specific body movements needed (open hand, grab sock, close hand).<\/p>"},{"title":"When You Get Product Market Fit on the First Try\u2026","link":"https:\/\/jbennett.me\/articles\/when-you-get-product-market-fit-on-the-first-try\/","pubDate":"Mon, 03 Nov 2025 08:00:00 -0400","guid":"https:\/\/jbennett.me\/articles\/when-you-get-product-market-fit-on-the-first-try\/","description":"<p>Yesterday I turned this:<\/p>\n<p><img src=\"https:\/\/jbennett.me\/images\/chicken-wings-before.jpeg\" alt=\"Raw chicken wings. One with a honey, brown sugar marinade and the other with a garlic, onion marinade.\"><\/p>\n<p>into this:<\/p>\n<p class=\"center\"><img src=\"https:\/\/jbennett.me\/images\/chicken-wings-after.jpeg\" alt=\"The boys excitedly waiting to dig into nachos and wings.\"><\/p>\n<p>Some product launches are more <em><strong>delicious<\/strong><\/em> than others.<\/p>"},{"title":"Deploy Kamal with Github Actions","link":"https:\/\/jbennett.me\/articles\/deploy-kamal-with-github-actions\/","pubDate":"Sat, 25 Oct 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/deploy-kamal-with-github-actions\/","description":"<p>Automatically deploying a Kamal application when you push to Github can be a simple 3 step process:<\/p>\n<ol>\n<li>Setup secrets<\/li>\n<li>Add a deploy workflow<\/li>\n<li>Profit<\/li>\n<\/ol>\n<h2 id=\"1-setup-secrets\">1. Setup Secrets<\/h2>\n<p>You are likely going to need a new personal access token for DockerHub and an SSH key for deployment.<\/p>\n<h3 id=\"docker-hub\">Docker Hub<\/h3>\n<p>In you Docker Hub, click your profile picture in the top right corner, and navigate to the <em>Account Settings<\/em> screen, then the <a href=\"https:\/\/app.docker.com\/accounts\/jonbca7\/settings\/personal-access-tokens\">Personal access tokens<\/a> screen. Make a new token, giving it a name for this project with read and wrote access. Take note of this new token.<\/p>"},{"title":"A Modern Solution to Frontend Styles","link":"https:\/\/jbennett.me\/articles\/a-modern-solution-to-frontend-styles\/","pubDate":"Fri, 24 Oct 2025 13:00:00 -0400","guid":"https:\/\/jbennett.me\/articles\/a-modern-solution-to-frontend-styles\/","description":"<p>Yesterday, we looked at the <a href=\"https:\/\/jbennett.me\/articles\/the-case-against-css-frameworks\">problems with CSS frameworks<\/a>, and I suggested going completely without one. To do that, let me make an initially awful suggestion\u2014and then make it much more appealing.<\/p>\n<h2 id=\"the-concept\">The Concept<\/h2>\n<p>Here\u2019s my (conceptual) suggestion: use something like ViewComponents and write all your styles inline.<\/p>\n<p>Hear me out. If you have a button component with inline styles, that\u2019s essentially the same as having a button component and a separate <code>button.css<\/code> file that you import\u2014this approach simply keeps everything in one place.<\/p>"},{"title":"The Case Against CSS Frameworks","link":"https:\/\/jbennett.me\/articles\/the-case-against-css-frameworks\/","pubDate":"Thu, 23 Oct 2025 22:00:00 -0400","guid":"https:\/\/jbennett.me\/articles\/the-case-against-css-frameworks\/","description":"<p>Our Rails testing friend, Jason Swett, has an unpopular opinion: <a href=\"https:\/\/x.com\/JasonSwett\/status\/1978835680445805031\">don\u2019t use third-party CSS frameworks<\/a>. I don\u2019t think he goes far enough. In fact, I believe you shouldn\u2019t use any frameworks\u2014not even first-party ones.<\/p>\n<h2 id=\"why-use-a-framework\">Why Use a Framework?<\/h2>\n<p>Before diving into the problems with frameworks, let\u2019s quickly review why we use them in the first place. A few major problems CSS frameworks aim to solve include:<\/p>\n<ul>\n<li><strong>Pre-built UI Components:<\/strong> A solid framework provides a wide range of ready-made components. This allows you to quickly drop in elements and focus on solving your actual business problems.<\/li>\n<li><strong>Accessibility:<\/strong> I\u2019m not an accessibility expert, and every time I revisit the topic, I discover new ways to make systems more inclusive for everyone.<\/li>\n<li><strong>Quality Design:<\/strong> CSS frameworks are often developed by dedicated teams focused solely on creating polished, high-quality design systems. For smaller teams, it\u2019s unlikely you\u2019ll produce a more robust general-purpose UI toolkit on your own.<\/li>\n<\/ul>\n<h2 id=\"the-problems-with-third-party-frameworks\">The Problems with Third-Party Frameworks<\/h2>\n<p>Third-party frameworks offer substantial value\u2014but they also come with serious drawbacks:<\/p>"},{"title":"Don't Forget Your Health Checks","link":"https:\/\/jbennett.me\/articles\/kamal-health-checks\/","pubDate":"Sun, 28 Sep 2025 08:49:50 -0400","guid":"https:\/\/jbennett.me\/articles\/kamal-health-checks\/","description":"<p>I&rsquo;m standardizing all my side projects to deploy via kamal, even ones not running Rails.<\/p>\n<p>Quick tip, make sure you have a reasonable health check in place. By default, Kamal will try to hit <code>\/up<\/code>. My Hugo site does not have that so the <code>deploy.yml<\/code> needs to be updated:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-yml\" data-lang=\"yml\"><span style=\"display:flex;\"><span><span style=\"color:#f92672\">proxy<\/span>:\n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#f92672\">ssl<\/span>: <span style=\"color:#66d9ef\">true<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#f92672\">hosts<\/span>:\n<\/span><\/span><span style=\"display:flex;\"><span>    - <span style=\"color:#ae81ff\">jbennett.me <\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>    - <span style=\"color:#ae81ff\">www.jbennett.me<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#f92672\">healthcheck<\/span>:\n<\/span><\/span><span style=\"display:flex;\"><span>    <span style=\"color:#f92672\">path<\/span>: <span style=\"color:#ae81ff\">\/<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>This looks for the site root to be accessible which is enough.<\/p>"},{"title":"The Real Power of AI Isn\u2019t Code, It\u2019s Perspective","link":"https:\/\/jbennett.me\/articles\/the-real-power-of-ai\/","pubDate":"Thu, 11 Sep 2025 08:00:00 -0400","guid":"https:\/\/jbennett.me\/articles\/the-real-power-of-ai\/","description":"<p>AI can write code that would take me hours, but that\u2019s not what excites me most.<\/p>\n<p>It\u2019s impressive what AI can code today, but that\u2019s not the part I like most.<\/p>\n<p>The real value is that AI frees me to focus on shaping and refining what\u2019s being built. When I write the code myself, it\u2019s too easy to cut corners or live with small flaws. With AI, I can nitpick relentlessly and keep pushing until the result is exactly what I want.<\/p>"},{"title":"Only Running Smoke Tests: A Practical Guide","link":"https:\/\/jbennett.me\/articles\/only-running-smoke-test\/","pubDate":"Mon, 08 Sep 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/only-running-smoke-test\/","description":"<p>When building a new feature, system tests can be incredibly handy. They let you walk through the UI like a user, catch errors as controllers are scaffolded, and jump into lower-level tests when logic starts branching.<\/p>\n<p>But <a href=\"https:\/\/youtu.be\/gcwzWzC7gUA?si=zaZKpqgey9Kov5iL&amp;t=1740\">David said (mostly) not to<\/a>\u2026 System tests can be slow and brittle, and they don\u2019t always provide the best return on investment.<\/p>\n<p>Here\u2019s a practical approach you can use:<\/p>\n<ol>\n<li>Keep most of your system tests as ephemeral tools outside the main test suite, or just drop them.<\/li>\n<li>Tag system tests that you want to run regularly as part of the test suite.<\/li>\n<li>Use an environment variable to control whether those tagged tests are executed.<\/li>\n<\/ol>\n<p>To mark a system test as a smoke test so it runs as part of the test suite, add a tag:<\/p>"},{"title":"Don't use bare dom_id","link":"https:\/\/jbennett.me\/articles\/dont-use-bare-dom_id\/","pubDate":"Mon, 01 Sep 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/dont-use-bare-dom_id\/","description":"<p><code>dom_id<\/code> is an awesome tool for letting you easily and consistently identify elements on the page, especially if they are a model:<\/p>\n<pre tabindex=\"0\"><code class=\"language-erb\" data-lang=\"erb\">&lt;%= render @post %&gt;\n\n&lt;!-- posts\/_post.html.erb --&gt;\n&lt;%= tag.div id: dom_id(post) do %&gt;\n  &lt;!-- --&gt;\n&lt;% end %&gt;\n<\/code><\/pre><p>The problem is that you might want to present the same object in different ways. You might have a post show up in the main feed, a sidebar as a related post, and on an admin page as a row in a table. If these all use the same <code>post_123<\/code> id, you won&rsquo;t be able to target them specifically when sending updates via Turbo for example.<\/p>"},{"title":"Docker Build Cloud with Kamal","link":"https:\/\/jbennett.me\/articles\/docker-build-cloud-with-kamal.md\/","pubDate":"Mon, 18 Aug 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/docker-build-cloud-with-kamal.md\/","description":"<p>Awhile ago I ran into an issue with TailwindCSS v4 not building a docker container (<a href=\"https:\/\/jbennett.me\/articles\/why-tailwindcss-throws-unknown-utility-class\/\">details here<\/a>), which lead to discovering Kamal&rsquo;s remote builders.<\/p>\n<p>Well last week, while deploying to a side project (which was using a remote builder on the same machine) it kept dying while building the image. I could have figured out why it was happening, but this seemed like a good opportunity to look into DockerHub&rsquo;s Build Cloud service.<\/p>"},{"title":"When to expect and when to allow","link":"https:\/\/jbennett.me\/articles\/when-to-allow\/","pubDate":"Thu, 07 Aug 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/when-to-allow\/","description":"<p>Knowing when to <code>except<\/code> and when to <code>allow<\/code> in an RSpec test is important. When you don&rsquo;t care about the method being called, use <code>allow<\/code>. When you do care, use <code>expect<\/code>.<\/p>\n<p>For instance, I was working on some code that:<\/p>\n<ol>\n<li>Did some stuff<\/li>\n<li>Logged a message<\/li>\n<li>Did more stuff<\/li>\n<li>Logged a message we cared about<\/li>\n<\/ol>\n<p>The code essentially looked like:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span>expect(<span style=\"color:#66d9ef\">Logger<\/span>)<span style=\"color:#f92672\">.<\/span>to receive(<span style=\"color:#e6db74\">:log<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>foo <span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">Foo<\/span><span style=\"color:#f92672\">.<\/span>new\n<\/span><\/span><span style=\"display:flex;\"><span>expect(<span style=\"color:#66d9ef\">Logger<\/span>)<span style=\"color:#f92672\">.<\/span>to receive(<span style=\"color:#e6db74\">:log<\/span>)<span style=\"color:#f92672\">.<\/span>with(<span style=\"color:#e6db74\">&#34;Foo complete&#34;<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>foo<span style=\"color:#f92672\">.<\/span>do_stuff\n<\/span><\/span><\/code><\/pre><\/div><p>This worked great, except it was <a href=\"https:\/\/jbennett.me\/glossary#flaky-test\">flaky<\/a>. Sometimes <code>do_stuff<\/code> would log multiple times before the one our test cared about. This meant the 2nd expect did not get the right value and the test would <em>sometimes<\/em> fail. This solution to this was simple, use allow:<\/p>"},{"title":"Still Me, Now at Cisco","link":"https:\/\/jbennett.me\/articles\/still-me-now-at-cisco\/","pubDate":"Mon, 23 Jun 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/still-me-now-at-cisco\/","description":"<p>Today marks the beginning of a new chapter. For the past 12 years, I\u2019ve been running my creatively (read: lazily) named solo business: Jonathan Bennett. In that time, I\u2019ve worked with everyone from local engineering shops to Fortune 500 tech giants, building everything from marketing sites to internal tools to SaaS products.<\/p>\n<p>But that (mostly) changes today.<\/p>\n<p>I&rsquo;ve accepted a full-time role at a little startup you might\u2019ve heard of \u2014 Cisco. My new title? Senior Build Engineer. Not too shabby for my first day working <em>for the man.<\/em><\/p>"},{"title":"AI Workflow v0.1","link":"https:\/\/jbennett.me\/articles\/ai-worklflow-v01\/","pubDate":"Fri, 20 Jun 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/ai-worklflow-v01\/","description":"<p>I&rsquo;ve seen several posts on how different people are working with AI tools and\u2013shocker\u2013my workflow is a little different. I&rsquo;m currently using Junie within RubyMine and it looks like this:<\/p>\n<ol>\n<li>Create a new branch for the new feature.<\/li>\n<li>Write out a high level description of what you want. This should be the happy path through the problem with a focus on the UI you want to achieve. Ensure that you ask for a system test for this work. Feed it to the AI<\/li>\n<li>Review the system test only. Treat it similar to a human PR review. Iterate until you are satisfied with the happy path system test.<\/li>\n<li>Skim the rest of the changes. Pick the most egregious problem and have the AI start working on that.<\/li>\n<li>Repeat step #4 until you are happy with the results.<\/li>\n<\/ol>\n<p>A side activity I like to do while the AI is working on stuff is to make a list of nits to pick. I&rsquo;ll usually start a second document and collect a list of small things I&rsquo;d like corrected in the code base. This makes it really easy to follow the boy scout rule and leave things just a little nicer when I am done. This will be completed separately from the main issue to avoid complicating the final feature PR.<\/p>"},{"title":"Do Hard Things","link":"https:\/\/jbennett.me\/articles\/do-hard-things\/","pubDate":"Thu, 19 Jun 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/do-hard-things\/","description":"<p>This might be personal life bleeding into business life, but it still applies: We all need to get more comfortable with doing hard things.<\/p>\n<p>Scratch that. Comfortable is the wrong word. We just need to get over it.<\/p>\n<p>It&rsquo;s not going going to get more comfortable. Stop waiting for that day. Tolerate the discomfort better.<\/p>"},{"title":"Your Code Comments should <blink>","link":"https:\/\/jbennett.me\/articles\/your-code-comments-should-blink\/","pubDate":"Wed, 18 Jun 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/your-code-comments-should-blink\/","description":"<p>There is a huge difference between code documentation and code commenting. Documentation can be super valuable and should be done with a lot of your code. Commenting, the way it is usually done, actually has negative value and should probably be avoided!<\/p>\n<h2 id=\"what-makes-good-documentation\">What Makes Good Documentation<\/h2>\n<p>Good documentation has two common characteristics: it provides clarity, and it&rsquo;s stable.<\/p>\n<p>Clarity is given by explaining the unit of code at an appropriate level. You are unlikely to need to know the exact algorithm used in a method, but you will need to know how options are used and what the expected outcomes and side effects are. at an appropriate level. You are unlikely to need to know the exact algorithm used in a method, but you will need to know how options are used and what the expected outcomes and side effects are.<\/p>"},{"title":"The Backslash That Ruined My Day","link":"https:\/\/jbennett.me\/articles\/the-backslash-that-ruined-my-day\/","pubDate":"Tue, 17 Jun 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/the-backslash-that-ruined-my-day\/","description":"<p>Quick word of warning with complex Tailwindcss classes: check your escaping.<\/p>\n<p>I wasn\u2019t able to change my HTML for a section of an app I was working on, but I wanted the toggle button to change when a sidebar contained content:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-html\" data-lang=\"html\"><span style=\"display:flex;\"><span>&lt;<span style=\"color:#f92672\">div<\/span>&gt;\n<\/span><\/span><span style=\"display:flex;\"><span>  &lt;<span style=\"color:#f92672\">div<\/span>&gt;\n<\/span><\/span><span style=\"display:flex;\"><span>    &lt;<span style=\"color:#f92672\">a<\/span> <span style=\"color:#a6e22e\">href<\/span><span style=\"color:#f92672\">=<\/span><span style=\"color:#e6db74\">&#34;\/&#34;<\/span> \n<\/span><\/span><span style=\"display:flex;\"><span>      <span style=\"color:#a6e22e\">turbo-frame<\/span><span style=\"color:#f92672\">=<\/span><span style=\"color:#e6db74\">&#34;sidebar&#34;<\/span> \n<\/span><\/span><span style=\"display:flex;\"><span>      <span style=\"color:#a6e22e\">class<\/span><span style=\"color:#f92672\">=<\/span><span style=\"color:#e6db74\">&#34;blue-or-red-if-sidebar-is-filled&#34;<\/span>&gt;\n<\/span><\/span><span style=\"display:flex;\"><span>      Load stuff\n<\/span><\/span><span style=\"display:flex;\"><span>    &lt;\/<span style=\"color:#f92672\">a<\/span>&gt;\n<\/span><\/span><span style=\"display:flex;\"><span>  &lt;\/<span style=\"color:#f92672\">div<\/span>&gt;\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span>  &lt;<span style=\"color:#f92672\">turbo-frame<\/span> <span style=\"color:#a6e22e\">id<\/span><span style=\"color:#f92672\">=<\/span><span style=\"color:#e6db74\">&#34;my_sidebar&#34;<\/span>&gt;\n<\/span><\/span><span style=\"display:flex;\"><span>    &lt;<span style=\"color:#f92672\">div<\/span>&gt;stuff&lt;\/<span style=\"color:#f92672\">div<\/span>&gt;\n<\/span><\/span><span style=\"display:flex;\"><span>  &lt;\/<span style=\"color:#f92672\">turbo-frame<\/span>&gt;\n<\/span><\/span><span style=\"display:flex;\"><span>&lt;\/<span style=\"color:#f92672\">div<\/span>&gt;\n<\/span><\/span><\/code><\/pre><\/div><p>The selector I ended up using looked something like this:<\/p>"},{"title":"You Need to Copy This From Scammers ASAP","link":"https:\/\/jbennett.me\/articles\/you-need-to-copy-this-from-scammers\/","pubDate":"Mon, 16 Jun 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/you-need-to-copy-this-from-scammers\/","description":"<p>I get a few dozen spam emails every day that all look about the same:<\/p>\n<ul>\n<li>Your bank account is compromised! Click here!<\/li>\n<li>You won $1,000,000! Just send us your bank info.<\/li>\n<li>Final warning: your PayPal is locked\u2014verify now!<\/li>\n<li>Urgent: IRS needs your SSN or you\u2019ll be arrested!<\/li>\n<li>Cheap Viagra! Get it now!<\/li>\n<\/ul>\n<p><em>This email is probably going to end up in your spam just because I&rsquo;m talking about this<\/em><\/p>\n<p>Every one of them is packed with typos, wildly over-the-top claims, and so obviously fake it would be funny if it weren&rsquo;t so sad. For the longest time I assumed the person on the other end must be an idiot to write such bad emails. Then I realized that what they are doing is brilliant.<\/p>"},{"title":"Finally Buying the AI Hype (Reluctantly)","link":"https:\/\/jbennett.me\/articles\/finally-buying-the-ai-hype\/","pubDate":"Fri, 13 Jun 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/finally-buying-the-ai-hype\/","description":"<p>I hadn\u2019t spent much time with the newer AI coding tools, but my early experiments weren\u2019t great. One of my first tries involved asking a tool to move and rename a file \u2014 it took five minutes, way too much hand-holding, and left me wondering why I didn\u2019t just do it myself.<\/p>\n<p>I finally got around to trying <a href=\"https:\/\/www.jetbrains.com\/junie\/\">Junie<\/a> from JetBrains in RubyMine \u2014 and yeah, I think I\u2019m finally buying into the AI coding hype.<\/p>"},{"title":"One Commit, One Change","link":"https:\/\/jbennett.me\/articles\/one-commit-one-change\/","pubDate":"Thu, 12 Jun 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/one-commit-one-change\/","description":"<p>I mentioned commits being too large yesterday. What do I mean by that? Here&rsquo;s a simple rule of thumb I use: <em>If your commit message includes the word &ldquo;and,&rdquo; it\u2019s probably doing too much.<\/em><\/p>\n<blockquote>\n<p>This commit changes x <strong>and<\/strong> y<\/p><\/blockquote>\n<blockquote>\n<p>This removes a <strong>and<\/strong> b<\/p><\/blockquote>\n<blockquote>\n<p>This updates foo <strong>and<\/strong> allows admins to\u2026<\/p><\/blockquote>\n<p>Each commit should represent one logical change. If it moves the feature forward in a clear, isolated way, it probably belongs on its own. There\u2019s no real downside to making tiny, incremental commits\u2014especially since you can always squash them later when merging to main.<\/p>"},{"title":"A Month-Old Bug and How I Found It","link":"https:\/\/jbennett.me\/articles\/a-month-old-bug-and-how-i-found-it\/","pubDate":"Wed, 11 Jun 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/a-month-old-bug-and-how-i-found-it\/","description":"<p>In my <a href=\"https:\/\/jbennett.me\/articles\/a-humbling-turboframe-lesson-from-yours-truly\/\">previous post<\/a>, I talked about a bug involving an invalid <code>&lt;turbo-frame&gt;<\/code> tag. The UI was fine when merged into <code>main<\/code>, but after a month, it stopped working. Here&rsquo;s how I tracked it down using <code>git<\/code>.<\/p>\n<p>First, I noted that the feature was committed to <code>main<\/code> on May 7th and was known to be  broken on May 26th. This gave me a window for when the issue likely started.<\/p>\n<p>Next, I tested several merges on different dates: May 16th, 21st, 19th, and finally May 20th. If the UI passed, I moved forward; if it failed, I went back.<\/p>"},{"title":"A Humbling TurboFrame Lesson from Yours Truly","link":"https:\/\/jbennett.me\/articles\/a-humbling-turboframe-lesson-from-yours-truly\/","pubDate":"Tue, 10 Jun 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/a-humbling-turboframe-lesson-from-yours-truly\/","description":"<p>There\u2019s nothing worse than code that\u2019s <em>almost<\/em> right\u2026<\/p>\n<p>I was helping debug an issue that came up with a client. They had a section on the page that would dynamically update when you selected a different option. This was a perfect situation for a TurboFrame:<\/p>\n<pre tabindex=\"0\"><code class=\"language-erb\" data-lang=\"erb\">&lt;%= turbo_frame_tag :cool_stuff do %&gt;\n  &lt;blink&gt;Cool Stuff&lt;\/blink&gt;\n  \n  &lt;a href=&#34;cool_stuff?load=more&#34;&gt;Load more suff!&lt;\/a&gt;\n&lt;% end %&gt;\n<\/code><\/pre><p>Over time this moved into a ViewComponent, and eventually stopped working. (Far) too long of a story made short, ViewComponent didn&rsquo;t expose the <code>turbo_frame_tag<\/code> helper, so it was switched to use <code>tag<\/code> instead. Yeah, <code>tag.turbo_frame_tag id: :cool_stuff<\/code>.<\/p>"},{"title":"Start Planning for Life After Low-Code","link":"https:\/\/jbennett.me\/articles\/start-planning-for-life-after-low-code\/","pubDate":"Mon, 09 Jun 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/start-planning-for-life-after-low-code\/","description":"<p>I often get asked: \u201cWhen should I use a low-code platform, and when should I switch to custom development?\u201d I love this question. It means you&rsquo;re thinking like a founder \u2014 balancing speed, cost, and the future.<\/p>\n<p>My answer?<\/p>\n<ul>\n<li>Stick with low-code as long as it\u2019s helping.<\/li>\n<li>Start switching a little sooner than you think you need to.<\/li>\n<\/ul>\n<p>Let&rsquo;s look at this<\/p>\n<h2 id=\"how-long-to-stay-on-low-code\">How Long to Stay on low-code<\/h2>\n<p>Low-code tools have two benefits, speed and access. It&rsquo;s never been easier to build custom applications without requiring years of experience. I love using low-code tools to build out a prototype to rapidly test ideas, even validating them with customers.<\/p>"},{"title":"TDD Let's You Ship, Even on Your Worst Day","link":"https:\/\/jbennett.me\/articles\/tdd-lets-you-ship-even-on-your-worst-day\/","pubDate":"Thu, 05 Jun 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/tdd-lets-you-ship-even-on-your-worst-day\/","description":"<p>This week is the first full week of summer weather. Naturally, I\u2019ve come down with a cold. My waste basket is overflowing with tissues, and I sound like I\u2019ve been gurgling gravel.<\/p>\n<p>I feel awful \u2014 but I can still confidently ship (minor) updates to production, because I trust my test suite.<\/p>\n<p>Protect your SaaS from a cold. Practice TDD.<\/p>"},{"title":"System Tests When They Matter","link":"https:\/\/jbennett.me\/articles\/system-tests-when-they-matter\/","pubDate":"Wed, 04 Jun 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/system-tests-when-they-matter\/","description":"<p>My TDD workflow with a new feature is pretty simple:<\/p>\n<ul>\n<li>Create a system test walking through home page -&gt; feature successfully used<\/li>\n<li>Create a system test for any critical workflows, but disable them<\/li>\n<li>Make the initial test pass<\/li>\n<li>Write some unit tests to handle edge cases<\/li>\n<li>Clean things up<\/li>\n<li>Repeat process for all critical workflows<\/li>\n<\/ul>\n<p>I write system tests for critical workflows early to start thinking through what I\u2019ll need soon \u2014 but I disable them at first so they don\u2019t get in the way while I\u2019m getting that first test to pass. Sometimes I sketch out the interface instead if that\u2019s faster.<\/p>"},{"title":"It Compiles. But It\u2019s Still Wrong.","link":"https:\/\/jbennett.me\/articles\/it-compiles-but-its-still-wrong\/","pubDate":"Tue, 03 Jun 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/it-compiles-but-its-still-wrong\/","description":"<p>I love strongly typed languages \u2014 but they\u2019re no substitute for TDD. For example, let&rsquo;s make an add function in Swift:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-swift\" data-lang=\"swift\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">func<\/span> <span style=\"color:#a6e22e\">add<\/span>(a: Int, b: Int) -&gt; Int {\n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#66d9ef\">return<\/span> <span style=\"color:#ae81ff\">42<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>}\n<\/span><\/span><\/code><\/pre><\/div><p>See?<br>\nStrong types mean our code is perfect!<\/p>\n<p>In a world of imperfect AI agents, junior devs, and even seniors, types aren\u2019t enough. That\u2019s why we write tests.<\/p>"},{"title":"AI Code Needs Rules. TDD Writes Them.","link":"https:\/\/jbennett.me\/articles\/ai-code-needs-rules-tdd-writes-them\/","pubDate":"Mon, 02 Jun 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/ai-code-needs-rules-tdd-writes-them\/","description":"<p>It\u2019s June now, and you know what that means \u2014 it\u2019s AI month!<\/p>\n<p>Just kidding. Every month is AI now.<\/p>\n<p>So here\u2019s the thing: if you\u2019re vibe coding your way to rocket ship growth, you need TDD even more. AI-generated code is tricky to keep working right. Models tend to overwrite themselves, which makes it nearly impossible to know what your software actually does.<\/p>\n<p>Tests written while practicing TDD are small, focused, and easy to understand. They act as a specification for the AI \u2014 and give you a way to verify that the output actually does what you intended.<\/p>"},{"title":"Let Rails Help You \u2013 Now With Caching","link":"https:\/\/jbennett.me\/articles\/let-rails-help-you-now-with-caching\/","pubDate":"Fri, 30 May 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/let-rails-help-you-now-with-caching\/","description":"<p><a href=\"https:\/\/thoughtbot.com\">Thoughtbot<\/a> recently published a great <a href=\"https:\/\/thoughtbot.com\/blog\/let-rails-help-you\">article<\/a> on building a favouriting system in Rails using standard conventions. I built almost the exact same thing for a client two weeks ago. This is one of the reasons Rails is so great: developers are naturally encouraged to write similar code to solve similar problems. But there\u2019s one place their implementation could trip you up: caching.<\/p>\n<p>As written, their <code>_note<\/code> partial would run into issues if you\u2019re caching it. The naive implementation renders the favourite button for User A, caches it, and then shows that same button to User B. Unless your users like seeing each other\u2019s favourites, this is a problem.<\/p>"},{"title":"Why TDD Is the Best Bug Spray!","link":"https:\/\/jbennett.me\/articles\/why-tdd-is-the-best-bug-spray\/","pubDate":"Thu, 29 May 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/why-tdd-is-the-best-bug-spray\/","description":"<p>In a past life I was responsible for development, customer support, and quality control of some projects. This was 15 years ago and I don\u2019t know if Drupal had great testing tools back then, but this project didn\u2019t! By the end of the project, almost half of my time was spent checking and fixing issues where one change over here affected something over there. <\/p>\n<p>It was like playing an endless game of whack-a-mole \u2014 fix one bug, and two more popped up. If that sounds familiar, it might be time to stop and invest in TDD.<\/p>"},{"title":"TDD Helps You Scale Without Slowing Down","link":"https:\/\/jbennett.me\/articles\/tdd-helps-you-scale-without-slowing-down\/","pubDate":"Wed, 28 May 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/tdd-helps-you-scale-without-slowing-down\/","description":"<p>Your specs are sharper, your team\u2019s performing better, and your system is more reliable than ever. Besides that, what else does TDD buy you?<\/p>\n<p>In a word: <strong>scale<\/strong>.<\/p>\n<p>How does TDD let you scale? Simple \u2014 it lets you hire more people, often at lower cost, while keeping your system and process stable as the team grows.<\/p>\n<p>The most common cost as you scale is due to communication overhead. TDD builds communication into your system, greatly reducing that cost.<\/p>"},{"title":"Confidence Isn\u2019t Magic \u2014 It\u2019s TDD","link":"https:\/\/jbennett.me\/articles\/confidence-isnt-magic-its-tdd\/","pubDate":"Tue, 27 May 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/confidence-isnt-magic-its-tdd\/","description":"<p>We&rsquo;ve already talked about how TDD improves the <em>inputs<\/em> to the development process \u2014 clearer specs, better alignment, fewer surprises. But TDD also improves the <em>outputs<\/em>.<\/p>\n<p>So what do I mean when I say TDD improves the <em>outputs<\/em>?<\/p>\n<p>It improves them in two key ways:<\/p>\n<ul>\n<li>It leads to better code.<\/li>\n<li>It encourages better workflows.<\/li>\n<\/ul>\n<p>Our focus for today is the workflow that TDD encourages.<\/p>\n<h2 id=\"the-workflow\">The Workflow<\/h2>\n<p>The TDD cycle is simple: write a test, make it pass as simply as possible, then improve the code. Repeat.<\/p>"},{"title":"Docs Rot. Tests Don\u2019t.","link":"https:\/\/jbennett.me\/articles\/docs-rot-tests-dont\/","pubDate":"Mon, 26 May 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/docs-rot-tests-dont\/","description":"<p>Software documentation falls into two buckets: high-level architectural documentation and low-level descriptions.<\/p>\n<p>High-level documentation is wonderful. It gives your team an appropriate lay-of-the-land they need to understand the project as a whole.<\/p>\n<p>Low-level documentation is a waste of time and should be avoided unless special circumstances make it necessary \u2014 for example, documenting the return value of <code>query.destroy_all<\/code> in a Rails controller, where a subtle behaviour could trip up other developers.<\/p>"},{"title":"Even I Mess This Up (Don\u2019t Do the Same)","link":"https:\/\/jbennett.me\/articles\/even-i-mess-this-up\/","pubDate":"Fri, 23 May 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/even-i-mess-this-up\/","description":"<p>While reviewing yesterday\u2019s post, I realized my example was wrong. \u201cThe customer needs to download historical reports\u201d isn\u2019t the customer\u2019s <em>problem<\/em> \u2014 it\u2019s already a <em>solution<\/em>. I am already jumping to a solution. This is exactly what I told you not to do.<\/p>\n<p>Which just goes to show \u2014 this is an easy mistake to make.<\/p>\n<p>The actual problem is that the customer needs to share historical information with external people.<\/p>"},{"title":"What devs need to know","link":"https:\/\/jbennett.me\/articles\/what-devs-need-to-know\/","pubDate":"Thu, 22 May 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/what-devs-need-to-know\/","description":"<p>The work we did yesterday \u2014 clearly specifying what you actually want \u2014 is often the hardest part of the development process. Congratulations, everything should be easy from here on out!<\/p>\n<p>All joking aside, most development teams don\u2019t get a clear understanding of what their task is and why it matters. Without that, they can\u2019t move with direction or deliver with confidence. This is the communication gap that persists between developers and founders.<\/p>"},{"title":"Specify Before You Build (It Pays Off)","link":"https:\/\/jbennett.me\/articles\/specify-before-your-build\/","pubDate":"Wed, 21 May 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/specify-before-your-build\/","description":"<p>Test-Driven Development (TDD) starts with imagining the code you <em>wish<\/em> existed, watching it fail, and then building it piece by piece until it works.<\/p>\n<p>That first step, defining what the code does, is a huge opportunity for you as a non-technical founder to be involved in specifying what the code should actually do. You don\u2019t need to write code to be involved \u2014 while there are formal tools developers might use, even clear, structured prose from you can be enough.<\/p>"},{"title":"Don't guess. Give your devs a playbook.","link":"https:\/\/jbennett.me\/articles\/give-your-developers-a-playbook\/","pubDate":"Tue, 20 May 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/give-your-developers-a-playbook\/","description":"<p>If you haven&rsquo;t heard of <a href=\"https:\/\/jbennett.me\/glossary\/#tdd\">TDD<\/a>, it&rsquo;s a practice software developers use while building software. When done well, it leads to software that\u2019s more reliable, easier to change, and something you can actually feel confident about shipping.<\/p>\n<p>This series won\u2019t get into the nuts and bolts of how to write tests. Instead, it\u2019s about how non-technical founders can guide and support developers who are using a playbook approach to building software \u2014 specifically, Test-Driven Development (TDD).<\/p>"},{"title":"Small but mighty: How teams should start","link":"https:\/\/jbennett.me\/articles\/small-but-mighty-how-teams-should-start\/","pubDate":"Fri, 16 May 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/small-but-mighty-how-teams-should-start\/","description":"<p>You&rsquo;ve probably heard the saying: &ldquo;If you want to go fast, go alone; if you want to go far, go together.&rdquo; But when it comes to software startups, this advice misses the mark. Early on, a single skilled developer \u2014 or a very small, focused team \u2014 will often move faster and build better foundations than a larger group of less experienced developers. Here\u2019s why.<\/p>\n<p>Here\u2019s why the common wisdom falls apart in software development:<\/p>"},{"title":"MVPs are dead. Build delight instead.","link":"https:\/\/jbennett.me\/articles\/mvps-are-dead-build-delight-instead\/","pubDate":"Thu, 15 May 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/mvps-are-dead-build-delight-instead\/","description":"<p>Yesterday we talked about how the <a href=\"https:\/\/jbennett.me\/articles\/the-death-of-the-mvp\/\">MVP is dead<\/a>. So what now?<\/p>\n<p>The new hot term you\u2019re looking for is the <strong>Minimally Delightful Product<\/strong>.<\/p>\n<p>This isn\u2019t just the smallest thing you can ship \u2014 it\u2019s the smallest thing you can ship that <em>delights<\/em> your customer. That moment of joy when they realize your product just made something hard feel effortless. That\u2019s what you\u2019re aiming for.<\/p>\n<p>Think of it like this: there are people faster than me, smarter than me, stronger than me \u2014 but superpowers are something else entirely. Superman isn\u2019t just strong. He\u2019s orders of magnitude stronger.<\/p>"},{"title":"The death of the MVP","link":"https:\/\/jbennett.me\/articles\/the-death-of-the-mvp\/","pubDate":"Wed, 14 May 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/the-death-of-the-mvp\/","description":"<p>The usual advice when launching a new SaaS? Build the smallest thing that delivers value \u2014 an <a href=\"https:\/\/jbennett.me\/glossary#mvp\">MVP<\/a>. It used to be solid guidance. But things have changed.<\/p>\n<p>Thanks to no-code tools and AI-fueled <a href=\"https:\/\/jbennett.me\/articles\/built-by-ai-maintained-by-regret\/\">vibe coding<\/a>, your potential customer can probably duct-tape together a halfway-decent solution themselves. In other words:<\/p>\n<p><strong>The \u201cminimum\u201d in MVP just doesn\u2019t cut it anymore.<\/strong><\/p>\n<p>You can\u2019t just solve the core problem. You have to do it better than a founder with ChatGPT and a few Zapier zaps. That means your product needs to be:<\/p>"},{"title":"The problem with reusing queries after destroy_all","link":"https:\/\/jbennett.me\/articles\/the-problem-with-reusing-queries-after-destroy_all\/","pubDate":"Tue, 13 May 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/the-problem-with-reusing-queries-after-destroy_all\/","description":"<p>Hopefully these emails help you generally, but today this one is a reminder for me.<\/p>\n<p>Jonathan, <code>destroy_all<\/code> affects the results of the original query. You can&rsquo;t just blindly reuse it:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">PostsController<\/span> <span style=\"color:#f92672\">&lt;<\/span> <span style=\"color:#66d9ef\">ApplicationController<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">delete_a_lot<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>    @posts <span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">Post<\/span><span style=\"color:#f92672\">.<\/span>where(id: params<span style=\"color:#f92672\">[<\/span><span style=\"color:#e6db74\">:post_ids<\/span><span style=\"color:#f92672\">]<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>    @posts<span style=\"color:#f92672\">.<\/span>destroy_all <span style=\"color:#75715e\"># nope <\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span>    render <span style=\"color:#e6db74\">turbo_stream<\/span>: @posts<span style=\"color:#f92672\">.<\/span>map { turbo_stream<span style=\"color:#f92672\">.<\/span>remove post }\n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>The <code>@posts<\/code> array is always blank when it hits the render call\u2014even though the posts are deleted. The correction is to update the &ldquo;nope&rdquo; line above to:<\/p>"},{"title":"Why Tailwindcss throws \"unknown utility class\"","link":"https:\/\/jbennett.me\/articles\/why-tailwindcss-throws-unknown-utility-class\/","pubDate":"Mon, 12 May 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/why-tailwindcss-throws-unknown-utility-class\/","description":"<p>Over the weekend, I ran into a deployment issue using Kamal and TailwindCSS. Everything had been working fine until I suddenly started seeing this error during asset precompilation:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-plain\" data-lang=\"plain\"><span style=\"display:flex;\"><span>#16 [build 6\/6] RUN SECRET_KEY_BASE_DUMMY=1 .\/bin\/rails assets:precompile\n<\/span><\/span><span style=\"display:flex;\"><span>#16 2.683 Error: Cannot apply unknown utility class: py-2\n<\/span><\/span><span style=\"display:flex;\"><span>#16 2.694 bin\/rails aborted!\n<\/span><\/span><\/code><\/pre><\/div><p>There were no recent changes to the codebase that would explain it.<\/p>\n<h2 id=\"what-happened\">What Happened?<\/h2>\n<p>After digging into the issue and checking GitHub discussions, I found that the root cause was a platform mismatch during Docker image builds. This may have been triggered by a recent macOS update, which seems to have affected how native binaries behave when building images for a different architecture. Specifically:<\/p>"},{"title":"To ViewComponent, or not to ViewComponent","link":"https:\/\/jbennett.me\/articles\/to-view-component-or-not-to-view-component\/","pubDate":"Fri, 09 May 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/to-view-component-or-not-to-view-component\/","description":"<p>I was recently asked by a client about when to create a new partial and when to create a new ViewComponent. My rules of thumb are:<\/p>\n<ol>\n<li>Use a ViewComponent for everything that isn&rsquo;t a view (new.html.erb etc), and isn&rsquo;t an ActiveModel partial (posts\/_post.html.erb)<\/li>\n<li>ActiveModel partials should be moved into a ViewComponent as soon as you are tempted to add a view only concept to the model.<\/li>\n<li>Feel free to leave a one line partial that just renders a ViewComponent if it makes it easier to integrate with the normal Rails way of doing things.<\/li>\n<\/ol>\n<p>This will leave you with a lot more ViewComponents, so make sure you keep them organized!<\/p>"},{"title":"Rails Tip: Skip has_many_attached and make a model","link":"https:\/\/jbennett.me\/articles\/skip-has_many_attached\/","pubDate":"Thu, 08 May 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/skip-has_many_attached\/","description":"<p><a href=\"https:\/\/guides.rubyonrails.org\/active_storage_overview.html\">ActiveStorage<\/a> is great, and I love the ease and flexibility of <code>has_one_atached<\/code>.<\/p>\n<p>Here&rsquo;s the thing though: I try to avoid <code>has_many_attached<\/code> as much as possible. Why? There are two big reasons<\/p>\n<h2 id=\"unexpected-behaviour\">Unexpected Behaviour<\/h2>\n<p>One thing that can be unexpected when you first work with ActiveStorage&rsquo;s <code>has_many_attached<\/code> is that an individual upload will replace the entire list of attachments. This makes sense when you think about it and is <a href=\"https:\/\/guides.rubyonrails.org\/active_storage_overview.html#replacing-vs-adding-attachments\">clearly documented<\/a> so I do not fault them. It is simply something to be aware of.<\/p>"},{"title":"Design routes around intent, not tables","link":"https:\/\/jbennett.me\/articles\/design-routes-around-intent-not-tables\/","pubDate":"Wed, 07 May 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/design-routes-around-intent-not-tables\/","description":"<p>When I first started with Rails, my routes, models, and tables always matched up.\nI had <code>resources :posts<\/code>, a <code>Post<\/code> model, and a <code>posts<\/code> table. It felt tidy and predictable.<\/p>\n<p>But here&rsquo;s the thing: <strong>you don\u2019t have to keep it that way\u2014and you probably shouldn\u2019t<\/strong>.<\/p>\n<h2 id=\"an-example-smart-deletion\">An Example: Smart Deletion<\/h2>\n<p>Let\u2019s say you have an <code>Event<\/code> resource. Events can have nested child events. When a user deletes a parent event, they should be able to choose:<\/p>"},{"title":"Screenshot or It Didn\u2019t Fail","link":"https:\/\/jbennett.me\/articles\/screenshot-or-it-didnt-fail\/","pubDate":"Tue, 06 May 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/screenshot-or-it-didnt-fail\/","description":"<p>One simple trick I like to use to make TDD a little smoother: <strong>take a screenshot whenever a system spec fails<\/strong>.<\/p>\n<p>Lately, I\u2019ve been using Playwright via Capybara for system tests, and while it\u2019s fast, that speed can cause issues. Sometimes the test runner would blaze past the flash message (<code>&quot;Thing successfully created&quot;<\/code>) or try to interact with a page it hadn\u2019t quite landed on yet.<\/p>\n<p>Having a screenshot at the time of failure made it instantly obvious what went wrong. And the best part? It only takes a tiny bit of config to make it happen.<\/p>"},{"title":"How to do OCR in Noteful","link":"https:\/\/jbennett.me\/articles\/how-to-ocr-in-noteful\/","pubDate":"Mon, 05 May 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/how-to-ocr-in-noteful\/","description":"<p>Ever wanted to do OCR in an iOS app that doesn&rsquo;t support it? I know I have! Here&rsquo;s a video with how I do that in Noteful, my favourite digital notebook app:<\/p>\n<p><a href=\"https:\/\/www.youtube.com\/watch?v=ddt4rUxyi4o\">![How to do OCR in Noteful]({{ &ldquo;\/images\/youtube-noteful-ocr.png&rdquo; | absolute_url }})<\/a><\/p>"},{"title":"How I handle bugs without losing my mind","link":"https:\/\/jbennett.me\/articles\/how-i-handle-bugs\/","pubDate":"Fri, 02 May 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/how-i-handle-bugs\/","description":"<p>I have a confession: I&rsquo;ve written bugs before.<\/p>\n<p class=\"text-center text-4xl\">\ud83d\ude31<\/p>\n<p>But here&rsquo;s the thing: bugs happen. What matters is having a consistent, reliable process for dealing with them\u2014especially one that keeps you from fixing the <em>same<\/em> bug more than once.<\/p>\n<p>Here\u2019s the approach I use when a bug shows up:<\/p>\n<h2 id=\"-the-bug-fix-loop-\">\ud83e\udeb2 The Bug-Fix Loop \ud83d\udc1e<\/h2>\n<ol>\n<li>\n<p><strong>Quick triage:<\/strong><br>\nIs this bug worth fixing? If it\u2019s clearly a one-off or edge case that\u2019s not affecting users, sometimes it\u2019s okay to leave it alone. Triage saves you from wasting time.<\/p>"},{"title":"The Positive Side of Vibe Coding","link":"https:\/\/jbennett.me\/articles\/the-positive-side-of-vibe-coding\/","pubDate":"Thu, 01 May 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/the-positive-side-of-vibe-coding\/","description":"<p><a href=\"https:\/\/jbennett.me\/articles\/built-by-ai-maintained-by-regret\">Yesterday<\/a>, we talked about the downsides of vibe coding\u2014how it can lead to messy, unmaintainable code if you\u2019re not careful.<\/p>\n<p>But today? Let\u2019s flip the script and explore why vibe coding might actually be the perfect approach in certain situations.<\/p>\n<p>When you\u2019re moving fast and experimenting, AI-generated features can be your best friend. It\u2019s not about perfection\u2014it\u2019s about learning quickly, testing ideas, and figuring out what works. In the early stages of building a product, <strong>speed<\/strong> matters more than <strong>structure<\/strong>, and vibe coding is built for speed.<\/p>"},{"title":"Built by AI, Maintained by Regret","link":"https:\/\/jbennett.me\/articles\/built-by-ai-maintained-by-regret\/","pubDate":"Wed, 30 Apr 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/built-by-ai-maintained-by-regret\/","description":"<p>Vibe coding is the trend of letting AI go wild and build features with minimal human input. You don\u2019t write the code\u2014you describe what you want, and the AI delivers. It\u2019s like having a junior developer who works fast, doesn\u2019t ask questions, and doesn\u2019t sleep.<\/p>\n<p>If you\u2019re trying to get a prototype up fast, it feels magical.<\/p>\n<p>But\u2014and here\u2019s the part where I ruin the fun\u2014<strong>not all code is meant to last<\/strong>, and vibe coding doesn\u2019t care which kind it\u2019s writing.<\/p>"},{"title":"Talk to your customers","link":"https:\/\/jbennett.me\/articles\/talk-to-your-customers\/","pubDate":"Tue, 29 Apr 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/talk-to-your-customers\/","description":"<p>Quick reminder: As a founder, your most important job is to talk to customers.<\/p>\n<p>No one knows if you are building the right thing, except your customers.<\/p>\n<p>The best people to evaluate your ideas, are your customers.<\/p>\n<p>The people who will fall in love and rave about your product, is your customers.<\/p>\n<p>The are the life of a SaaS. Especially yours.<\/p>"},{"title":"Recurring Jobs in Rails with Solid Queue","link":"https:\/\/jbennett.me\/articles\/recurring-jobs-with-solid-queue\/","pubDate":"Mon, 28 Apr 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/recurring-jobs-with-solid-queue\/","description":"<p>So <a href=\"https:\/\/jbennett.me\/articles\/rails-background-jobs-with-solid-queue\/\">last time<\/a>, we looked at installing Solid Queue. Today we\u2019ll use it to move some work off the request cycle and into a recurring background job.<\/p>\n<h2 id=\"the-current-setup\">The Current Setup<\/h2>\n<p>Right now, the system triggers an update to the kids\u2019 schedule during page load. Here\u2019s a simplified version of what that looks like:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">DashboardsController<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">show<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t@dashboard <span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">Dashboard<\/span><span style=\"color:#f92672\">.<\/span>find_by(<span style=\"color:#e6db74\">slug<\/span>: params<span style=\"color:#f92672\">[<\/span><span style=\"color:#e6db74\">:id<\/span><span style=\"color:#f92672\">]<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t@dashboard<span style=\"color:#f92672\">.<\/span>account<span style=\"color:#f92672\">.<\/span>update_kids_schedule <span style=\"color:#75715e\"># \ud83d\udc4e<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>This works, but it slows down the page and means schedules aren\u2019t up-to-date until someone visits the dashboard. We can do better.<\/p>"},{"title":"Rails background jobs with Solid Queue","link":"https:\/\/jbennett.me\/articles\/rails-background-jobs-with-solid-queue\/","pubDate":"Fri, 25 Apr 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/rails-background-jobs-with-solid-queue\/","description":"<p>I made an app to help track my kids\u2019 chores and responsibilities (yes, I am <em>that<\/em> kind of dad). We\u2019ve got an old iPad mounted in the kitchen that stays on a dashboard screen, showing outstanding tasks and reminders.<\/p>\n<p>One of my favourite features is the weekly schedule builder. My wife and I can quickly create recurring chores\u2014but those tasks don\u2019t get created automatically. Some pages in the app create missing tasks on page load, which was easy to implement at first. But since the dashboard uses Turbo Streams and doesn\u2019t refresh constantly, it\u2019s usually out of date by morning.<\/p>"},{"title":"Deploy with caution","link":"https:\/\/jbennett.me\/articles\/deploy-with-caution\/","pubDate":"Thu, 24 Apr 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/deploy-with-caution\/","description":"<p>What is it about pushing an update to staging or production to make all the visual issues jump off the screen?<\/p>\n<p>Not that this ever happens to me. Just mentioning it err\u2026 for a friend.<\/p>\n<p>Deploy safely my friends.<\/p>"},{"title":"Please push back when you are right","link":"https:\/\/jbennett.me\/articles\/please-push-back\/","pubDate":"Wed, 23 Apr 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/please-push-back\/","description":"<p>I recently received a bug report: a reprinted invoice total didn&rsquo;t match the original hard copy. Getting your financials wrong never feels good\u2026<\/p>\n<p>Fortunately, after restoring backups in a test database for far too long, I found the missing line item.<\/p>\n<p>Long story short, one of the administrators deleted a line item the was flagged as already invoiced, ie are you really <em>really<\/em> sure you want to delete this?<\/p>\n<p>Here&rsquo;s the problem. When this was originally built about 6 years ago, I strongly recommended not allowing it at all. The concern was that there might be a time where we must be able to delete something that we now think deleted. So I said fine, and it was built.<\/p>"},{"title":"Fixing this in event handlers","link":"https:\/\/jbennett.me\/articles\/fixing-this-in-event-handlers\/","pubDate":"Tue, 22 Apr 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/fixing-this-in-event-handlers\/","description":"<p>If you&rsquo;re using Stimulus or modern JavaScript classes, you\u2019ve probably hit this:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-js\" data-lang=\"js\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">import<\/span> { <span style=\"color:#a6e22e\">Controller<\/span> } <span style=\"color:#a6e22e\">from<\/span> <span style=\"color:#e6db74\">&#39;@hotwired\/stimulus&#39;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">export<\/span> <span style=\"color:#66d9ef\">default<\/span> <span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#66d9ef\">extends<\/span> <span style=\"color:#a6e22e\">Controller<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#a6e22e\">connect<\/span>() {\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">this<\/span>.<span style=\"color:#a6e22e\">element<\/span>.<span style=\"color:#a6e22e\">addEventListener<\/span>(<span style=\"color:#e6db74\">&#34;click&#34;<\/span>, <span style=\"color:#66d9ef\">this<\/span>.<span style=\"color:#a6e22e\">clicked<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t}\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#a6e22e\">clicked<\/span>() {\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#a6e22e\">console<\/span>.<span style=\"color:#a6e22e\">log<\/span>(<span style=\"color:#66d9ef\">this<\/span>) <span style=\"color:#75715e\">\/\/ Window?! It should be the controller.\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"><\/span>\t}\n<\/span><\/span><span style=\"display:flex;\"><span>}\n<\/span><\/span><\/code><\/pre><\/div><p>JavaScript loves to mess with this, and in this case, it\u2019s pointing to window, not your controller.<\/p>\n<h2 id=\"the-usual-fix-manual-binding\">The Usual Fix: Manual Binding<\/h2>\n<p>The usual advice is to bind your method in connect():<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-javascript\" data-lang=\"javascript\"><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">connect<\/span>() {\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">this<\/span>.<span style=\"color:#a6e22e\">clicked<\/span> <span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">this<\/span>.<span style=\"color:#a6e22e\">clicked<\/span>.<span style=\"color:#a6e22e\">bind<\/span>(<span style=\"color:#66d9ef\">this<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">this<\/span>.<span style=\"color:#a6e22e\">element<\/span>.<span style=\"color:#a6e22e\">addEventListener<\/span>(<span style=\"color:#e6db74\">&#34;click&#34;<\/span>, <span style=\"color:#66d9ef\">this<\/span>.<span style=\"color:#a6e22e\">clicked<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>}\n<\/span><\/span><\/code><\/pre><\/div><p>Sure, it works. But also&hellip; ew.<\/p>"},{"title":"A better way to handle reusable models","link":"https:\/\/jbennett.me\/articles\/a-better-way-to-handle-reusable-models\/","pubDate":"Mon, 21 Apr 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/a-better-way-to-handle-reusable-models\/","description":"<p>I recently saw a question on <a href=\"https:\/\/www.reddit.com\/r\/rails\/comments\/1jztl7z\">Reddit<\/a> asking how to structure a reusable (<a href=\"http:\/\/jbennett.me\/glossary\/#polymorphic-association\">polymorphic<\/a>) model <em>without<\/em> creating a mess of nearly duplicate routes and controllers:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># config\/routes.rb<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>resources <span style=\"color:#e6db74\">:accounts<\/span> <span style=\"color:#66d9ef\">do<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>  resources <span style=\"color:#e6db74\">:notes<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span>resources <span style=\"color:#e6db74\">:posts<\/span> <span style=\"color:#66d9ef\">do<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>  resources <span style=\"color:#e6db74\">:notes<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># ...repeat for every other model with notes<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># app\/controllers\/accounts\/notes_controller.rb<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># duplicate this for every noteable type<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">Accounts<\/span><span style=\"color:#f92672\">::<\/span><span style=\"color:#66d9ef\">NotesController<\/span> <span style=\"color:#f92672\">&lt;<\/span> <span style=\"color:#66d9ef\">ApplicationController<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">create<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t@account <span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">Account<\/span><span style=\"color:#f92672\">.<\/span>find(params<span style=\"color:#f92672\">[<\/span><span style=\"color:#e6db74\">:account_id<\/span><span style=\"color:#f92672\">]<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t@note <span style=\"color:#f92672\">=<\/span> @account<span style=\"color:#f92672\">.<\/span>notes<span style=\"color:#f92672\">.<\/span>build(note_params)\n<\/span><\/span><span style=\"display:flex;\"><span>\t@note<span style=\"color:#f92672\">.<\/span>save!\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span>\tredirect_to @account\n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#66d9ef\">private<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">note_params<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\tparams<span style=\"color:#f92672\">.<\/span>require(<span style=\"color:#e6db74\">:note<\/span>)<span style=\"color:#f92672\">.<\/span>permit(<span style=\"color:#e6db74\">:title<\/span>, <span style=\"color:#e6db74\">:body<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>They also expressed concern about exposing the related model IDs in the form, since those can be manipulated on the frontend.<\/p>"},{"title":"\u201cNew and Shiny\u201d is not a strategy","link":"https:\/\/jbennett.me\/articles\/new-and-shiny-is-not-a-strategy\/","pubDate":"Thu, 17 Apr 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/new-and-shiny-is-not-a-strategy\/","description":"<p>Beware of new and shiny<\/p>\n<p>\u2026<\/p>\n<p>\u2026<\/p>\n<p>Oh, you want more than that?<\/p>\n<p>Alright. \u201cNew and shiny\u201d isn\u2019t a benefit. It doesn\u2019t help your customer. It doesn\u2019t improve your bottom line. And it sure doesn\u2019t make your project easier to maintain in six months.<\/p>\n<p>New and shiny has to <em>earn<\/em> its place.<\/p>\n<p>Too often, \u201cnew and shiny\u201d really just means <em>different<\/em>. It\u2019s novelty for novelty\u2019s sake \u2014 and that novelty doesn\u2019t magically translate into value.<\/p>"},{"title":"Big releases are a defect","link":"https:\/\/jbennett.me\/articles\/big-releases-are-a-defect\/","pubDate":"Wed, 16 Apr 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/big-releases-are-a-defect\/","description":"<p>Back in the day, shipping software meant months of planning, long dev cycles, and maybe a few major releases a year.<\/p>\n<p>It rarely worked well.<\/p>\n<p>Modern software\u2014especially web apps\u2014has flipped that on its head. Frequent releases reduce risk. Some of the best teams ship a dozen times a day.<\/p>\n<p>When releases are small, you can actually understand what changed. And if something does go wrong, you\u2019re in a much better position to respond quickly and cleanly\u2014with a fix, not a fire drill.<\/p>"},{"title":"Over-managed, under-resolved","link":"https:\/\/jbennett.me\/articles\/over-managed-under-resolved\/","pubDate":"Tue, 15 Apr 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/over-managed-under-resolved\/","description":"<p>Things fall apart. Plans go sideways.<\/p>\n<p>When a software project starts to struggle\u2014slipping deadlines, slow performance, mysterious bugs\u2014the natural instinct is to <em>add management<\/em>. More meetings. More status updates. More reports.<\/p>\n<p>On a recent project, performance tanked. The app slowed to a crawl. A dozen status meetings were held. Several reporting formats were invented. People talked about moving off Rails.<\/p>\n<p>Turned out? It was a change in their network hardware. Nothing to do with the code at all.<\/p>"},{"title":"How to define your features","link":"https:\/\/jbennett.me\/articles\/how-to-define-features\/","pubDate":"Mon, 14 Apr 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/how-to-define-features\/","description":"<p>Non-technical founders often make the same mistakes when they first start working with developers. These aren\u2019t just annoying \u2014 they waste time, drain energy, and, worst of all, they don\u2019t actually create value.<\/p>\n<p>One of the biggest mistakes? Not having a clear definition of what you actually want. This leads to misaligned work, products that miss the mark, or over-engineered solutions that solve nothing.<\/p>\n<p>So what should you <em>actually<\/em> include? Here&rsquo;s my checklist:<\/p>"},{"title":"Sell the want, not the need","link":"https:\/\/jbennett.me\/articles\/sell-the-want-not-the-need\/","pubDate":"Fri, 11 Apr 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/sell-the-want-not-the-need\/","description":"<p>Convincing someone to buy something they <em>need<\/em> but don\u2019t <em>want<\/em> is a losing game.<\/p>\n<ul>\n<li>\u201cI need to quit smoking,\u201d they say\u2014while lighting up.<\/li>\n<li>\u201cI need to lose weight,\u201d they say\u2014while in the McDonald\u2019s drive-thru.<\/li>\n<li>\u201cI need to spend more time with the kids,\u201d they say\u2014while turning on the game.<\/li>\n<\/ul>\n<p>We\u2019ve all been that person. But this isn\u2019t a self-help post.<\/p>\n<p>It\u2019s about customer development.<\/p>\n<p>If you\u2019re picking a problem to solve, don\u2019t choose something your customers <em>should<\/em> care about. Choose something they <strong>already do<\/strong>.<\/p>"},{"title":"Features are a trap","link":"https:\/\/jbennett.me\/articles\/features-are-a-trap\/","pubDate":"Thu, 10 Apr 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/features-are-a-trap\/","description":"<p>I\u2019ve started planning a new side project.<\/p>\n<p>Its most obvious competitor has thousands of employees and is worth billions of dollars.<\/p>\n<p>So no, I\u2019m not going to compete on features.<\/p>\n<p>Instead, I\u2019m focused on figuring out the small set of features my customer actually uses \u2014 and making those feel tighter, faster, and better integrated than the big guy\u2019s version.<\/p>\n<p>The best way to figure that out?\nAsk people <em>how they actually use<\/em> the tools they already rely on.<\/p>"},{"title":"What makes code \u201cgood\u201d?","link":"https:\/\/jbennett.me\/articles\/what-makes-code-good\/","pubDate":"Wed, 09 Apr 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/what-makes-code-good\/","description":"<p>So if working code isn\u2019t the goal\u2014what is?<\/p>\n<p>Here\u2019s my take:<\/p>\n<ol>\n<li>Does it solve today\u2019s problem?<\/li>\n<li>Can it be adapted to tomorrow\u2019s problem?<\/li>\n<\/ol>\n<p>If your code checks both boxes, congrats\u2014it\u2019s good. Maybe not perfect. Maybe the test suite is thin. Maybe <a href=\"https:\/\/jbennett.me\/glossary\/#cyclomatic-complexity\">cyclomatic complexity<\/a> is creeping up. Those are potential problems for future-you, sure\u2014but not dealbreakers today.<\/p>\n<p>The code doesn\u2019t need to be pretty, it needs to be <em>practical<\/em>.\nSolve the problem. Don\u2019t block future progress. That\u2019s enough.<\/p>"},{"title":"Your code doesn\u2019t matter","link":"https:\/\/jbennett.me\/articles\/your-code-doesnt-matter\/","pubDate":"Tue, 08 Apr 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/your-code-doesnt-matter\/","description":"<p>Here&rsquo;s the dirty secret developers don&rsquo;t want you to know:<\/p>\n<p><strong>Your code doesn\u2019t really matter.<\/strong><\/p>\n<p>Sure, it can be <em>bad<\/em> enough to cause problems. But as long as your code hits a basic level of quality (and that bar is lower than you think), it won\u2019t be the thing that kills your startup.<\/p>\n<p>The number one killer of early products isn\u2019t broken code.<\/p>\n<p>It\u2019s customer apathy.<\/p>\n<p>They just don\u2019t care.<\/p>"},{"title":"Automatically using environment specific images","link":"https:\/\/jbennett.me\/articles\/automatically-using-environment-specific-images\/","pubDate":"Mon, 07 Apr 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/automatically-using-environment-specific-images\/","description":"<p>It&rsquo;s pretty common to want to use different assets, icons etc, in different environments \u2013 a development, staging, and production icon for example.<\/p>\n<p>One way I&rsquo;ve done this is by using different variants with the <a href=\"https:\/\/github.com\/Rails-Designer\/rails_icons\">rails_icons<\/a> gem for each environment, and then setting the default variant dynamically. File paths look like:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-plain\" data-lang=\"plain\"><span style=\"display:flex;\"><span>app\/assets\/svg\/icons\/marketing\/development\/logo.svg\n<\/span><\/span><span style=\"display:flex;\"><span>app\/assets\/svg\/icons\/marketing\/staging\/logo.svg\n<\/span><\/span><span style=\"display:flex;\"><span>app\/assets\/svg\/icons\/marketing\/production\/logo.svg\n<\/span><\/span><\/code><\/pre><\/div><p>And for the custom library setup, I include:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># config\/initializers\/rails_icons.rb<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">RailsIcons<\/span><span style=\"color:#f92672\">.<\/span>configure <span style=\"color:#66d9ef\">do<\/span> <span style=\"color:#f92672\">|<\/span>config<span style=\"color:#f92672\">|<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#75715e\"># heroicon config etc<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\n<\/span><\/span><span style=\"display:flex;\"><span>\tconfig<span style=\"color:#f92672\">.<\/span>libraries<span style=\"color:#f92672\">.<\/span>merge!({\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#e6db74\">marketing<\/span>: {\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\t<span style=\"color:#e6db74\">default_variant<\/span>: <span style=\"color:#66d9ef\">Rails<\/span><span style=\"color:#f92672\">.<\/span>env,\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t}\n<\/span><\/span><span style=\"display:flex;\"><span>\t})\n<\/span><\/span><\/code><\/pre><\/div><p>By using the Rails environment as the default variant, this means your assets usage will automatically switch for you.<\/p>"},{"title":"Test First, Stress Less","link":"https:\/\/jbennett.me\/articles\/test-first-stress-less\/","pubDate":"Fri, 04 Apr 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/test-first-stress-less\/","description":"<p>If you&rsquo;ve ever hesitated to update your MVP\u2014or held your breath while deploying \u201cjust one little change\u201d\u2014you really need to consider Test Driven Development.<\/p>\n<p>Test Driven Development (TDD) is a technique where developers write a test before they write the code. Then they write just enough code to make that test pass. Not more, not less.<\/p>\n<p>This simple habit changes everything.<\/p>\n<p>When done right:<\/p>\n<ul>\n<li>Developers don\u2019t write extra code that nobody asked for.<\/li>\n<li>Every feature is proven to work the moment it\u2019s built.<\/li>\n<li>Updates don\u2019t feel like rolling the dice.<\/li>\n<\/ul>\n<p>You don\u2019t need to understand the internals to appreciate the results. With TDD in place:<\/p>"},{"title":"Waiting on Turbo the right way","link":"https:\/\/jbennett.me\/articles\/waiting-on-turbo-the-right-way\/","pubDate":"Thu, 03 Apr 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/waiting-on-turbo-the-right-way\/","description":"<p>Quick tip for waiting on <code>&lt;form&gt;<\/code> submission when using Turbo:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">SomethingTest<\/span> <span style=\"color:#f92672\">&lt;<\/span> <span style=\"color:#66d9ef\">ApplicationSystemTestCase<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\ttest <span style=\"color:#e6db74\">&#34;something works&#34;<\/span> <span style=\"color:#66d9ef\">do<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\tvisit something_path\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#75715e\"># this might automatically submit<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\tfill_in <span style=\"color:#e6db74\">&#34;Name&#34;<\/span>, <span style=\"color:#e6db74\">with<\/span>: <span style=\"color:#e6db74\">&#34;Jonathan&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t\tsleep <span style=\"color:#ae81ff\">10<\/span> <span style=\"color:#75715e\"># pure magic!<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t\tassert_selector <span style=\"color:#e6db74\">&#34;h1&#34;<\/span>, <span style=\"color:#e6db74\">text<\/span>: <span style=\"color:#e6db74\">&#34;Jonathan&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>Okay, sure. You could toss a random <code>sleep 10<\/code> into your tests and go get coffee. But don\u2019t.<\/p>\n<h2 id=\"the-right-way\">The Right Way<\/h2>\n<p>Turbo adds <code>aria-busy=&quot;true&quot;<\/code> to forms while they\u2019re submitting. You can use this to wait for the submission to start and finish:<\/p>"},{"title":"Do you have the right technical expertise on your team?","link":"https:\/\/jbennett.me\/articles\/do-you-have-the-right-tehnical-expertise-on-your-team\/","pubDate":"Wed, 02 Apr 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/do-you-have-the-right-tehnical-expertise-on-your-team\/","description":"<blockquote>\n<p>There&rsquo;s a lack of knowledge on the business side to know what to ask for, and a lack of incentive for the off-shore team to provide what is best for the business.<br>\n\u2013 <a href=\"https:\/\/overcast.fm\/+ABNMnWnuBXk\/8:27\">Interview on Finest City Digital Podcast<\/a><\/p><\/blockquote>\n<p>Having an experienced technical advisor that is solely focused on your business success can pay pay dividends. That&rsquo;s peace of mind is what you are purchasing with a <a href=\"https:\/\/jbennett.me\/services\/strategy-retainer\">strategy retainer<\/a>.<\/p>\n<p>You can listen to the entire interview <a href=\"https:\/\/overcast.fm\/+ABNMnWnuBXk\">here<\/a><\/p>"},{"title":"Don't cut corners on the essentials","link":"https:\/\/jbennett.me\/articles\/dont-cut-corners-on-the-essentials\/","pubDate":"Tue, 01 Apr 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/dont-cut-corners-on-the-essentials\/","description":"<p>Cutting corners when painting your house won&rsquo;t cause irreparable harm. Cutting corners on your foundation however might.<\/p>\n<p>Make sure when you are setting up the foundation of your SaaS that you aren&rsquo;t cutting corners you&rsquo;ll regret in the future.<\/p>"},{"title":"Make the Right Changes, Not the Perfect Product","link":"https:\/\/jbennett.me\/articles\/make-the-right-changes-not-the-perfect-product\/","pubDate":"Mon, 31 Mar 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/make-the-right-changes-not-the-perfect-product\/","description":"<p><em>Turns out I should have double checked that my email was setup right before heading out on a road trip. Sorry for the lack of detail on Friday. \ud83e\udd26\ud83c\udffb\u200d\u2642\ufe0f<\/em><\/p>\n<hr>\n<p>I&rsquo;ve seen so many founders pour time and energy into building the perfect product, convinced that customers will love it and buy it.<\/p>\n<p>But more often than not, customers don\u2019t love it\u2014and they don\u2019t buy it.<\/p>\n<p>Success doesn\u2019t come from getting it perfect on the first try. It comes from making the right changes\u2014whatever is needed\u2014to ensure the product truly solves a problem and gets in front of the right people.<\/p>"},{"title":"Shiny Features vs. Stability: Why You Need Both","link":"https:\/\/jbennett.me\/articles\/shiny-features-vs-stability\/","pubDate":"Thu, 27 Mar 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/shiny-features-vs-stability\/","description":"<p>Software development isn\u2019t a straight path of constant progress\u2014it moves in cycles. A period of <strong>rapid development<\/strong> is often followed by a phase of <strong>maintenance and improvement<\/strong>. Understanding and embracing this cycle is crucial for long-term success.<\/p>\n<h2 id=\"the-rapid-development-phase\">The Rapid Development Phase<\/h2>\n<p>At the start of a project or major feature push, development moves fast. New features roll out, and visible progress is easy to measure. This phase is exciting but also introduces technical debt\u2014shortcuts taken to move quickly that must be addressed later.<\/p>"},{"title":"SolidQueue + Cron? There's a Better Way","link":"https:\/\/jbennett.me\/articles\/solid-queue-cron\/","pubDate":"Wed, 26 Mar 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/solid-queue-cron\/","description":"<p>A few months ago <a href=\"https:\/\/jbennett.me\/articles\/kamal-cron-theres-a-better-way\/\">I showed<\/a> how to schedule recurring rake tasks with Clock. A few of my projects have started using SolidQueue more extensively, including it&rsquo;s support for recurring jobs.<\/p>\n<p>Rake tasks could be run hourly with Clock by using the following:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span>using <span style=\"color:#66d9ef\">RubyClock<\/span><span style=\"color:#f92672\">::<\/span><span style=\"color:#66d9ef\">DSL<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span>cron <span style=\"color:#e6db74\">&#39;0 * * * *&#39;<\/span> <span style=\"color:#66d9ef\">do<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>  rake <span style=\"color:#e6db74\">&#39;my_rake_task&#39;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>With SolidQueue, we can schedule in a similar way:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-yaml\" data-lang=\"yaml\"><span style=\"display:flex;\"><span><span style=\"color:#f92672\">production<\/span>:\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#f92672\">my_task<\/span>:\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#f92672\">class<\/span>: <span style=\"color:#ae81ff\">MyJobClass<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#f92672\">schedule<\/span>: <span style=\"color:#e6db74\">&#34;0 * * * *&#34;<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>The <code>my_task<\/code> key uniquely identifies the task and is how SolidQueue tracks job completing and scheduling. You can see more documentation in the <a href=\"https:\/\/github.com\/rails\/solid_queue?tab=readme-ov-file#recurring-tasks\">SolidQueue readme<\/a>.<\/p>"},{"title":"Here's how to avoid 3 weeks of pain","link":"https:\/\/jbennett.me\/articles\/heres-how-to-avoid-3-weeks-of-pain\/","pubDate":"Tue, 25 Mar 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/heres-how-to-avoid-3-weeks-of-pain\/","description":"<p>Pro tip, make sure you know what Kamal is doing behind the scenes to avoid a lot of pain\u2026<\/p>\n<p>For a recent project I was using <a href=\"https:\/\/github.com\/rails\/solid_queue\"><code>solid_queue<\/code><\/a> for background jobs and <a href=\"https:\/\/github.com\/basecamp\/kamal\">Kamal<\/a> for deployment. Everything worked perfectly\u2014until it didn\u2019t.<\/p>\n<p>Some of my jobs, Turbo broadcasts, would run just fine, but my custom jobs and notifications using <a href=\"https:\/\/github.com\/excid3\/noticed\">Noticed<\/a> were throwing <code>NameError<\/code>s. Strangely, when I connected to the jobs server, I could run them manually without issues.<\/p>"},{"title":"Submitting partial forms","link":"https:\/\/jbennett.me\/articles\/submitting-partial-forms\/","pubDate":"Mon, 24 Mar 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/submitting-partial-forms\/","description":"<p><a href=\"https:\/\/jbennett.me\/articles\/rewriting-form-action-and-method\">Last time<\/a>, we looked at having a specific <code>button<\/code> in a form trigger a different action and method than the <code>form<\/code>&rsquo;s attributes dictate.<\/p>\n<p>One way this can be used is to submit the form back to the <code>new<\/code> action, and make adjustments on the server. This is a great way to dynamically load data if you have dependant fields like a country and a province\/state:<\/p>\n<pre tabindex=\"0\"><code class=\"language-erb\" data-lang=\"erb\">&lt;%= form_with model: @account do |form| %&gt;\n\t&lt;!-- ... --&gt;\n\t\n\t&lt;div&gt;\n\t\t&lt;%= form.label :county %&gt;\n\t\t&lt;%= form.select :country, options_for_country(@account.country) %&gt;\n\t\t\n\t\t&lt;%= form.submit &#34;Update Options&#34;, \n\t\t\tformaction: new_account_path, \n\t\t\tformmethod: :get %&gt;\n\t&lt;\/div&gt;\n\t\n\t&lt;div&gt;\n\t\t&lt;%= form.label :province %&gt;\n\t\t&lt;%= form.select :province, \n\t\t\toptions_for_province(@account.country, @account.province) %&gt;\n\t&lt;\/div&gt;\n\t\n\t&lt;!-- ... --&gt;\n&lt;% end %&gt;\n<\/code><\/pre><p>If you click the &ldquo;Update Options&rdquo; button, instead of sending the form data to the <code>create<\/code> action, it will be sent to the <code>new<\/code> action:<\/p>"},{"title":"Rewriting form action and method","link":"https:\/\/jbennett.me\/articles\/rewriting-form-action-and-method\/","pubDate":"Fri, 21 Mar 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/rewriting-form-action-and-method\/","description":"<p>Did you know that buttons in forms can override the <code>method<\/code> and <code>action<\/code> of its form?<\/p>\n<pre tabindex=\"0\"><code class=\"language-erb\" data-lang=\"erb\">&lt;%= form_with url: search_path do |form| %&gt;\n\t&lt;%= form.text_field :q %&gt;\n\t\n\t&lt;%= form.submit &#34;Internal Search&#34; %&gt;\n\t&lt;%= form.submit &#34;Google Search&#34;, formmethod: :get, formaction: &#34;https:\/\/google.com&#34; %&gt;\n&lt;% end %&gt;\n<\/code><\/pre><p>This is a little contrived of an example, but using this approach allows for some very elegant use of Hotwire\u2026 next time.<\/p>"},{"title":"View oragnization showdown: View Components vs Partials","link":"https:\/\/jbennett.me\/articles\/view-components-vs-partials\/","pubDate":"Thu, 20 Mar 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/view-components-vs-partials\/","description":"<p>Ever wondered when to use a partial vs a component? As usual, it <a href=\"https:\/\/jbennett.me\/glossary\/#it-depends\">depends<\/a> but I generally fall into one of two camps on this topic:<\/p>\n<ol>\n<li>All general UI elements are components and all app specific views are partials that use the components. This would give you <code>Button<\/code>, <code>Panel<\/code>, and <code>Modal<\/code> components which would be used in a <code>posts\/_post.html.erb<\/code> partial.<\/li>\n<li>Everything is a component. The only partials are things that directly integrate with Rails, and even these should be a thin wrapper around Components. This would look like:<\/li>\n<\/ol>\n<pre tabindex=\"0\"><code class=\"language-erb\" data-lang=\"erb\">&lt;!-- app\/views\/posts\/index.html.erb --&gt;\n&lt;%= render partial: &#34;posts\/post&#34;, collection: @posts %&gt;\n\n&lt;!-- app\/views\/posts\/_post.html.erb --&gt;\n&lt;%= render Posts::CardComponent.new(post) %&gt;\n<\/code><\/pre><p>I haven&rsquo;t yet decided which option is the silver bullet. How about yourself? How do you organize your views?<\/p>"},{"title":"How to connect the dots in your relationships","link":"https:\/\/jbennett.me\/articles\/how-to-connect-the-dots-in-your-relationships\/","pubDate":"Wed, 19 Mar 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/how-to-connect-the-dots-in-your-relationships\/","description":"<p>One of the things that makes Rails &ldquo;magical&rdquo; is that it&rsquo;s default behaviour is most likely what you would want. One place that this <em>doesn&rsquo;t<\/em> happen is when connecting both sides of an association when it isn&rsquo;t following the normal Rails conventions. An example of this is a self referential association:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">User<\/span> <span style=\"color:#f92672\">&lt;<\/span> <span style=\"color:#66d9ef\">ApplicationRecord<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>  has_many <span style=\"color:#e6db74\">:subordinates<\/span>, <span style=\"color:#e6db74\">class_name<\/span>: <span style=\"color:#e6db74\">&#34;User&#34;<\/span>, <span style=\"color:#e6db74\">foreign_key<\/span>: <span style=\"color:#e6db74\">:manager_id<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>  belongs_to <span style=\"color:#e6db74\">:manager<\/span>, <span style=\"color:#e6db74\">class_name<\/span>: <span style=\"color:#e6db74\">&#34;User&#34;<\/span>, <span style=\"color:#e6db74\">optional<\/span>: <span style=\"color:#66d9ef\">true<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span>manager <span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">User<\/span><span style=\"color:#f92672\">.<\/span>new\n<\/span><\/span><span style=\"display:flex;\"><span>employee <span style=\"color:#f92672\">=<\/span> manager<span style=\"color:#f92672\">.<\/span>subordinates<span style=\"color:#f92672\">.<\/span>build\n<\/span><\/span><span style=\"display:flex;\"><span>puts employee<span style=\"color:#f92672\">.<\/span>manager <span style=\"color:#75715e\"># nil ?!?<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>Well that won&rsquo;t do. Whats going wrong?<\/p>"},{"title":"All low code tools are dead ends","link":"https:\/\/jbennett.me\/articles\/all-low-code-tools-are-dead-ends\/","pubDate":"Tue, 18 Mar 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/all-low-code-tools-are-dead-ends\/","description":"<p>All low-code tools are dead ends\u2014eventually, you\u2019ll hit a limitation you can&rsquo;t work around. And that\u2019s okay. Most SaaS ideas are also dead ends.<\/p>\n<p>If a tool gets you to that dead end more quickly, that\u2019s a good thing. Think of all the time and money you\u2019ve saved!<\/p>\n<p>The key isn\u2019t whether you\u2019re using a low-code tool or not\u2014it\u2019s whether you\u2019re validating your idea with potential customers. If you\u2019ve validated your idea using a low-code tool, you can move forward with a coded solution with confidence.<\/p>"},{"title":"Appearance: Finest City Digital Podcast","link":"https:\/\/jbennett.me\/articles\/appearance-finest-city-digital-podcast\/","pubDate":"Mon, 17 Mar 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/appearance-finest-city-digital-podcast\/","description":"<p>I recently had a conversation with Feargal Walsh for the Finest City podcast. Topics include:<\/p>\n<ul>\n<li>working with off-shore teams<\/li>\n<li>what non-technical founders typically need<\/li>\n<li>how to compete against a team as a solo-developer<\/li>\n<li>side projects<\/li>\n<\/ul>\n<p><a href=\"https:\/\/overcast.fm\/+ABNMnWnuBXk\">Check it out<\/a><\/p>"},{"title":"How to recover a lost Rails master key","link":"https:\/\/jbennett.me\/articles\/how-to-recover-a-lost-rails-master-key\/","pubDate":"Fri, 14 Mar 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/how-to-recover-a-lost-rails-master-key\/","description":"<p>You know that sinking feeling when you accidentally delete your <code>master.key<\/code> file and need to rebuild your local set of credentials\u2026 well that was going to be my morning.<\/p>\n<p>Fortunately Rails apps only reload configuration when the server is restarted. That means the key should be somewhere in memory.<\/p>\n<p>One <code>puts Rails.application.credentials.key<\/code> and I was back in business.<\/p>\n<p>PS Keep a copy of your key, especially for production, in your password manager. Future you will greatly appreciate it.<\/p>"},{"title":"Ruby on Rails in 2025","link":"https:\/\/jbennett.me\/articles\/rails-in-2025\/","pubDate":"Thu, 13 Mar 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/rails-in-2025\/","description":"<p>Rails has a ton of competition with other frameworks, but here&rsquo;s 3 reasons to choose Rails in 2025:<\/p>\n<ol>\n<li><strong>Developer Productivity:<\/strong> So much of how Rails works is based on making your development teams as productive as possible. Good default settings and a healthy ecosystem with standard tools all contribute to allowing developers to move quickly.<\/li>\n<li><strong>Cost:<\/strong> Out-of-the-box tools like <a href=\"https:\/\/kamal-deploy.org\">Kamal<\/a> give you options for how you deploy your application. This provides a pathway from running on a platform like <a href=\"https:\/\/heroku.com\">Heroku<\/a>, moving to <a href=\"https:\/\/aws.amazon.com\">AWS<\/a>, and growing into your own infrastructure if desired.<\/li>\n<li><strong>Full Stack Framework:<\/strong> Rails includes everything you need to build your backend, frontend , and even deploy to native devices. With Rails, you don&rsquo;t need to include a completely separate ecosystem like React native your you don&rsquo;t want to. This has a the distinct benefit of highly leveraging your developers as they are less constrained in what they can work on.<\/li>\n<\/ol>\n<p>What do you think? Is Rails meeting your needs or is there anything missing?<\/p>"},{"title":"RAILS_ASSET_URL is chef's kiss","link":"https:\/\/jbennett.me\/articles\/rails_asset_url-is-chefs-kiss\/","pubDate":"Wed, 12 Mar 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/rails_asset_url-is-chefs-kiss\/","description":"<p>I was in the <a href=\"https:\/\/discord.com\/invite\/gorails\">GoRails Discord<\/a> and <a href=\"https:\/\/discord.com\/channels\/874684608686477352\/874687644108488704\/1348322402794471517\">@Sean posted<\/a> about <code>RAILS_ASSET_URL<\/code>. This is part of the Propshaft pipeline and let&rsquo;s you access your assets without needing their digest hash. ie <code>this.img = RAILS_ASSET_URL(&quot;\/image.png&quot;)<\/code> becomes <code>this.img = &quot;\/assets\/image-28sd34.png&quot;<\/code> once your javascript is served.<\/p>\n<p>I&rsquo;ve needed something like this a few times so this is just wonderful to have. Thanks for sharing @Sean.<\/p>"},{"title":"Paginate in Turbo Mode - Part 3: Which to Use","link":"https:\/\/jbennett.me\/articles\/paginate-in-turbo-mode-which-to-pick\/","pubDate":"Tue, 11 Mar 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/paginate-in-turbo-mode-which-to-pick\/","description":"<p>The last couple posts show two options for doing pagination <a href=\"https:\/\/jbennett.me\/articles\/paginate-in-turbo-mode-part-1\/\">normal link based<\/a> and <a href=\"https:\/\/jbennett.me\/articles\/paginate-in-turbo-mode-infinite-scroll\/\">infinite scroll based<\/a>. The obvious question is which should you use? The standard answer, as always, is <a href=\"https:\/\/jbennett.me\/glossary\/#it-depends\">it depends<\/a>.<\/p>\n<p>Link based pagination is the &ldquo;normal&rdquo; and is expected by most users. This also makes excellent use of web affordances, giving you proper navigation history and bookmarkable pages for free.<\/p>\n<p>Infinite scroll pagination does give you a cool &ldquo;shininess&rdquo; factor that is not present with normal linked based pagination. However, it does have a tradeoff of losing the current page when navigating and not having linkable pages out of the box. Building in these affordances can be complicated to get right and feel as natural as the default option you get with link based pagination.<\/p>"},{"title":"Paginate in Turbo Mode - Part 2: Infinite Scroll","link":"https:\/\/jbennett.me\/articles\/paginate-in-turbo-mode-infinite-scroll\/","pubDate":"Mon, 10 Mar 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/paginate-in-turbo-mode-infinite-scroll\/","description":"<p>An alternative to next\/previous style pagination is to have infinite scrolling. With infinite scroll, once you hit the bottom of the page, you load in the next set of results. Again, Turbo Stream makes this very achievable.<\/p>\n<p>This is done by:<\/p>\n<ol>\n<li>Loading in the first set of results.<\/li>\n<li>Adding a lazy loading <code>&lt;turbo-frame&gt;<\/code> that will return the next set of results.<\/li>\n<li>Append the new results and replace the lazy <code>&lt;turbo-frame&gt;<\/code> with a new one that will load the next page.<\/li>\n<\/ol>\n<p>The updated <code>index.html.erb<\/code> that will accomplish steps 1 and 2 above will look something like:<\/p>"},{"title":"Paginate in Turbo Mode - Part 1","link":"https:\/\/jbennett.me\/articles\/paginate-in-turbo-mode-part-1\/","pubDate":"Fri, 07 Mar 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/paginate-in-turbo-mode-part-1\/","description":"<p>Replacing part of the screen using a <a href=\"https:\/\/turbo.hotwired.dev\/reference\/frames\">Turbo Frame<\/a> is a great way to keep your application more responsive. Lets add pagination without modifying the rest of the page as an example. We&rsquo;ll start by setting up a basic page with pagination:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-plain\" data-lang=\"plain\"><span style=\"display:flex;\"><span>rails new pagination\n<\/span><\/span><span style=\"display:flex;\"><span>cd pagination\n<\/span><\/span><span style=\"display:flex;\"><span>bundle add faker\n<\/span><\/span><span style=\"display:flex;\"><span>rails g model post title:string!\n<\/span><\/span><span style=\"display:flex;\"><span>rails db:migrate\n<\/span><\/span><span style=\"display:flex;\"><span>rails runner &#34;1000.times { Post.create title: Faker::Book.title }&#34;\n<\/span><\/span><\/code><\/pre><\/div><br>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">PostsController<\/span> <span style=\"color:#f92672\">&lt;<\/span> <span style=\"color:#66d9ef\">ApplicationController<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">index<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#75715e\"># Dont do this. Use something like the pagy gem<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t@count <span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">Post<\/span><span style=\"color:#f92672\">.<\/span>all<span style=\"color:#f92672\">.<\/span>count\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t@limit <span style=\"color:#f92672\">=<\/span> <span style=\"color:#ae81ff\">10<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t@current_page <span style=\"color:#f92672\">=<\/span> params<span style=\"color:#f92672\">[<\/span><span style=\"color:#e6db74\">:page<\/span><span style=\"color:#f92672\">].<\/span>to_i\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t@prev_page <span style=\"color:#f92672\">=<\/span> @current_page <span style=\"color:#f92672\">&gt;<\/span> <span style=\"color:#ae81ff\">0<\/span> <span style=\"color:#f92672\">?<\/span> @current_page <span style=\"color:#f92672\">-<\/span> <span style=\"color:#ae81ff\">1<\/span> : <span style=\"color:#66d9ef\">nil<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t@next_page <span style=\"color:#f92672\">=<\/span> @count <span style=\"color:#f92672\">\/<\/span> @limit <span style=\"color:#f92672\">-<\/span> <span style=\"color:#ae81ff\">1<\/span> <span style=\"color:#f92672\">&gt;<\/span> @current_page ? @current_page <span style=\"color:#f92672\">+<\/span> <span style=\"color:#ae81ff\">1<\/span> : <span style=\"color:#66d9ef\">nil<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t@posts <span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">Post<\/span><span style=\"color:#f92672\">.<\/span>all\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\t<span style=\"color:#f92672\">.<\/span>limit(@limit)<span style=\"color:#f92672\">.<\/span>offset(@current_page <span style=\"color:#f92672\">*<\/span> @limit)\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\t<span style=\"color:#f92672\">.<\/span>order(<span style=\"color:#e6db74\">title<\/span>: <span style=\"color:#e6db74\">:asc<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><br>\n<pre tabindex=\"0\"><code class=\"language-erb\" data-lang=\"erb\">&lt;!-- app\/view\/posts\/index.html.erb --&gt;\n&lt;h1&gt;Posts&lt;\/h1&gt;\n\n&lt;p&gt;&lt;%= rand %&gt;&lt;\/p&gt;\n\n&lt;%= render @posts %&gt;\n\n&lt;% if @prev_page %&gt;\n  &lt;%= link_to &#34;Prev&#34;, posts_path(page: @prev_page) %&gt;\n&lt;% end %&gt;\n\n&lt;% if @next_page %&gt;\n  &lt;%= link_to &#34;Next&#34;, posts_path(page: @next_page) %&gt;\n&lt;% end %&gt;\n<\/code><\/pre><p>This is basic offset pagination. In a real application you would want to use something like <a href=\"https:\/\/github.com\/ddnexus\/pagy\">Pagy<\/a> for a number of reasons (performance, edge cases, nicer interface), but to keep this simple we&rsquo;ll keep this all here. The random number is just to give us something simple on the page that will update on each load.<\/p>"},{"title":"Local AI: The Movie","link":"https:\/\/jbennett.me\/articles\/local-ai-the-movie\/","pubDate":"Thu, 06 Mar 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/local-ai-the-movie\/","description":"<p>So, I made a video running through the Local AI post series. If you&rsquo;d rather listen to my voice than read my words, this is where you can do it:<\/p>\n<p><a href=\"https:\/\/www.youtube.com\/watch?v=sa5poQpQtwE\"><img src=\"https:\/\/jbennett.me\/images\/local-ai.png\" alt=\"Link to Youtube video of Local AI tutorial.\"><\/a><\/p>\n<p>If you want to re-read any of the posts they are:<\/p>\n<ul>\n<li><a href=\"https:\/\/jbennett.me\/articles\/local-ai-getting-up-and-running\/\">Getting up and running<\/a><\/li>\n<li><a href=\"https:\/\/jbennett.me\/articles\/local-ai-connecting-rails-to-ollama\/\">Connecting Rails to Ollama<\/a><\/li>\n<li><a href=\"https:\/\/jbennett.me\/articles\/wiring-up-the-conversation-model\/\">Wiring Up the Conversation Model with Rails<\/a><\/li>\n<li><a href=\"https:\/\/jbennett.me\/articles\/local-ai-improving-response-time\/\">Improving response time<\/a><\/li>\n<li><a href=\"https:\/\/jbennett.me\/articles\/local-ai-streaming-ai-responses\/\">Streaming AI Responses<\/a><\/li>\n<li><a href=\"https:\/\/jbennett.me\/articles\/local-ai-adding-system-prompts\/\">Adding system prompts<\/a><\/li>\n<li><a href=\"https:\/\/jbennett.me\/articles\/user-experience-improvements\/\">User Experience Improvements<\/a><\/li>\n<\/ul>"},{"title":"`class_names` is the bees knees","link":"https:\/\/jbennett.me\/articles\/class_names-is-the-bees-knees\/","pubDate":"Wed, 05 Mar 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/class_names-is-the-bees-knees\/","description":"<p><code>class_names<\/code> might be my favourite view helper in a long time. It turns ugly, messy CSS into something that can ready just as nicely as your Ruby code. Well, nearly:<\/p>\n<p>Before:<\/p>\n<pre tabindex=\"0\"><code class=\"language-erb\" data-lang=\"erb\">&lt;%= tag.div class: &#34;bg-gray-300 #{ &#34;border-2&#34; if post.draft? } #{ &#34;border-red-500&#34; if post.invalid? }&#34; do %&gt;\n\t\u2026\n&lt;% end %&gt;\n<\/code><\/pre><p>After:<\/p>\n<pre tabindex=\"0\"><code class=\"language-erb\" data-lang=\"erb\">&lt;%= tag.div class: [\n\t&#34;bg-gray-300&#34;,\n\t&#34;border-2&#34;: post.draft?,\n   \t&#34;border-red-500&#34;: post.invalid?\n   ] do %&gt;\n\t\u2026\n&lt;% end %&gt;\n<\/code><\/pre><p>Now I know what you&rsquo;re thinking, &ldquo;You said you liked <code>class_names<\/code>?&rdquo; Yup. And the <code>class<\/code> attribute uses it internal so you don&rsquo;t need to use it directly! Look how helpful Rails is.<\/p>"},{"title":"Local AI: User Experience Improvements","link":"https:\/\/jbennett.me\/articles\/user-experience-improvements\/","pubDate":"Tue, 04 Mar 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/user-experience-improvements\/","description":"<p>One last tweak to our system should help users have just a bit more insight into what the AI system is doing and<\/p>\n<p>Our system now streams in responses and allows configurable system prompts, but it&rsquo;s missing an affordance to indicate that the AI is done. Fortunately, we can add that. We will add a boolean <code>done<\/code> field to the message models. On AI responses, it will start false and will be updated. We can use that to provide visible feedback to the user:<\/p>"},{"title":"Local AI: Adding system prompts","link":"https:\/\/jbennett.me\/articles\/local-ai-adding-system-prompts\/","pubDate":"Sat, 01 Mar 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/local-ai-adding-system-prompts\/","description":"<p>Ok, so last time I said the last thing we needed was streaming responses. Well now, the last last thing we need is to seed out conversation with a system prompt.<\/p>\n<p>What is a system prompt you ask? A system prompt let&rsquo;s us setup the initial behaviour of the AI assistant. A good prompt will put the AI into proof editor mode or business plan expert mode for instance. By making this a first class citizen in our system, we can offer a ton of flexibility to our users.<\/p>"},{"title":"Local AI: Streaming AI Responses","link":"https:\/\/jbennett.me\/articles\/local-ai-streaming-ai-responses\/","pubDate":"Fri, 28 Feb 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/local-ai-streaming-ai-responses\/","description":"<p>We are close to having a basic, working local AI system. The last technically issue to address is streaming the response. To stream a response, we need to enable <code>server_sent_events<\/code> on our client:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># app\/models\/conversation.rb<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">ollama_client<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t@client <span style=\"color:#f92672\">||=<\/span> <span style=\"color:#66d9ef\">Ollama<\/span><span style=\"color:#f92672\">.<\/span>new(\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#e6db74\">credentials<\/span>: { <span style=\"color:#e6db74\">address<\/span>: <span style=\"color:#e6db74\">&#34;http:\/\/localhost:11434&#34;<\/span> },\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#e6db74\">options<\/span>: { <span style=\"color:#e6db74\">server_sent_events<\/span>: <span style=\"color:#66d9ef\">true<\/span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\t)\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>Next we will want to break the thread updating into creation of the new message, and updating the message:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">Conversation<\/span> <span style=\"color:#f92672\">&lt;<\/span> <span style=\"color:#66d9ef\">ApplicationRecord<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">send_thread<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#75715e\"># collect the preceding messages<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\tmessages <span style=\"color:#f92672\">=<\/span> self<span style=\"color:#f92672\">.<\/span>messages<span style=\"color:#f92672\">.<\/span>order(<span style=\"color:#e6db74\">:created_at<\/span>)<span style=\"color:#f92672\">.<\/span>map { {\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t  <span style=\"color:#e6db74\">role<\/span>: _1<span style=\"color:#f92672\">.<\/span>role,\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t  <span style=\"color:#e6db74\">content<\/span>: _1<span style=\"color:#f92672\">.<\/span>message\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t} }\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#75715e\"># create the blank initial response<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\tresponse <span style=\"color:#f92672\">=<\/span> self<span style=\"color:#f92672\">.<\/span>messages<span style=\"color:#f92672\">.<\/span>create({\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t  <span style=\"color:#e6db74\">role<\/span>: <span style=\"color:#e6db74\">&#34;assistant&#34;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t  <span style=\"color:#e6db74\">message<\/span>: <span style=\"color:#e6db74\">&#34;&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t})\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t\tollama_client<span style=\"color:#f92672\">.<\/span>chat({\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t  <span style=\"color:#e6db74\">model<\/span>: <span style=\"color:#e6db74\">&#34;llama3&#34;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t  <span style=\"color:#e6db74\">stream<\/span>: <span style=\"color:#66d9ef\">true<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t  <span style=\"color:#e6db74\">messages<\/span>: messages\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t}) <span style=\"color:#66d9ef\">do<\/span> <span style=\"color:#f92672\">|<\/span>event, raw<span style=\"color:#f92672\">|<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t  response<span style=\"color:#f92672\">.<\/span>message <span style=\"color:#f92672\">+=<\/span> event<span style=\"color:#f92672\">[<\/span><span style=\"color:#e6db74\">&#34;message&#34;<\/span><span style=\"color:#f92672\">][<\/span><span style=\"color:#e6db74\">&#34;content&#34;<\/span><span style=\"color:#f92672\">]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t  response<span style=\"color:#f92672\">.<\/span>save\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>Notably, we enable the <code>stream: true<\/code> option and provide a block to collect the responses.<\/p>"},{"title":"Local AI: Improving response time","link":"https:\/\/jbennett.me\/articles\/local-ai-improving-response-time\/","pubDate":"Thu, 27 Feb 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/local-ai-improving-response-time\/","description":"<p>The big problem we have with our current AI implementation is that is slow. When the user submits a new message they have to wait for entire response before it shows up. Fortunately, Turbo gives us an excellent tool to improve the situation.<\/p>\n<p>We will be changing the implementation to queue a job to get the AI response and return from the controller immediately.<\/p>\n<h2 id=\"1-conversation-update-job\">1. Conversation Update Job<\/h2>\n<p>We will remove the inline call to update the conversation and move it into a background job:<\/p>"},{"title":"Local AI: Wiring Up the Conversation Model with Rails","link":"https:\/\/jbennett.me\/articles\/wiring-up-the-conversation-model\/","pubDate":"Wed, 26 Feb 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/wiring-up-the-conversation-model\/","description":"<p>We tested our Ollama connection, now let&rsquo;s actually wire it in. We will want create a <code>Conversation<\/code> model that have many <code>Message<\/code>s. Each message will be an individual piece of the conversation. Each message will have the message itself, and a role for who submitted it, the user, the AI, or the system. System Prompts are used to prime the AI.<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-shell\" data-lang=\"shell\"><span style=\"display:flex;\"><span>rails g scaffold conversation title:string\n<\/span><\/span><span style=\"display:flex;\"><span>rails g model message conversation:references role:string message:text\n<\/span><\/span><span style=\"display:flex;\"><span>rails db:migrate\n<\/span><\/span><\/code><\/pre><\/div><div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># app\/models\/conversation.rb<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">Conversation<\/span> <span style=\"color:#f92672\">&lt;<\/span> <span style=\"color:#66d9ef\">ApplicationRecord<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\thas_many <span style=\"color:#e6db74\">:messages<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>We will update the conversation view to include the messages and a form to post another message in the thread:<\/p>"},{"title":"Local AI: Connecting Rails to Ollama","link":"https:\/\/jbennett.me\/articles\/local-ai-connecting-rails-to-ollama\/","pubDate":"Tue, 25 Feb 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/local-ai-connecting-rails-to-ollama\/","description":"<p>With Ollama running locally, we want to connect it with a Rails app. We will create a new Rails app and use a simple wrapper to make interacting with the Ollama API a little easier:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-plain\" data-lang=\"plain\"><span style=\"display:flex;\"><span>rails new ai-test\n<\/span><\/span><span style=\"display:flex;\"><span>cd ai-test\n<\/span><\/span><span style=\"display:flex;\"><span>bundle add ollama-ai\n<\/span><\/span><\/code><\/pre><\/div><p>The <code>bin\/dev<\/code> command uses <a href=\"https:\/\/github.com\/ddollar\/foreman\">Foreman<\/a> to run all the necessary processes for development. We will want update this to add an additional process for Ollama:<\/p>\n<pre tabindex=\"0\"><code class=\"language-procfile\" data-lang=\"procfile\"># Procfile.dev\nweb: env RUBY_DEBUG_OPEN=true bin\/rails server\njs: yarn build --watch\ncss: yarn build:css --watch\nai: ollama start # this is new\n<\/code><\/pre><p>In a new terminal, run <code>bin\/dev<\/code> to start up the server, including Ollama. Let&rsquo;s open a Rails console to test we are connected correctly (<code>rails c<\/code>):<\/p>"},{"title":"Local AI: Getting up and running","link":"https:\/\/jbennett.me\/articles\/local-ai-getting-up-and-running\/","pubDate":"Mon, 24 Feb 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/local-ai-getting-up-and-running\/","description":"<p>I don&rsquo;t know if you&rsquo;ve heard, but there&rsquo;s this new tech that people are starting to talk about called AI. I think it might have some potential\u2026<\/p>\n<p>The easiest way to get started with AI is to sign up with one of the cloud AI solutions, ChatGPT for instance, but there are plenty of reasons to run AI on a box you control:<\/p>\n<ul>\n<li>Lower costs: By running AI on your own machine, you can avoid the costs associated with cloud services. This can be especially important if you&rsquo;re in the experimentation phase and are not sure what level of compute you will require.<\/li>\n<li>Control over data: When you run AI on a cloud service, you have to trust that the company is handling your data securely and in compliance with relevant regulations. By running AI on your own machine, you have complete control over your data and can ensure it&rsquo;s being handled in accordance with your organization&rsquo;s policies.<\/li>\n<li>Regulatory compliance: For example, if you&rsquo;re working in an industry that requires GDPR or HIPPA compliance, having control over your AI environment ensures that you can meet those requirements and avoid potential fines or reputational damage.<\/li>\n<\/ul>\n<p>To really unlock the power of AI, though, we need to set up a basic development environment. This will allow us to experiment and learn from our mistakes.<\/p>"},{"title":"Evolving your views: Enable caching","link":"https:\/\/jbennett.me\/articles\/evolving-you-views-caching\/","pubDate":"Fri, 21 Feb 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/evolving-you-views-caching\/","description":"<p>One of the first big wins we can achieve by separating out view into different objects and partials is better performance through caching. In fact, by deeply nesting our partials, we can easily do <a href=\"https:\/\/guides.rubyonrails.org\/caching_with_rails.html#russian-doll-caching\">Russian Doll Caching<\/a>.<\/p>\n<p>To take advantage of this, we pull out a <code>post_row.html.erb<\/code> partial and enabled caching:<\/p>\n<pre tabindex=\"0\"><code class=\"language-erb\" data-lang=\"erb\">&lt;!-- app\/views\/posts\/post_row.html.erb --&gt;\n&lt;li&gt;&lt;%= link_to post.title, post %&gt;&lt;\/li&gt;\n\n&lt;!-- app\/views\/posts\/post_list.html.erb --&gt;\n&lt;ul&gt;\n\t&lt;%= render partial: &#34;posts\/post_row&#34;, \n\t\t\tcollection: dashboard.posts, \n\t\t\tas: :post, \n\t\t\tcached: true %&gt;\n&lt;\/ul&gt;\n<\/code><\/pre><p>This change alone will significantly decrease the render time of your screens.<\/p>"},{"title":"Evolving your views: Recursively specialize","link":"https:\/\/jbennett.me\/articles\/evolving-your-views-recursively-specialize\/","pubDate":"Thu, 20 Feb 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/evolving-your-views-recursively-specialize\/","description":"<p>Yesterday we broke the entire dashboard into a component. This can make sense to apply to all the subcomponents of a page, the posts list in this case:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">PostList<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">posts<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">Post<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\t<span style=\"color:#f92672\">.<\/span>where(<span style=\"color:#e6db74\">publish_at<\/span>: <span style=\"color:#f92672\">[..<\/span><span style=\"color:#66d9ef\">Time<\/span><span style=\"color:#f92672\">.<\/span>current<span style=\"color:#f92672\">]<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\t<span style=\"color:#f92672\">.<\/span>order(<span style=\"color:#e6db74\">publish_at<\/span>: <span style=\"color:#e6db74\">:desc<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\t<span style=\"color:#f92672\">.<\/span>limit(<span style=\"color:#ae81ff\">10<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">to_partial_path<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#e6db74\">&#34;posts\/post_list&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">Dashboard<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">post_list<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">PostList<\/span><span style=\"color:#f92672\">.<\/span>new\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><br\/>\n<pre tabindex=\"0\"><code class=\"language-erb\" data-lang=\"erb\">&lt;!-- app\/views\/posts\/post_list.html.erb --&gt;\n&lt;ul&gt;\n\t&lt;%= dashboard.comments.each do |comment| %&gt;\n\t\t&lt;li&gt;&lt;%= link_to comment.body, \n\t\t\tpost_path(comment.post_id, anchor: dom_id(comment))\n\t\t%&gt;&lt;\/li&gt;\n\t&lt;% end %&gt;\n&lt;\/ul&gt;\n\n&lt;!-- app\/views\/dashboards\/dashboard.html.erb --&gt;\n&lt;div&gt;\n\t&lt;h2&gt;Recent Comments&lt;\/h2&gt;\n\t\n\t&lt;%= render dashboard.post_list %&gt;\n&lt;\/div&gt;\n<\/code><\/pre><p>Where this can provide a lot of benefit is if you add additional customization to the <code>PostsList<\/code> class, filtering, sorting, and searching for example. You could easily reuse this and be able to make calls like:<\/p>"},{"title":"Evolving your views: Extracting a page object","link":"https:\/\/jbennett.me\/articles\/evolving-your-views-extract-a-page-object\/","pubDate":"Wed, 19 Feb 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/evolving-your-views-extract-a-page-object\/","description":"<p>Extracting our dashboard&rsquo;s configuration out of the controller will let us keep our controller simple and focuses, and let us test things more easily in the future when things become more complicated. Where we want to end up is creating an object in our controller and rendering it in our view:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># app\/controllers\/dashboards_controller.rb<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">DashboardsController<\/span> <span style=\"color:#f92672\">&lt;<\/span> <span style=\"color:#66d9ef\">ApplicationController<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">show<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t@dashboard <span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">Dashboard<\/span><span style=\"color:#f92672\">.<\/span>new\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><br\/>\n<pre tabindex=\"0\"><code class=\"language-erb\" data-lang=\"erb\">&lt;%= render @dashboard %&gt;\n<\/code><\/pre><p>We&rsquo;ll start by pulling the <code>posts<\/code> and <code>comments<\/code> into the new class:<\/p>"},{"title":"Evolving your views: A conventional setup","link":"https:\/\/jbennett.me\/articles\/evolving-your-views-starting-point\/","pubDate":"Tue, 18 Feb 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/evolving-your-views-starting-point\/","description":"<p>A normal Rails page will pull data in the controller and present it in the view:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># app\/controllers\/dashboards_controller.rb<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">DashboardsController<\/span> <span style=\"color:#f92672\">&lt;<\/span> <span style=\"color:#66d9ef\">ApplicationController<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">show<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t@recent_posts <span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">Post<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\t<span style=\"color:#f92672\">.<\/span>where(<span style=\"color:#e6db74\">publish_at<\/span>: <span style=\"color:#f92672\">[..<\/span><span style=\"color:#66d9ef\">Time<\/span><span style=\"color:#f92672\">.<\/span>current<span style=\"color:#f92672\">]<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\t<span style=\"color:#f92672\">.<\/span>order(<span style=\"color:#e6db74\">publish_at<\/span>: <span style=\"color:#e6db74\">:desc<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\t<span style=\"color:#f92672\">.<\/span>limit(<span style=\"color:#ae81ff\">10<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t@recent_comments <span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">Comment<\/span><span style=\"color:#f92672\">.<\/span>all\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\t<span style=\"color:#f92672\">.<\/span>order(<span style=\"color:#e6db74\">create_at<\/span>: <span style=\"color:#e6db74\">:desc<\/span>)<span style=\"color:#f92672\">.<\/span>limit(<span style=\"color:#ae81ff\">10<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><br\/>\n<pre tabindex=\"0\"><code class=\"language-erb\" data-lang=\"erb\">&lt;!-- app\/views\/dashboards\/show.html.erb --&gt;\n&lt;div class=&#34;grid grid-cols-2&#34;&gt;\n\t&lt;div&gt;\n\t\t&lt;h2&gt;Recent Posts&lt;\/h2&gt;\n\t\t\n\t\t&lt;ul&gt;\n\t\t\t&lt;%= @recent_posts.each do |post| %&gt;\n\t\t\t\t&lt;li&gt;&lt;%= link_to post.title, post %&gt;&lt;\/li&gt;\n\t\t\t&lt;% end %&gt;\n\t\t&lt;\/ul&gt;\n\t&lt;\/div&gt;\n\t\n\t&lt;div&gt;\n\t\t&lt;h2&gt;Recent Comments&lt;\/h2&gt;\n\t\t\n\t\t&lt;ul&gt;\n\t\t\t&lt;%= @recent_comments.each do |comment| %&gt;\n\t\t\t\t&lt;li&gt;&lt;%= link_to comment.body, \n\t\t\t\t\tpost_path(comment.post_id, anchor: dom_id(comment))\n\t\t\t\t%&gt;&lt;\/li&gt;\n\t\t\t&lt;% end %&gt;\n\t\t&lt;\/ul&gt;\n\t&lt;\/div&gt;\n&lt;\/div&gt;\n<\/code><\/pre><p>I generally try to avoid using <code>where<\/code> outside of model classes. Typically there is a concept trying to come out. Identifying it and naming it, usually with a <a href=\"https:\/\/guides.rubyonrails.org\/active_record_querying.html#scopes\"><code>scope<\/code><\/a> is a better choice:<\/p>"},{"title":"Evolving out of the view's primordial soup","link":"https:\/\/jbennett.me\/articles\/evolving-out-of-the-primordial-soup\/","pubDate":"Fri, 14 Feb 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/evolving-out-of-the-primordial-soup\/","description":"<p>When a project is new, it&rsquo;s easy to just access what you need, where you need it. This inline access however becomes complicated quickly. This can be mitigated by moving away from inline, adhoc access, and formalizing things into reusable pieces. There are gems like <a href=\"https:\/\/viewcomponent.org\">ViewComponent<\/a> and <a href=\"https:\/\/www.phlex.fun\">Phlex<\/a>, but Rails can do a lot of this natively too.<\/p>\n<p>Over the next few emails, we&rsquo;ll take this:<\/p>\n<pre tabindex=\"0\"><code class=\"language-erb\" data-lang=\"erb\">&lt;% Post.offset(params.fetch(:page, 0) * 25).limit(25).each do |post| %&gt;\n\t&lt;div&gt;\n\t\t&lt;h2&gt;&lt;%= post.title %&gt;&lt;\/h2&gt;\n\t\t&lt;%= post.body %&gt;\n\t&lt;\/div&gt;\n&lt;% end %&gt;\n<\/code><\/pre><p>and turn it into:<\/p>"},{"title":"Proper programatic navigation in Hotwire Native","link":"https:\/\/jbennett.me\/articles\/proper-programatic-navigation\/","pubDate":"Thu, 13 Feb 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/proper-programatic-navigation\/","description":"<p>Just a quick Hotwire Native\/Turbo tip: use <code>Turbo.visit(url)<\/code> for navigation not <code>window.location = url<\/code>.<\/p>\n<p><code>Turbo.visit<\/code> makes sure frames are respected etc and avoids Hotwire Native opening the address in a modal popup.<\/p>"},{"title":"Ever struggle with \"whack-a-mole\" bugs","link":"https:\/\/jbennett.me\/articles\/whack-a-mole-bugs\/","pubDate":"Wed, 12 Feb 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/whack-a-mole-bugs\/","description":"<p>I have a confession: I sometimes write bugs. I know, I&rsquo;ve ruined your perfect image of me,<\/p>\n<p>Here&rsquo;s the thing: all programmers write bugs. Good programmer make sure that a bug only needs to be fixed once.<\/p>\n<p>When a bug happens, the path to fix it is a simple (though sometimes very difficult) process:<\/p>\n<ol>\n<li>Replicate the issue locally.<\/li>\n<li>Write a failing test with the expected outcome.<\/li>\n<li>Get the failing test to pass.<\/li>\n<li>Review changes and improve your code.<\/li>\n<\/ol>\n<p>By always writing a test for bugs, you can move forward with confidence, knowing that the issue is fixed and won&rsquo;t happen again in the future.<\/p>"},{"title":"Does your business have the reporting tools it needs?","link":"https:\/\/jbennett.me\/articles\/business-reporting\/","pubDate":"Tue, 11 Feb 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/business-reporting\/","description":"<p>Getting data out of your system and into a format that is usable for technical and business decisions is critical. Avoiding development cycles for this kind of work is helpful as it lets you treat your data as a playground where you can explore and gain insights.<\/p>\n<p>I have used <a href=\"https:\/\/www.metabase.com\">Metabase<\/a> as my go to reporting package. Setting it up with read-only access to production database means you can access your information and be confident you aren&rsquo;t going to accidentally harm the production environment. At that point, developers and technically inclined business admins can access the entire system.<\/p>"},{"title":"It's more fun if your business has some RUM","link":"https:\/\/jbennett.me\/articles\/fun-with-rum\/","pubDate":"Mon, 10 Feb 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/fun-with-rum\/","description":"<p>Understanding how customers are using your system is key to building good software and evaluating the success of new features and improvements. This type of software is call user analytics or real-time user monitoring (RUM).<\/p>\n<p>User analytics can broadly be broken down into two categories, general analytics and targeted analytics.<\/p>\n<p>General analytics would be events like page views, button clicks etc that are very generic. By tracking all these details you&rsquo;re able to retroactively look at customer behaviour to analyze how people are using your site. Analytics packages typically provide general analytics &ldquo;for free&rdquo; automatically when added to your application.<\/p>"},{"title":"You need a business project management tool","link":"https:\/\/jbennett.me\/articles\/business-project-management-tool\/","pubDate":"Fri, 07 Feb 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/business-project-management-tool\/","description":"<p>Your software team is likely already using something for project management. It might be complicated and robust like Jira, or simple and straightforward like a <code>todo.md<\/code> file in your project.<\/p>\n<p>Many businesses don&rsquo;t have a business level project management tool thought.<\/p>\n<p>A business project management tool is typically much less complicated than a general tool. When I work with clients, we are typically tracking 3\u20137 features at a time at a high level. This means we can get away with using almost any tool. I think viewing a column oriented list (kanban board) of Done, Review, Active, Pending, and Up Next lets you easily see what is active.<\/p>"},{"title":"Does your business have the necessay tools?","link":"https:\/\/jbennett.me\/articles\/necessary-tools\/","pubDate":"Thu, 06 Feb 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/necessary-tools\/","description":"<p>All SaaSs have technical tools: cloud hosting, error loggers, task management tools, and more. By they also need business tools to run well:<\/p>\n<ul>\n<li>Business level project management<\/li>\n<li>User analytics<\/li>\n<li>Reporting<\/li>\n<\/ul>\n<p>Do you have all these in place in your business? If not, do you know what to look for? Stay tuned over the next few days as we cover each one.<\/p>"},{"title":"How much \"I\" is there in AI today?","link":"https:\/\/jbennett.me\/articles\/how-much-i-in-ai\/","pubDate":"Wed, 05 Feb 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/how-much-i-in-ai\/","description":"<p>The &ldquo;I&rdquo; in AI might be the most overhyped letter in tech today. AI companies, startups, and the tech industry as a whole, is obsessed with really good summaries and image generation.<\/p>\n<p>Am I belittling this some \u2013 yes. But the rest of tech is exaggerating it. Is there potential there \u2013 again yes. But there is potential in everything.<\/p>\n<p>My issue is a terminology issue. Please stop calling it intelligent. Will it get there, maybe. But it isn&rsquo;t there today.<\/p>"},{"title":"Clean up your associations using Association Extensions","link":"https:\/\/jbennett.me\/articles\/clean-up-your-associations-using-extensions\/","pubDate":"Tue, 04 Feb 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/clean-up-your-associations-using-extensions\/","description":"<p>Sometimes having a custom method on an association would be handy. Well good news, you can do that with Rails!<\/p>\n<p>I recently used this to reorganize and simplify some deeply nested <code>has_many through<\/code> associations with complex self-referential joins. It was something like this:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">User<\/span> <span style=\"color:#f92672\">&lt;<\/span> <span style=\"color:#66d9ef\">ApplicationRecord<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\thas_many <span style=\"color:#e6db74\">:memberships<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\thas_many <span style=\"color:#e6db74\">:teams<\/span>, <span style=\"color:#e6db74\">through<\/span>: <span style=\"color:#e6db74\">:memberships<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\thas_many <span style=\"color:#e6db74\">:tasks<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">Team<\/span> <span style=\"color:#f92672\">&lt;<\/span> <span style=\"color:#66d9ef\">ApplicationRecord<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\thas_many <span style=\"color:#e6db74\">:memberships<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\thas_many <span style=\"color:#e6db74\">:users<\/span>, <span style=\"color:#e6db74\">through<\/span>: <span style=\"color:#e6db74\">:memberships<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">Membership<\/span> <span style=\"color:#f92672\">&lt;<\/span> <span style=\"color:#66d9ef\">ApplicationRecord<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\tenum <span style=\"color:#e6db74\">:role<\/span>, <span style=\"color:#e6db74\">%w[peon manager]<\/span><span style=\"color:#f92672\">.<\/span>index_by(<span style=\"color:#f92672\">&amp;<\/span><span style=\"color:#e6db74\">:itself<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t\n<\/span><\/span><span style=\"display:flex;\"><span>\tbelongs_to <span style=\"color:#e6db74\">:user<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\tbelongs_to <span style=\"color:#e6db74\">:team<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">Task<\/span> <span style=\"color:#f92672\">&lt;<\/span> <span style=\"color:#66d9ef\">ApplicationRecord<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\tbelongs_to <span style=\"color:#e6db74\">:user<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>Accessing my tasks is done easily via <code>User#tasks<\/code>, but if you wanted to get all tasks for all members of the teams you are a manager of, the query could be a bit complicated. However, another option is to add it the association itself using an <a href=\"https:\/\/guides.rubyonrails.org\/association_basics.html#extensions\">extension<\/a>:<\/p>"},{"title":"How will you avoid the AI regurgitation machine?","link":"https:\/\/jbennett.me\/articles\/how-will-you-avoid-ai-regurgitation\/","pubDate":"Mon, 03 Feb 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/how-will-you-avoid-ai-regurgitation\/","description":"<p>A high-schooler&rsquo;s essays have a certain level of depth and, shall we say, quality to them. They are only ever read by the teacher, maybe the parents, and on occasion the student themselves (that&rsquo;s a joke\u2026 probably). This happens because the student lacks skill and experience in writing, and experience in life in general. This is the reason they write; to gain the necessary experience and to move out of the &ldquo;regurgitate everything I am told&rdquo; phase and into the &ldquo;I have thoughts and ideas phase&rdquo;.<\/p>"},{"title":"Adding multi-model search, with just a bit of complexity","link":"https:\/\/jbennett.me\/articles\/adding-multi-search-with-a-bit-of-complexity\/","pubDate":"Fri, 31 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/adding-multi-search-with-a-bit-of-complexity\/","description":"<p>Last time we used <a href=\"https:\/\/github.com\/Casecommons\/pg_search\">pg_search<\/a> to search a single model, but we also have the option to search multiple models. To support multisearch we will need to:<\/p>\n<ol>\n<li>Add a backing table that search is performed against<\/li>\n<li>Specify configuration for all multisearch-able models<\/li>\n<li>Rebuild Search Index<\/li>\n<li>Profit!<\/li>\n<\/ol>\n<h2 id=\"1-add-backing-table\">1. Add Backing Table<\/h2>\n<p>Fortunately, this is quick and painless. pg_search includes a migration for us se we just need to copy that over and run it:<\/p>"},{"title":"The Thing","link":"https:\/\/jbennett.me\/articles\/the-thing\/","pubDate":"Thu, 30 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/the-thing\/","description":"<p>Every project has it&rsquo;s Thing\u2122\ufe0f.<\/p>\n<p>The Thing is the problem that isn&rsquo;t really the problem you need to solve, but it&rsquo;s in the way of getting to the main thing.<\/p>\n<p>If you are on a journey to slay a dragon, you clearly need to slay the dragon, that is the main thing. The mountain in the way through, that&rsquo;s The Thing.<\/p>\n<p>One one hand, The Thing is annoying and frustrating. It&rsquo;s the hardship you don&rsquo;t want to solve. It might even what is preventing you from having success.<\/p>"},{"title":"Adding Search Without Complexity","link":"https:\/\/jbennett.me\/articles\/adding-search-without-complexity\/","pubDate":"Wed, 29 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/adding-search-without-complexity\/","description":"<p>Adding search to your system is somewhere between super easy and &ldquo;let&rsquo;s hire a team&rdquo;. Let&rsquo;s look at the first tiers of difficulty.<\/p>\n<h1 id=\"1-simple-search\">1. Simple Search<\/h1>\n<p>Adding simple search of a single model can be achieved using the Rails basics: create a form, and filter in the controller:<\/p>\n<pre tabindex=\"0\"><code class=\"language-erb\" data-lang=\"erb\">&lt;!-- global search form --&gt;\n&lt;%= form_with url: global_search_path do |form| %&gt;\n\t&lt;%= form.search_field :q, value: params[:q] %&gt;\n&lt;% end %&gt;\n\n&lt;!-- app\/views\/global_searches\/show.html.erb --&gt;\n&lt;%= render @results %&gt;\n<\/code><\/pre><br>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># config\/routes.rb<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">Rails<\/span><span style=\"color:#f92672\">.<\/span>application<span style=\"color:#f92672\">.<\/span>routes<span style=\"color:#f92672\">.<\/span>draw <span style=\"color:#66d9ef\">do<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\tresource <span style=\"color:#e6db74\">:global_search<\/span>, <span style=\"color:#e6db74\">only<\/span>: <span style=\"color:#e6db74\">:show<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># app\/controllers\/global_searches_controller.rb<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">GlobalSearchesController<\/span> <span style=\"color:#f92672\">&lt;<\/span> <span style=\"color:#66d9ef\">ApplicationController<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">show<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\tsearch <span style=\"color:#f92672\">=<\/span> <span style=\"color:#e6db74\">&#34;%<\/span><span style=\"color:#e6db74\">#{<\/span>params<span style=\"color:#f92672\">[<\/span><span style=\"color:#e6db74\">:q<\/span><span style=\"color:#f92672\">].<\/span>to_s<span style=\"color:#f92672\">.<\/span>downcase<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\">%&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t@results <span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">MyModel<\/span><span style=\"color:#f92672\">.<\/span>where(<span style=\"color:#e6db74\">&#34;my_models.title LIKE ?&#34;<\/span>, search)\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>This will do a case insensitive search across a single field on a single model.<\/p>"},{"title":"Numbers in Your Code are Red Flags","link":"https:\/\/jbennett.me\/articles\/numbers-are-red-flags\/","pubDate":"Tue, 28 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/numbers-are-red-flags\/","description":"<p>Number in your code are red flags.<\/p>\n<p>For instance, what does &ldquo;12&rdquo; mean? this could be used for a dozen in a recipe app, months of the year for a Gregorian calendar, or number of disciples in a religion app.<\/p>\n<p>I <em><strong>always<\/strong><\/em> raise a flag when I see a number.<\/p>\n<p>Where this bit me recently was with an invitation system. Invites expired after 2 days, but that was going to be changed to a week. This had to be updated in 3 places: the invite link code, and the HTML and plain text templates.<\/p>"},{"title":"A Short Programmer Grammar Lesson","link":"https:\/\/jbennett.me\/articles\/programmer-grammar-lesson\/","pubDate":"Thu, 23 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/programmer-grammar-lesson\/","description":"<p>If you take my suggestion from yesterday and drastically increase the number of models in you system, there is one problem you&rsquo;ll run into a lot more: naming things.<\/p>\n<p>Here are a couple suggestions to help easy the pain:<\/p>\n<ol>\n<li>Classes should be nouns. They are the things in you system.<\/li>\n<li>Methods are verbs. These are the actions your nouns can take.<\/li>\n<li>The length of a name should match the area where it is accessible. Calling a loop index variable x is fine. Calling a global variable x is not.<\/li>\n<\/ol>\n<p>There are a million other rules-of-thumb, but this should get your started.<\/p>"},{"title":"Conventions for Rails Partials","link":"https:\/\/jbennett.me\/articles\/conventions-for-rails-partials\/","pubDate":"Thu, 23 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/conventions-for-rails-partials\/","description":"<p>Part of organizing your Rails views is having reliable partials. You should hav a standard set of templates, which helps you know what is what.<\/p>\n<p>My main views are still going to match the actions, <code>index<\/code>, <code>new<\/code>, and <code>edit<\/code>. These will typically have an HTML version, and now-a-days, often a TurboStream version. <code>create<\/code>, <code>update<\/code>, and <code>delete<\/code> typically only have a version for TurboStream.<\/p>\n<p><code>_form<\/code> will provide a fully featured editing experience. It&rsquo;s not uncommon to also have an <code>_inline_form<\/code> with a subset of fields.<\/p>"},{"title":"Using Layouts for Better Partials","link":"https:\/\/jbennett.me\/articles\/using-layouts-for-better-partials\/","pubDate":"Thu, 23 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/using-layouts-for-better-partials\/","description":"<p>An additional way to reuse partials is to render them within a wrapper layout.<\/p>\n<p>For instance, if you have a normal partial for a model, you can wrap it with an admin partial that adds edit or delete links:<\/p>\n<pre tabindex=\"0\"><code class=\"language-erb\" data-lang=\"erb\">&lt;!-- _item.html.erb --&gt;\n&lt;div id=&#34;&lt;%= dom_id(item) %&gt;&#34;&gt;\n\t&lt;%= link_to item.name, item %&gt;\n&lt;\/div&gt;\n\n&lt;!-- _admin_wrapper.html.erb --&gt;\n&lt;div class=&#34;flex justify-between&#34;&gt;\n\t&lt;%= yield %&gt;\n\t\n\t&lt;%= button_to &#34;Delete&#34;, item, methed: :delete %&gt;\n&lt;\/div&gt;\n\n&lt;!-- usage --&gt;\n&lt;%= render partial: &#34;items\/item&#34;, collection: @items, layout: &#34;items\/admin_wrapper&#34; %&gt;\n<\/code><\/pre><p>By using a layout, your item partial remains clutter free and reusable throughout your system.<\/p>"},{"title":"Solve your Rails Problems with More Models","link":"https:\/\/jbennett.me\/articles\/solve-rails-problems-with-more-models\/","pubDate":"Wed, 22 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/solve-rails-problems-with-more-models\/","description":"<p>One of the most common ways I solve problems in a Rails app is by creating more model classes.<\/p>\n<p>| Note: The folder is called <code>models<\/code> note <code>table_classes<\/code>. These classes don&rsquo;t have to extend <code>ActiveRecord::Base<\/code><\/p>\n<p>For instance, if you have a page with a primary record and a bunch of related or computed stuff, move it into a custom object and clear up your view&rsquo;s code.<\/p>\n<p>My ideal view has simple <code>if<\/code>, and <code>each<\/code> statements, minimal logic, and lots of <code>render<\/code> calls.<\/p>"},{"title":"Avoiding x-amz-checksum-mode Errors on Alternative S3 backends","link":"https:\/\/jbennett.me\/articles\/avoiding-a-amz-checksum-errors\/","pubDate":"Tue, 21 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/avoiding-a-amz-checksum-errors\/","description":"<p>If you are using an alternative S3 backend, like BackBlaze&rsquo;s B2, you might be getting a <code>Aws::S3::Errors::InvalidArgument<\/code> error for an unsupported &lsquo;x-amz-checksum-mode&rsquo; header. The quick fix is to add a couple environment variables to disable the checksum until B2 supports it:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-plain\" data-lang=\"plain\"><span style=\"display:flex;\"><span>AWS_REQUEST_CHECKSUM_CALCULATION = WHEN_REQUIRED\n<\/span><\/span><span style=\"display:flex;\"><span>AWS_RESPONSE_CHECKSUM_VALIDATION = WHEN_REQUIRED\n<\/span><\/span><\/code><\/pre><\/div><p>For my deployments with Kamal, this was added to the variables in <code>env.clear<\/code>.<\/p>"},{"title":"Hotwire Native: An Early Review","link":"https:\/\/jbennett.me\/articles\/hotwire-native-early-review\/","pubDate":"Mon, 20 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/hotwire-native-early-review\/","description":"<p>All new tech is wonderful and full of magic\u2026 until it isn&rsquo;t. It&rsquo;s only when things aren&rsquo;t 100% right that a technology shows it&rsquo;s full set of colours. This is my, very early, review of Hotwire Native.<\/p>\n<h2 id=\"the-pros\">The Pros<\/h2>\n<p>For my projects, Hotwire Native has largely delivered on it&rsquo;s promise of coding for the web, and running on device.<\/p>\n<p>Bridge Components worked very well for web to native communication and felt very Rails-y to me. Adding fully native screens with ViewControllers\/Fragments worked very well.<\/p>"},{"title":"Use puts proudly","link":"https:\/\/jbennett.me\/articles\/use-puts-proudly\/","pubDate":"Fri, 17 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/use-puts-proudly\/","description":"<p>One of the most powerful tools in your Ruby programming arsenal is the humble <code>puts<\/code>. Something like <code>puts x.class.name<\/code> has helped me understand the nouns of my work better that anything else.<\/p>\n<p>Once you understand what is in your scope, you&rsquo;ll be well on your way to solving your problems.<\/p>"},{"title":"One Person Framework","link":"https:\/\/jbennett.me\/articles\/one-person-framework\/","pubDate":"Thu, 16 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/one-person-framework\/","description":"<p>DHH says Rails can be a <a href=\"https:\/\/world.hey.com\/dhh\/the-one-person-framework-711e6318\">&ldquo;one person framework&rdquo;<\/a>. He&rsquo;s right.<\/p>\n<p>I am currently developing or maintaining nine production applications, and an additional three mobile applications. A single developer would have a very hard time accomplishing that on another framework.<\/p>\n<p>If you are looking for that kind of efficiency for your software needs, reply and we can talk about it.<\/p>"},{"title":"The High Cost of Cheap Foundations","link":"https:\/\/jbennett.me\/articles\/high-cost-of-cheap-foundations\/","pubDate":"Wed, 15 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/high-cost-of-cheap-foundations\/","description":"<p>If you were building a house, would you hire the cheapest company to lay the foundation?<\/p>\n<p>Of course not. You know that cutting corners on the foundation would lead to massive costs and endless headaches down the road.<\/p>\n<p>The same is true for your software. Don&rsquo;t treat the foundation as an afterthought.<\/p>"},{"title":"Hotwire Native Release Plan","link":"https:\/\/jbennett.me\/articles\/hotwire-native-release-plan\/","pubDate":"Tue, 14 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/hotwire-native-release-plan\/","description":"<p>Launching a SaaS can be stressful. Adding in native apps can make it even more so. Hotwire Native can vastly simplify the complexity so you can release your app more quick and maintain it more easily.<\/p>\n<p>Here&rsquo;s the project gameplan I&rsquo;ve recommended:<\/p>\n<ol>\n<li>Build out basic functionality in Rails.<\/li>\n<li>Make sure everything looks reasonable on mobile.<\/li>\n<li>Deploy to production or staging servers.<\/li>\n<li>Build basic Hotwire Native iOS and Android apps. Submit for review.<\/li>\n<li>Work on more rails features.<\/li>\n<li>Update and resubmit native apps if needed.<\/li>\n<\/ol>\n<p>This lets you address any review issues ASAP with is the area you can most expect unexpected delays.<\/p>"},{"title":"Hotwire Native is a super power","link":"https:\/\/jbennett.me\/articles\/hotwire-native-is-a-super-power\/","pubDate":"Mon, 13 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/hotwire-native-is-a-super-power\/","description":"<p>If you are in the Rails ecosystem and you haven&rsquo;t tried out <a href=\"https:\/\/native.hotwired.dev\">Hotwire Native<\/a>, drop what you are doing now and go check it out.<\/p>\n<p>In the last two weeks I&rsquo;ve taken a client&rsquo;s app from only being a PWA, to being submitted to both app stores. This means native tabs, navigation, and toolbar buttons, and theming so that the application feels right on the user&rsquo;s device.<\/p>\n<p>Here&rsquo;s your homework:<\/p>\n<ol>\n<li>Follow the Hotwire Native tutorial (<a href=\"https:\/\/native.hotwired.dev\/ios\/getting-started\">iOS<\/a>, <a href=\"https:\/\/native.hotwired.dev\/android\/getting-started\">Android<\/a>) to get the demo site running on your device or in the simulator.<\/li>\n<li>Swap the demo site URL with your responsive site&rsquo;s URL<\/li>\n<li>Profit\u2026<\/li>\n<\/ol>\n<p>I&rsquo;ll be following up with more info like:<\/p>"},{"title":"My Favourite Tools for Getting Things Done","link":"https:\/\/jbennett.me\/articles\/my-favourite-tools\/","pubDate":"Fri, 10 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/my-favourite-tools\/","description":"<p>I&rsquo;ve been asked what tools I use day-to-day. Here are a few of my favourites:<\/p>\n<h2 id=\"applications\">Applications<\/h2>\n<ul>\n<li><a href=\"https:\/\/www.jetbrains.com\/ruby\">RubyMine<\/a>: My go-to editor for writing and debugging Rails code, with powerful features like intelligent code navigation and refactoring tools.<\/li>\n<li><a href=\"https:\/\/www.git-tower.com\/\">Tower<\/a>: Helps me visually manage complex Git workflows, making version control less of a headache.<\/li>\n<li><a href=\"https:\/\/eggerapps.at\/postico2\/\">Postico<\/a>: A clean, user-friendly PostgreSQL UI for managing databases with ease.<\/li>\n<li><a href=\"https:\/\/www.omnigroup.com\/omnifocus\/\">OmniFocus<\/a>: Personal task management to stay organized and on top of my daily priorities.<\/li>\n<\/ul>\n<h2 id=\"frameworks\">Frameworks<\/h2>\n<ul>\n<li><a href=\"https:\/\/rubyonrails.org\">Ruby on Rails<\/a>: Obviously, the backbone of most of my web development work.<\/li>\n<li><a href=\"https:\/\/hotwired.dev\">Hotwire<\/a>: Brings interactivity to my apps without the pain of managing heavy JavaScript frameworks.<\/li>\n<li><a href=\"https:\/\/github.com\/excid3\/noticed\">Noticed<\/a>: Makes notifications simple and consistent across channels.<\/li>\n<li><a href=\"https:\/\/github.com\/pushpad\/web-push\">web-push<\/a>: Takes the hassle out of setting up web-based push notifications.<\/li>\n<li><a href=\"https:\/\/viewcomponent.org\">view_component<\/a>: Provides structure for creating reusable, modular views in Rails.<\/li>\n<li><a href=\"https:\/\/www.flippercloud.io\">flipper<\/a>: A feature flag library that lets me gradually roll out new features.<\/li>\n<li><a href=\"https:\/\/github.com\/basecamp\/local_time\">local_time<\/a>: Automatically adjusts database timestamps to the user\u2019s local time zone.<\/li>\n<li><a href=\"https:\/\/github.com\/scenic-views\/scenic\">scenic<\/a>: Lets me treat database views like normal tables in Rails, simplifying complex queries.<\/li>\n<li><a href=\"https:\/\/kamal-deploy.org\">Kamal<\/a>:  A fantastic deployment tool for smoother launches with better performance and cost savings.<\/li>\n<li><a href=\"https:\/\/shopify.github.io\/liquid\/\">liquid<\/a>: A safe templating library for user-customizable content.<\/li>\n<li><a href=\"https:\/\/github.com\/schpet\/cool_id\">cool_id<\/a>: Generates user-friendly identifiers, like &ldquo;prod_1n4dl,&rdquo; for external use.<\/li>\n<li><a href=\"https:\/\/hotwirecombobox.com\">hotwire_combobox<\/a>: Makes it easy to create dynamic, user-friendly dropdown selectors.<\/li>\n<\/ul>\n<h2 id=\"services\">Services<\/h2>\n<ul>\n<li><a href=\"https:\/\/rollbar.com\/\">Rollbar<\/a>: Tracks and monitors errors in my applications to keep things running smoothly.<\/li>\n<li><a href=\"https:\/\/betterstack.com\">BetterStack<\/a>: A reliable choice for logging and monitoring server processes.<\/li>\n<li><a href=\"https:\/\/posthog.com\">PostHog<\/a>: Analyzes user behavior and helps me make data-driven decisions.<\/li>\n<li><a href=\"https:\/\/stripe.com\/en-ca\">Stripe<\/a>: Reliable and robust payment processing for handling transactions.<\/li>\n<li><a href=\"https:\/\/tailwindui.com\">Tailwindcss UI<\/a>: A fantastic library of prebuilt UI components that speed up development and look great.<\/li>\n<\/ul>\n<p>These are some of the tools that help me stay productive and efficient. What tools do you use day-to-day? I\u2019d love to hear about them!<\/p>"},{"title":"Building Adaptive Layouts with Hotwire Native","link":"https:\/\/jbennett.me\/articles\/building-adaptive-layouts-with-hotwire-native\/","pubDate":"Thu, 09 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/building-adaptive-layouts-with-hotwire-native\/","description":"<p>I&rsquo;m building a <a href=\"https:\/\/native.hotwired.dev\">Hotwire Native<\/a> app with two distinct layouts:<\/p>\n<ol>\n<li>A single navigation controller for guests.<\/li>\n<li>A tab bar for signed-in users.<\/li>\n<\/ol>\n<p>The key challenge is determining on the client side whether a user is signed in. Thanks to Hotwire Native&rsquo;s shared process pool, which shares cookies across windows and network requests, we can achieve this by querying the server and adapting the layout dynamically.<\/p>\n<hr>\n<h2 id=\"step-1-server-side-endpoint\">Step 1: Server-Side Endpoint<\/h2>\n<p>We&rsquo;ll create an endpoint to check the user&rsquo;s session status:<\/p>"},{"title":"A Guide to Creating a Push Testing Interface","link":"https:\/\/jbennett.me\/articles\/a-guide-to-creating-a-push-testing-interface\/","pubDate":"Wed, 08 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/a-guide-to-creating-a-push-testing-interface\/","description":"<p>Push messages can be tricky to manage, and giving users clarity about what&rsquo;s happening can save a lot of headaches. Here&rsquo;s how to create a user-friendly interface to:<\/p>\n<ol>\n<li>Show the status of a test push for all subscriptions.<\/li>\n<li>Provide instructions for enabling pushes if no subscriptions exist.<\/li>\n<\/ol>\n<hr>\n<h2 id=\"step-1-add-routes\">Step 1: Add Routes<\/h2>\n<p>First, set up routes for the test interface:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># config\/routes.rb  <\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">Rails<\/span><span style=\"color:#f92672\">.<\/span>application<span style=\"color:#f92672\">.<\/span>routes<span style=\"color:#f92672\">.<\/span>draw <span style=\"color:#66d9ef\">do<\/span>  \n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#75715e\"># \u2026  <\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>  resource <span style=\"color:#e6db74\">:push_test<\/span>, <span style=\"color:#e6db74\">only<\/span>: <span style=\"color:#f92672\">[<\/span><span style=\"color:#e6db74\">:new<\/span>, <span style=\"color:#e6db74\">:create<\/span><span style=\"color:#f92672\">]<\/span>  \n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>  \n<\/span><\/span><\/code><\/pre><\/div><hr>\n<h2 id=\"step-2-build-the-interface\">Step 2: Build the Interface<\/h2>\n<p>Create a <code>PushTestsController<\/code> to handle the test page and trigger actions:<\/p>"},{"title":"Reliable Web Push Delivery with Error Handling","link":"https:\/\/jbennett.me\/articles\/reliable-web-push-delivery\/","pubDate":"Tue, 07 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/reliable-web-push-delivery\/","description":"<p>Notifications usually work behind the scenes, so they don\u2019t typically include much in the way of error handling for users.<\/p>\n<p>Currently, we try to push notifications and silently handle most errors:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># app\/models\/web_push_subscription.rb<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">WebPushSubscription<\/span> <span style=\"color:#f92672\">&lt;<\/span> <span style=\"color:#66d9ef\">ApplicationRecord<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#75715e\"># \u2026<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>  \n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">deliver_payload<\/span>(data)\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">WebPush<\/span><span style=\"color:#f92672\">.<\/span>payload_send(\n<\/span><\/span><span style=\"display:flex;\"><span>\t  <span style=\"color:#e6db74\">message<\/span>: data<span style=\"color:#f92672\">.<\/span>to_json,\n<\/span><\/span><span style=\"display:flex;\"><span>\t  <span style=\"color:#e6db74\">endpoint<\/span>: endpoint,\n<\/span><\/span><span style=\"display:flex;\"><span>\t  <span style=\"color:#e6db74\">p256dh<\/span>: p256dh,\n<\/span><\/span><span style=\"display:flex;\"><span>\t  <span style=\"color:#e6db74\">auth<\/span>: auth,\n<\/span><\/span><span style=\"display:flex;\"><span>\t  <span style=\"color:#e6db74\">vapid<\/span>: {\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#e6db74\">public_key<\/span>: <span style=\"color:#66d9ef\">ENV<\/span><span style=\"color:#f92672\">.<\/span>fetch(<span style=\"color:#e6db74\">&#34;VAPID_PUBLIC&#34;<\/span>),\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#e6db74\">private_key<\/span>: <span style=\"color:#66d9ef\">ENV<\/span><span style=\"color:#f92672\">.<\/span>fetch(<span style=\"color:#e6db74\">&#34;VAPID_PRIVATE&#34;<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t  }\n<\/span><\/span><span style=\"display:flex;\"><span>\t)\n<\/span><\/span><span style=\"display:flex;\"><span>\t\n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#66d9ef\">rescue<\/span> <span style=\"color:#66d9ef\">WebPush<\/span><span style=\"color:#f92672\">::<\/span><span style=\"color:#66d9ef\">ExpiredSubscription<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">Rails<\/span><span style=\"color:#f92672\">.<\/span>logger<span style=\"color:#f92672\">.<\/span>info <span style=\"color:#e6db74\">&#34;Removing expired WebPush subscription&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\tdestroy\n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#66d9ef\">rescue<\/span> <span style=\"color:#66d9ef\">WebPush<\/span><span style=\"color:#f92672\">::<\/span><span style=\"color:#66d9ef\">Unauthorized<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">Rails<\/span><span style=\"color:#f92672\">.<\/span>logger<span style=\"color:#f92672\">.<\/span>info <span style=\"color:#e6db74\">&#34;Removing unauthorized WebPush subscription&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\tdestroy\n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>This approach works well for simply pushing notifications. However, if we want to build a user-facing interface for managing notifications, we need to expose more details about delivery success or failure.<\/p>"},{"title":"A Kamal Tip: Dummy Values for Smooth Deploys","link":"https:\/\/jbennett.me\/articles\/kamal-dummy-environemnt-variables\/","pubDate":"Mon, 06 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/kamal-dummy-environemnt-variables\/","description":"<p>Quick detour from our notifications journey to share a handy Kamal tip!<\/p>\n<p>Have you ever encountered an error during deployment because of a missing environment variable, even though it\u2019s set correctly? The issue might stem from how Kamal handles environment variables during its deployment process.<\/p>\n<p>Here\u2019s what happens when Kamal deploys:<\/p>\n<ol>\n<li>Creates an image for your application.<\/li>\n<li>Uploads the image to your repository (e.g., DockerHub).<\/li>\n<li>Downloads the image on the server(s).<\/li>\n<li>Boots new containers with the environment variables applied.<\/li>\n<\/ol>\n<p><strong>The Problem:<\/strong> Environment variables are only included in Step #4. If your application relies on <code>ENV.fetch('FOOBAR')<\/code> during the image-building process (like asset precompilation), it will fail in Step #1 because those variables aren&rsquo;t available yet.<\/p>"},{"title":"Custom Web Push Delivery Method in Noticed","link":"https:\/\/jbennett.me\/articles\/custom-web-push-delivery-method\/","pubDate":"Fri, 03 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/custom-web-push-delivery-method\/","description":"<p>Currently, there\u2019s no built-in support for web push notifications in Rails. Fortunately, we\u2019ve already done most of the groundwork to integrate it seamlessly with <strong>Noticed<\/strong>.<\/p>\n<p>To make this work, we\u2019ll create a custom delivery method for web push notifications. Our goal is to end up with a configuration like this:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">NewUserNotifier<\/span> <span style=\"color:#f92672\">&lt;<\/span> <span style=\"color:#66d9ef\">ApplicationNotifier<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>  deliver_by <span style=\"color:#e6db74\">:email<\/span> <span style=\"color:#75715e\"># Existing delivery method<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span>  deliver_by <span style=\"color:#e6db74\">:web_push<\/span>, <span style=\"color:#e6db74\">class<\/span>: <span style=\"color:#e6db74\">&#34;DeliveryMethods::WebPush&#34;<\/span> <span style=\"color:#66d9ef\">do<\/span> <span style=\"color:#f92672\">|<\/span>config<span style=\"color:#f92672\">|<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\tconfig<span style=\"color:#f92672\">.<\/span>json <span style=\"color:#f92672\">=<\/span> <span style=\"color:#f92672\">-&gt;<\/span> <span style=\"color:#66d9ef\">do<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t  {\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#e6db74\">title<\/span>: <span style=\"color:#e6db74\">&#34;New user signed up&#34;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#e6db74\">body<\/span>: <span style=\"color:#e6db74\">&#34;This is the body of the notification&#34;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#e6db74\">url<\/span>: user_profile_path(record),\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#e6db74\">badge_count<\/span>: recipient<span style=\"color:#f92672\">.<\/span>notifications<span style=\"color:#f92672\">.<\/span>unread<span style=\"color:#f92672\">.<\/span>count\n<\/span><\/span><span style=\"display:flex;\"><span>\t  }\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><h2 id=\"creating-the-custom-delivery-method\">Creating the Custom Delivery Method<\/h2>\n<p>The custom delivery method allows us to set the notification&rsquo;s title and optionally include additional data such as the body, URL, and badge count using the configuration:<\/p>"},{"title":"Quick Guide: Installing Noticed","link":"https:\/\/jbennett.me\/articles\/installing-noticed\/","pubDate":"Thu, 02 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/installing-noticed\/","description":"<p><a href=\"https:\/\/github.com\/excid3\/noticed\">Noticed<\/a> is a gem designed to simplify managing user notifications. While it might be overkill for today\u2019s task, it shines when you need to send multiple types of notifications (email, push, Slack) to various users, which we will address tomorrow.<\/p>\n<p>For today\u2019s example, we\u2019ll set up email notifications for new user signups. Let\u2019s dive in:<\/p>\n<hr>\n<h2 id=\"step-1-install-noticed\">Step 1: Install Noticed<\/h2>\n<p>First, add noticed to your Gemfile and install it:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-plain\" data-lang=\"plain\"><span style=\"display:flex;\"><span>bundle add noticed\n<\/span><\/span><span style=\"display:flex;\"><span>rails noticed:install:migrations\n<\/span><\/span><span style=\"display:flex;\"><span>rails db:migrate\n<\/span><\/span><\/code><\/pre><\/div><p>This prepares your app for notifications by creating a table to track events and notifications. For example, when a new user signs up, you could notify all admins in your system\u2014creating one event and multiple notifications.<\/p>"},{"title":"Set Up Push Notifications in Rails: Part 4","link":"https:\/\/jbennett.me\/articles\/setup-push-notifications-part-4\/","pubDate":"Wed, 01 Jan 2025 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/setup-push-notifications-part-4\/","description":"<p>When sending push notifications, several errors can occur. The most common are <strong>Unauthorized<\/strong> and <strong>Expired<\/strong> errors. These happen when a subscription becomes invalid, often due to changes in the browser or user actions like disabling notifications.<\/p>\n<p>Here\u2019s how we can handle these errors:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># app\/models\/web_push_subscription.rb<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">WebPushSubscription<\/span> <span style=\"color:#f92672\">&lt;<\/span> <span style=\"color:#66d9ef\">ApplicationRecord<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#75715e\"># \u2026<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">deliver_payload<\/span>(data)\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">WebPush<\/span><span style=\"color:#f92672\">.<\/span>payload_send(\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\t<span style=\"color:#e6db74\">message<\/span>: data<span style=\"color:#f92672\">.<\/span>to_json,\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\t<span style=\"color:#e6db74\">endpoint<\/span>: endpoint,\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\t<span style=\"color:#e6db74\">p256dh<\/span>: p256dh,\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\t<span style=\"color:#e6db74\">auth<\/span>: auth,\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\t<span style=\"color:#e6db74\">vapid<\/span>: {\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\t\t<span style=\"color:#e6db74\">public_key<\/span>: <span style=\"color:#66d9ef\">ENV<\/span><span style=\"color:#f92672\">.<\/span>fetch(<span style=\"color:#e6db74\">&#34;VAPID_PUBLIC&#34;<\/span>),\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\t\t<span style=\"color:#e6db74\">private_key<\/span>: <span style=\"color:#66d9ef\">ENV<\/span><span style=\"color:#f92672\">.<\/span>fetch(<span style=\"color:#e6db74\">&#34;VAPID_PRIVATE&#34;<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\t}\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t)\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">rescue<\/span> <span style=\"color:#66d9ef\">WebPush<\/span><span style=\"color:#f92672\">::<\/span><span style=\"color:#66d9ef\">ExpiredSubscription<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">Rails<\/span><span style=\"color:#f92672\">.<\/span>logger<span style=\"color:#f92672\">.<\/span>info <span style=\"color:#e6db74\">&#34;Removing expired WebPush subscription&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\tdestroy\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">rescue<\/span> <span style=\"color:#66d9ef\">WebPush<\/span><span style=\"color:#f92672\">::<\/span><span style=\"color:#66d9ef\">Unauthorized<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">Rails<\/span><span style=\"color:#f92672\">.<\/span>logger<span style=\"color:#f92672\">.<\/span>info <span style=\"color:#e6db74\">&#34;Removing unauthorized WebPush subscription&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\tdestroy\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>Since there\u2019s no way to fix an invalid, unauthorized, or expired subscription, the example above simply removes it. However, you might consider marking the subscription as inactive instead. This approach allows you to investigate issues later or retry the subscription under specific conditions.<\/p>"},{"title":"Set Up Push Notifications in Rails: Part 3","link":"https:\/\/jbennett.me\/articles\/setup-push-notifications-part-3\/","pubDate":"Tue, 31 Dec 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/setup-push-notifications-part-3\/","description":"<p>Currently, push subscription tokens are only sent to the backend when the subscription is first created. However, tokens can occasionally update in the background, so it\u2019s important to ensure the server has the latest version.<\/p>\n<p>Here\u2019s how we can keep the backend up-to-date with any token changes:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-js\" data-lang=\"js\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ app\/javascript\/push_notifications_controller.js\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"><\/span><span style=\"color:#66d9ef\">export<\/span> <span style=\"color:#66d9ef\">default<\/span> <span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#66d9ef\">extends<\/span> <span style=\"color:#a6e22e\">Controller<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#a6e22e\">connect<\/span>() {\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">this<\/span>.<span style=\"color:#a6e22e\">saveSubscription<\/span>()\n<\/span><\/span><span style=\"display:flex;\"><span>\t}\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#75715e\">\/\/ \u2026\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"><\/span>}\n<\/span><\/span><\/code><\/pre><\/div><p>This ensures that the server stays synchronized with the client browser by uploading the token whenever they are available.<\/p>"},{"title":"Set Up Push Notifications in Rails: Part 2","link":"https:\/\/jbennett.me\/articles\/setup-push-notifications-part-2\/","pubDate":"Mon, 30 Dec 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/setup-push-notifications-part-2\/","description":"<p>With the foundation for SSL and push notifications in place, it\u2019s time to enable client-side notifications. Here\u2019s the standard workflow:<\/p>\n<ol>\n<li>Show an &ldquo;Enable Notifications&rdquo; button.<\/li>\n<li>Request user permission.<\/li>\n<li>Create a subscription and submit tokens if permission is granted.<\/li>\n<li>Save tokens on the backend.<\/li>\n<li>Send notifications to the client.<\/li>\n<\/ol>\n<p>Let\u2019s break it down:<\/p>\n<h2 id=\"step-1-show-button\">Step 1: Show Button<\/h2>\n<p>To prevent spam, push notification requests must be triggered by a user action. Additionally, we\u2019ll use the previously created VAPID keys. This logic will live in a Stimulus controller:<\/p>"},{"title":"Set Up Push Notifications in Rails: Part 1","link":"https:\/\/jbennett.me\/articles\/setup-push-notifications-part-1\/","pubDate":"Fri, 27 Dec 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/setup-push-notifications-part-1\/","description":"<p>With SSL set up, let\u2019s dive into setting up push notifications!<\/p>\n<p>The <code>web-push<\/code> gem will handle the heavy lifting, including creating keys and securely communicating with third-party push notification servers. Setting up web-push involves five steps. We\u2019ll cover the first three today:<\/p>\n<ol>\n<li>Add the web-push gem.<\/li>\n<li>Generate secret keys.<\/li>\n<li>Add a service worker to receive notifications on the client.<\/li>\n<li>Add frontend JavaScript to manage the subscription lifecycle.<\/li>\n<li>Add a backend model to store subscriptions.<\/li>\n<\/ol>\n<h2 id=\"step-1-add-the-web-push-gem\">Step 1: Add the web-push Gem<\/h2>\n<p>Add the web-push gem to your project by running: <code>bundle add web-push<\/code><\/p>"},{"title":"SSL: Final Clean Up and Moving Forward","link":"https:\/\/jbennett.me\/articles\/ssl-final-clena-up\/","pubDate":"Thu, 26 Dec 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/ssl-final-clena-up\/","description":"<p>Our SSL setup works, but we need to do a little housekeeping to wrap everything up.<\/p>\n<p>The generated certificates are dependent on your per-machine Mkcert installation. Because of this, we&rsquo;ll want to ensure these files are ignored by Git:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-plain\" data-lang=\"plain\"><span style=\"display:flex;\"><span># .gitignore\n<\/span><\/span><span style=\"display:flex;\"><span>\/localhost-key.pem\n<\/span><\/span><span style=\"display:flex;\"><span>\/localhost.pem\n<\/span><\/span><span style=\"display:flex;\"><span>\/public\/rootCA.pem\n<\/span><\/span><\/code><\/pre><\/div><p>Once this is done, <code>bin\/setup<\/code> will consistently handle SSL setup for the project without adding unnecessary files to version control.<\/p>\n<p>With SSL fully automated, we&rsquo;re ready to move on to setting up notifications for our application.<\/p>"},{"title":"Expand Your Dev Setup to Mobile","link":"https:\/\/jbennett.me\/articles\/expand-yourr-dev-setup-to-mobile\/","pubDate":"Tue, 24 Dec 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/expand-yourr-dev-setup-to-mobile\/","description":"<p>Now that we&rsquo;ve <a href=\"https:\/\/jbennett.me\/articles\/automate-local-https-setup\/\">automated certificate setup<\/a> on our development machine, it\u2019s time to extend this setup to our mobile test devices.<\/p>\n<p>This ensures seamless testing across both your local machine and devices like an iOS simulator or physical mobile devices.<\/p>\n<h2 id=\"sharing-the-mkcert-root-certificate\">Sharing the mkcert Root Certificate<\/h2>\n<p>To test on the iOS simulator, we need to make the root certificate authority accessible. A convenient way to do this is by linking the root certificate to the public folder so it can be easily downloaded:<\/p>"},{"title":"Automate Local HTTPS Setup","link":"https:\/\/jbennett.me\/articles\/automate-local-https-setup\/","pubDate":"Mon, 23 Dec 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/automate-local-https-setup\/","description":"<p>We\u2019ve entered the dreaded \u201cit works on my computer\u201d zone with our HTTPS setup. Let\u2019s take things further by automating the manual steps so anyone on the team can set it up effortlessly.<\/p>\n<p>Rails includes a handy bin\/setup script for first-time project setup. We\u2019ll extend it to handle the mkcert setup for HTTPS automatically:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># bin\/setup<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">FileUtils<\/span><span style=\"color:#f92672\">.<\/span>chdir <span style=\"color:#66d9ef\">APP_ROOT<\/span> <span style=\"color:#66d9ef\">do<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#75715e\"># \u2026<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#e6db74\">`which mkcert`<\/span><span style=\"color:#f92672\">.<\/span>strip<span style=\"color:#f92672\">.<\/span>empty?\n<\/span><\/span><span style=\"display:flex;\"><span>\t\tputs <span style=\"color:#e6db74\">&#34;Please install mkcert (e.g., brew install mkcert)&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\texit <span style=\"color:#ae81ff\">1<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\tsystem <span style=\"color:#e6db74\">&#34;mkcert localhost&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#75715e\"># \u2026<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>This script ensures that mkcert is installed and then creates the necessary certificates for local SSL. No more manual setup!<\/p>"},{"title":"Unlock Push Notifications with This Simple SSL Setup","link":"https:\/\/jbennett.me\/articles\/unlock-push-notifications-with-simple-ssl\/","pubDate":"Thu, 19 Dec 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/unlock-push-notifications-with-simple-ssl\/","description":"<p>Push notifications are a powerful tool, but getting them set up can feel overwhelming. Before diving into notifications themselves, there\u2019s a crucial first step: enabling SSL. Push notifications require a secure connection, even in your local development environment.<\/p>\n<p>The easiest way to enable SSL locally is with <a href=\"https:\/\/github.com\/FiloSottile\/mkcert\">mkcert<\/a>. This tool allows you to trust your own computer as a certificate authority, making it perfect for local testing (and only for local testing).<\/p>"},{"title":"N+1 Queries Aren\u2019t Evil\u2014Here\u2019s How to Use Them Right","link":"https:\/\/jbennett.me\/articles\/n-1-arnt-evil\/","pubDate":"Tue, 17 Dec 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/n-1-arnt-evil\/","description":"<p>The general recommendation is to avoid &ldquo;n+1&rdquo; database queries. What does that look like?<\/p>\n<p>Typically, you make one database request, and then an additional request for each result. For example, fetching <code>Post<\/code> records and their <code>Comments<\/code>:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">Post<\/span> <span style=\"color:#f92672\">&lt;<\/span> <span style=\"color:#66d9ef\">ApplicationRecord<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>  has_many <span style=\"color:#e6db74\">:comments<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">Comment<\/span> <span style=\"color:#f92672\">&lt;<\/span> <span style=\"color:#66d9ef\">ApplicationRecord<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>  belongs_to <span style=\"color:#e6db74\">:post<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><br>\n<pre tabindex=\"0\"><code class=\"language-erb\" data-lang=\"erb\">&lt;% Post.all.each do |post| %&gt;\n  &lt;div&gt;\n    &lt;%= post.body %&gt;\n    &lt;%= render post.comments %&gt;\n  &lt;\/div&gt;\n&lt;% end %&gt;\n<\/code><\/pre><p>This approach can cause serious performance issues as the number of posts grows or as each post becomes more complex.<\/p>"},{"title":"Simplify Rails Forms with One Reusable Module","link":"https:\/\/jbennett.me\/articles\/simplify-rails-forms-with-module\/","pubDate":"Mon, 16 Dec 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/simplify-rails-forms-with-module\/","description":"<p>After trying out form objects like the <code>SignUp<\/code> <a href=\"https:\/\/jbennett.me\/articles\/rails-friendly-form-objects\/\">example from last week<\/a>, you\u2019ll quickly see the value of extracting common logic into a reusable module. Let\u2019s take our <code>SignUp<\/code> class and transform it into a flexible, module that you can apply across your app.<\/p>\n<p>Here&rsquo;s where we are starting:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">SignUp<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">include<\/span> <span style=\"color:#66d9ef\">ActiveModel<\/span><span style=\"color:#f92672\">::<\/span><span style=\"color:#66d9ef\">Model<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">user<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t@user <span style=\"color:#f92672\">||=<\/span> <span style=\"color:#66d9ef\">User<\/span><span style=\"color:#f92672\">.<\/span>new\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">user_attributes<\/span><span style=\"color:#f92672\">=<\/span>(attributes)\n<\/span><\/span><span style=\"display:flex;\"><span>\t\tuser<span style=\"color:#f92672\">.<\/span>assign_attribute(attributes)\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">account<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t@account <span style=\"color:#f92672\">||=<\/span> <span style=\"color:#66d9ef\">Account<\/span><span style=\"color:#f92672\">.<\/span>new\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">account_attributes<\/span><span style=\"color:#f92672\">=<\/span>(attributes)\n<\/span><\/span><span style=\"display:flex;\"><span>\t\taccount<span style=\"color:#f92672\">.<\/span>assign_attribute(attributes)\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">save!<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">ActiveRecord<\/span><span style=\"color:#f92672\">::<\/span><span style=\"color:#66d9ef\">Base<\/span><span style=\"color:#f92672\">.<\/span>transaction <span style=\"color:#66d9ef\">do<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\tuser<span style=\"color:#f92672\">.<\/span>save!\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\taccount<span style=\"color:#f92672\">.<\/span>user <span style=\"color:#f92672\">=<\/span> user\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\taccount<span style=\"color:#f92672\">.<\/span>save!\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><h2 id=\"step-1-extract-a-formobject-module\">Step 1: Extract a FormObject Module<\/h2>\n<p>To simplify and standardize how we create form objects, we\u2019ll move the <code>ActiveModel::Model<\/code> inclusion into a shared <code>FormObject<\/code> module. Here&rsquo;s the updated code:<\/p>"},{"title":"Form Objects: A Rails-Friendly Approach","link":"https:\/\/jbennett.me\/articles\/rails-friendly-form-objects\/","pubDate":"Thu, 12 Dec 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/rails-friendly-form-objects\/","description":"<p><code>accepts_nested_attributes<\/code> is a great tool when working with <code>ActiveRecord<\/code> objects. But what if you&rsquo;re managing complex signup logic with multiple models, like <code>User<\/code> and <code>Account<\/code>? Enter the PORO (Plain Old Ruby Object): a clean, Rails-friendly solution for maintaining separation of concerns while keeping signup logic in one place.<\/p>\n<p>For instance, you might be looking to create a signup form that manages <code>User<\/code> and <code>Account<\/code> objects:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">SignUp<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">user<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t@user <span style=\"color:#f92672\">||=<\/span> <span style=\"color:#66d9ef\">User<\/span><span style=\"color:#f92672\">.<\/span>new\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">account<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t@account <span style=\"color:#f92672\">||=<\/span> <span style=\"color:#66d9ef\">Account<\/span><span style=\"color:#f92672\">.<\/span>new\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>This provides basic support for handling the data. The way we want to use this in our views would be:<\/p>"},{"title":"Using Accessories to Extend Your Kamal Projects","link":"https:\/\/jbennett.me\/articles\/using-accessories-to-extend-your-kamal-projects\/","pubDate":"Tue, 10 Dec 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/using-accessories-to-extend-your-kamal-projects\/","description":"<p>We <a href=\"https:\/\/jbennett.me\/articles\/kamal-isnt-just-for-rails\/\">previously explored<\/a> how to integrate a Docker image into a Kamal project. Did you know you can also use it as an accessory in an existing deployment? This approach extends Kamal&rsquo;s utility, allowing you to enhance your projects with additional services seamlessly.<\/p>\n<p>Kamal accessories can leverage any Docker image to extend your application&rsquo;s capabilities. While DockerHub is the default image source, you can specify other registries, such as ghcr.io, by including the full image address.<\/p>"},{"title":"Mastering Nested Attributes","link":"https:\/\/jbennett.me\/articles\/mastering-nested-attributes\/","pubDate":"Mon, 09 Dec 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/mastering-nested-attributes\/","description":"<p>Here are a few quick tips to help you sidestep some common issues with <code>accepts_nested_attributes<\/code>:<\/p>\n<h2 id=\"1-dont-forget-the-_attributes\">1. Don\u2019t Forget the _attributes<\/h2>\n<p>This catches me all the time\u2014be sure to append <code>_attributes<\/code> when using fields for a nested object.<\/p>\n<p>Example:<\/p>\n<ul>\n<li>In your view: <code>form.fields_for :profile_attributes<\/code><\/li>\n<li>In your controller:\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span>params<span style=\"color:#f92672\">.<\/span>require(<span style=\"color:#e6db74\">:user<\/span>)<span style=\"color:#f92672\">.<\/span>permit(<span style=\"color:#e6db74\">profile_attributes<\/span>: <span style=\"color:#f92672\">[<\/span><span style=\"color:#e6db74\">:name<\/span>, <span style=\"color:#e6db74\">:description<\/span><span style=\"color:#f92672\">]<\/span>)\n<\/span><\/span><\/code><\/pre><\/div><\/li>\n<\/ul>\n<h2 id=\"2-prevent-unintended-object-replacement\">2. Prevent Unintended Object Replacement<\/h2>\n<p>By default, <code>accepts_nested_attributes<\/code> will delete an existing object and create a new one if it receives an ID it doesn\u2019t recognize. This behaviour can unintentionally destroy data or break associations.<\/p>"},{"title":"One Form, Multiple Objects with Rails Magic","link":"https:\/\/jbennett.me\/articles\/one-form-multiple-objects\/","pubDate":"Fri, 06 Dec 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/one-form-multiple-objects\/","description":"<p>If you&rsquo;ve ever needed to handle complex object relationships with a single form, <code>accepts_nested_attributes<\/code> might be exactly what you\u2019re looking for.<\/p>\n<p><code>accepts_nested_attributes<\/code> creates an attribute on the host object that, by default, handles mass assignment for a guest object.<\/p>\n<p>For example, if a <code>User<\/code> class has a <code>Profile<\/code> via a <code>has_one<\/code> relationship, adding <code>accepts_nested_attributes_for :profile<\/code> to the <code>User<\/code> class with create a <code>profile_attributes=<\/code> method. At its core, this method looks like:<\/p>"},{"title":"Kamal Isn\u2019t Just for Rails\u2026","link":"https:\/\/jbennett.me\/articles\/kamal-isnt-just-for-rails\/","pubDate":"Thu, 05 Dec 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/kamal-isnt-just-for-rails\/","description":"<p>Kamal plays wonderfully with Rails, but did you know it\u2019s a general-purpose container tool? I recently set up <a href=\"https:\/\/www.metabase.com\">Metabase<\/a> with Kamal, and while there were minor bumps, the process went smoothly overall. Here\u2019s a walkthrough of how I got it running.<\/p>\n<hr>\n<h2 id=\"1-spin-up-a-machine\">1. Spin Up a Machine<\/h2>\n<p>For this deployment, I used a 2GB RAM Linode instance to handle the workload. After creating the machine:<\/p>\n<ul>\n<li>Noted the public IP address.<\/li>\n<li>Added my SSH key for access.<\/li>\n<li>Configured a firewall to allow HTTP, HTTPS, and SSH traffic only.<\/li>\n<\/ul>\n<hr>\n<h2 id=\"2-set-up-a-local-project\">2. Set Up a Local Project<\/h2>\n<p>Locally, I created a new project directory and initialized it with Kamal:<\/p>"},{"title":"Tame Your Nils","link":"https:\/\/jbennett.me\/articles\/tame-your-nils\/","pubDate":"Wed, 04 Dec 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/tame-your-nils\/","description":"<p>Have you encountered frustrating errors caused by <code>nil<\/code> values in your code? A more reliable solution is to design your code to avoid them whenever possible. One of Ruby\u2019s strengths is its ability to substitute meaningful objects where nil might otherwise appear, allowing for more predictable and resilient behaviour.<\/p>\n<p>For example, imagine a page that lists all chat messages for the current user:<\/p>\n<pre tabindex=\"0\"><code class=\"language-erb\" data-lang=\"erb\">&lt;%= render partial: &#34;chats\/summary&#34;, collection: Current.user.chats %&gt;\n<\/code><\/pre><p>This works well\u2014until the user signs out. At that point, the page fails because <code>Current.user<\/code> is <code>nil<\/code>.<\/p>"},{"title":"This Small Linode Detail Can Save You Hours","link":"https:\/\/jbennett.me\/articles\/fixing-linode-connections\/","pubDate":"Tue, 03 Dec 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/fixing-linode-connections\/","description":"<p>Quick Tip for Linode Managed Databases<\/p>\n<p>If you\u2019re unable to connect to the database <code>akmadmin<\/code> and see an error, it\u2019s because that database isn\u2019t created by default.<\/p>\n<p>To resolve this, connect to the database <code>defaultdb<\/code> first. From there, you can create your desired database.<\/p>\n<p>This can be a small detail, but it\u2019s one that can easily trip you up if you\u2019re not expecting it.<\/p>\n<p>When was the last time you spent <em>far too long<\/em> troubleshooting something so simple?<\/p>"},{"title":"Turbo-Boost Your Toggle Updates","link":"https:\/\/jbennett.me\/articles\/turbo-boost-your-toggle-updates\/","pubDate":"Mon, 02 Dec 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/turbo-boost-your-toggle-updates\/","description":"<p><a href=\"https:\/\/jbennett.me\/articles\/cache-safe-user-toggles\/\">Currently<\/a>, adding a source element to a page refreshes all scopes indiscriminately. While functional, this approach can be optimized to update only the necessary scopes:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-javascript\" data-lang=\"javascript\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ app\/javascript\/controllers\/user_toggle_controller.js\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"><\/span><span style=\"color:#66d9ef\">export<\/span> <span style=\"color:#66d9ef\">default<\/span> <span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#66d9ef\">extends<\/span> <span style=\"color:#a6e22e\">Controller<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#75715e\">\/\/ \u2026\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"><\/span>\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#a6e22e\">sourceTargetConnected<\/span>(<span style=\"color:#a6e22e\">target<\/span>) {\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#75715e\">\/\/ values should always be lists so wrap them if not\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"><\/span>\t\t<span style=\"color:#66d9ef\">const<\/span> <span style=\"color:#a6e22e\">scope<\/span> <span style=\"color:#f92672\">=<\/span> <span style=\"color:#a6e22e\">target<\/span>.<span style=\"color:#a6e22e\">dataset<\/span>.<span style=\"color:#a6e22e\">userToggleScope<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">this<\/span>.<span style=\"color:#a6e22e\">toggleValues<\/span>[<span style=\"color:#a6e22e\">scope<\/span>] <span style=\"color:#f92672\">=<\/span> [].<span style=\"color:#a6e22e\">concat<\/span>(\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t  <span style=\"color:#a6e22e\">JSON<\/span>.<span style=\"color:#a6e22e\">parse<\/span>(<span style=\"color:#a6e22e\">target<\/span>.<span style=\"color:#a6e22e\">dataset<\/span>.<span style=\"color:#a6e22e\">userToggleValues<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t)\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">if<\/span> (<span style=\"color:#66d9ef\">this<\/span>.<span style=\"color:#a6e22e\">autoRemoveValue<\/span>) <span style=\"color:#a6e22e\">target<\/span>.<span style=\"color:#a6e22e\">remove<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#75715e\">\/\/ this.updateToggles()\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"><\/span>\t\t<span style=\"color:#66d9ef\">this<\/span>.<span style=\"color:#a6e22e\">updateScope<\/span>(<span style=\"color:#a6e22e\">scope<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t}\n<\/span><\/span><span style=\"display:flex;\"><span>\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#75715e\">\/\/ updateToggles() {\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"><\/span>\t<span style=\"color:#75715e\">\/\/ \tObject.keys(this.toggleValues).forEach((scope) =&gt; {\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"><\/span>\t<span style=\"color:#75715e\">\/\/ \tthis.toggleTargets\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"><\/span>\t<span style=\"color:#75715e\">\/\/ \t\t.filter((toggle) =&gt; toggle.dataset.userToggleScope === scope)\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"><\/span>\t<span style=\"color:#75715e\">\/\/ \t\t.forEach((toggle) =&gt; {\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"><\/span>\t<span style=\"color:#75715e\">\/\/ \t\tconst value = JSON.parse(toggle.dataset.userToggleShowIf)\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"><\/span>\t<span style=\"color:#75715e\">\/\/ \t\tconst show = this.toggleValues[scope].indexOf(value) !== -1\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"><\/span>\t<span style=\"color:#75715e\">\/\/ \t\ttoggle.classList.toggle(&#34;hidden&#34;, !show)\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"><\/span>\t<span style=\"color:#75715e\">\/\/ \t\t})\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"><\/span>\t<span style=\"color:#75715e\">\/\/ \t})\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"><\/span>\t<span style=\"color:#75715e\">\/\/ }\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"><\/span>\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#a6e22e\">updateScopes<\/span> <span style=\"color:#f92672\">=<\/span> () =&gt; {\n<\/span><\/span><span style=\"display:flex;\"><span>\t\tObject.<span style=\"color:#a6e22e\">keys<\/span>(<span style=\"color:#66d9ef\">this<\/span>.<span style=\"color:#a6e22e\">toggleValues<\/span>).<span style=\"color:#a6e22e\">forEach<\/span>(<span style=\"color:#66d9ef\">this<\/span>.<span style=\"color:#a6e22e\">updateScope<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t}\n<\/span><\/span><span style=\"display:flex;\"><span>\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#a6e22e\">updateScope<\/span> <span style=\"color:#f92672\">=<\/span> (<span style=\"color:#a6e22e\">scope<\/span>) =&gt; {\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">this<\/span>.<span style=\"color:#a6e22e\">toggleTargets<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\t.<span style=\"color:#a6e22e\">filter<\/span>((<span style=\"color:#a6e22e\">toggle<\/span>) =&gt; <span style=\"color:#a6e22e\">toggle<\/span>.<span style=\"color:#a6e22e\">dataset<\/span>.<span style=\"color:#a6e22e\">userToggleScope<\/span> <span style=\"color:#f92672\">===<\/span> <span style=\"color:#a6e22e\">scope<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\t.<span style=\"color:#a6e22e\">forEach<\/span>(<span style=\"color:#66d9ef\">this<\/span>.<span style=\"color:#a6e22e\">updateToggle<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t}\n<\/span><\/span><span style=\"display:flex;\"><span>\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#a6e22e\">updateToggle<\/span> <span style=\"color:#f92672\">=<\/span> (<span style=\"color:#a6e22e\">toggle<\/span>) =&gt; {\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">const<\/span> <span style=\"color:#a6e22e\">scope<\/span> <span style=\"color:#f92672\">=<\/span> <span style=\"color:#a6e22e\">toggle<\/span>.<span style=\"color:#a6e22e\">dataset<\/span>.<span style=\"color:#a6e22e\">userToggleScope<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">const<\/span> <span style=\"color:#a6e22e\">value<\/span> <span style=\"color:#f92672\">=<\/span> <span style=\"color:#a6e22e\">JSON<\/span>.<span style=\"color:#a6e22e\">parse<\/span>(<span style=\"color:#a6e22e\">toggle<\/span>.<span style=\"color:#a6e22e\">dataset<\/span>.<span style=\"color:#a6e22e\">userToggleShowIf<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">const<\/span> <span style=\"color:#a6e22e\">show<\/span> <span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">this<\/span>.<span style=\"color:#a6e22e\">toggleValues<\/span>[<span style=\"color:#a6e22e\">scope<\/span>].<span style=\"color:#a6e22e\">indexOf<\/span>(<span style=\"color:#a6e22e\">value<\/span>) <span style=\"color:#f92672\">!==<\/span> <span style=\"color:#f92672\">-<\/span><span style=\"color:#ae81ff\">1<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#a6e22e\">toggle<\/span>.<span style=\"color:#a6e22e\">classList<\/span>.<span style=\"color:#a6e22e\">toggle<\/span>(<span style=\"color:#e6db74\">&#34;hidden&#34;<\/span>, <span style=\"color:#f92672\">!<\/span><span style=\"color:#a6e22e\">show<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t}\n<\/span><\/span><span style=\"display:flex;\"><span>}\n<\/span><\/span><\/code><\/pre><\/div><p>One key adjustment here is the use of &ldquo;fat arrow syntax (<code>() =&gt; {}<\/code>)&rdquo; for the <code>update*<\/code> functions. This ensures <code>this<\/code> is correctly bound to the controller, whether the functions are executed in loops or called directly.<\/p>"},{"title":"Cache-Safe User Toggles in Rails\u2013Here\u2019s How","link":"https:\/\/jbennett.me\/articles\/cache-safe-user-toggles\/","pubDate":"Fri, 29 Nov 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/cache-safe-user-toggles\/","description":"<p>Toggling links or buttons based on a user\u2019s role is usually straightforward\u2014something like this works great:<\/p>\n<pre tabindex=\"0\"><code class=\"language-erb\" data-lang=\"erb\">&lt;%= link_to &#34;Admin Panel&#34;, &#34;#&#34; if Current.user.admin? %&gt;\n<\/code><\/pre><p>But when caching enters the picture, things get messy. Cached partials can unintentionally expose sensitive features to users who shouldn\u2019t see them or clutter the interface with irrelevant options. The same cached content is served to everyone, regardless of their role.<\/p>\n<p>Fortunately, there\u2019s a simple, cache-safe solution to keep your UI clean, secure, and user-specific: always include the link (hidden by default) and use JavaScript to show it when appropriate. Here\u2019s how I set this up.<\/p>"},{"title":"A Simple Way to Share Your Rails Config with JS","link":"https:\/\/jbennett.me\/articles\/simple-js-config-from-rails\/","pubDate":"Thu, 28 Nov 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/simple-js-config-from-rails\/","description":"<p>Rails has a cornucopia of options to access configuration values in your application. JavaScript, not so much.<\/p>\n<p>What I&rsquo;ve done is standardized on storing and retrieving values from meta tags in the <code>&lt;head&gt;<\/code>:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># app\/helpers\/application_helper.rb  <\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">js_config<\/span>(name, value)  \n<\/span><\/span><span style=\"display:flex;\"><span>  tag<span style=\"color:#f92672\">.<\/span>meta name: name, <span style=\"color:#e6db74\">content<\/span>: value  \n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>  \n<\/span><\/span><\/code><\/pre><\/div><h2 id=\"use-in-your-views\">Use in Your Views<\/h2>\n<pre tabindex=\"0\"><code class=\"language-erb\" data-lang=\"erb\">&lt;% content_for :head do %&gt;  \n  &lt;%= js_config &#34;userId&#34;, Current.user.id %&gt;  \n&lt;% end %&gt;  \n<\/code><\/pre><h2 id=\"access-values-from-javascript\">Access Values from Javascript<\/h2>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-javascript\" data-lang=\"javascript\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ app\/javascript\/helpers\/configuration.js  \n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"><\/span><span style=\"color:#66d9ef\">export<\/span> <span style=\"color:#66d9ef\">function<\/span> <span style=\"color:#a6e22e\">getConfig<\/span>(<span style=\"color:#a6e22e\">name<\/span>) {  \n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#66d9ef\">return<\/span> document.<span style=\"color:#a6e22e\">head<\/span>  \n<\/span><\/span><span style=\"display:flex;\"><span>\t.<span style=\"color:#a6e22e\">querySelector<\/span>(<span style=\"color:#e6db74\">`meta[name=&#34;<\/span><span style=\"color:#e6db74\">${<\/span><span style=\"color:#a6e22e\">name<\/span><span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\">&#34;]`<\/span>)  \n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#f92672\">?<\/span>.<span style=\"color:#a6e22e\">getAttribute<\/span>(<span style=\"color:#e6db74\">&#34;content&#34;<\/span>)  \n<\/span><\/span><span style=\"display:flex;\"><span>}\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ Wrap global helpers  \n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"><\/span><span style=\"color:#66d9ef\">export<\/span> <span style=\"color:#66d9ef\">function<\/span> <span style=\"color:#a6e22e\">getUserId<\/span>() {  \n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#66d9ef\">return<\/span> <span style=\"color:#a6e22e\">getConfig<\/span>(<span style=\"color:#e6db74\">&#34;userId&#34;<\/span>)  \n<\/span><\/span><span style=\"display:flex;\"><span>}  \n<\/span><\/span><\/code><\/pre><\/div><h2 id=\"why-meta-tags\">Why Meta Tags?<\/h2>\n<p>This approach avoids adding additional HTTP requests or relying on JSON blobs inserted into the page. It\u2019s lightweight, keeps your app organized, and ensures configuration values are always accessible when you need them.<\/p>"},{"title":"Upgrade Your Tailwind Setup for JS States","link":"https:\/\/jbennett.me\/articles\/upgrade-your-tailwind-js-variants\/","pubDate":"Wed, 27 Nov 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/upgrade-your-tailwind-js-variants\/","description":"<p>Ever wondered how to seamlessly toggle features based on JavaScript availability? Here\u2019s a simple way to ensure your app looks great whether JavaScript is enabled or not.<\/p>\n<p>This method lets you work with CSS variants just like <code>hover:<\/code> and <code>lg:<\/code> \u2014 in only three steps.<\/p>\n<hr>\n<h2 id=\"step-1-add-a-no-js-fallback-class\">Step 1: Add a no-js Fallback Class<\/h2>\n<p>Ensure a <code>no-js<\/code> class is always present on your <code>&lt;body&gt;<\/code> tag. If JavaScript fails to load, the class will remain intact.<\/p>"},{"title":"Kamal + Cron? There's a Better Way","link":"https:\/\/jbennett.me\/articles\/kamal-cron-theres-a-better-way\/","pubDate":"Tue, 26 Nov 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/kamal-cron-theres-a-better-way\/","description":"<p>Migrating cron tasks to a Dockerized environment can be tricky\u2014especially since running cron as a non-root user inside a container isn&rsquo;t straightforward.<\/p>\n<p>Fortunately, there\u2019s a great alternative: Clock. It\u2019s perfect for handling cron-like functionality, especially if you mostly work with rake tasks and job scheduling.<\/p>\n<p>Here\u2019s how to set it up in just three steps:<\/p>\n<h2 id=\"1-add-clock-to-your-gemfile\">1. Add Clock to Your Gemfile<\/h2>\n<p>Add the gem to your project dependencies:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># Gemfile<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>gem <span style=\"color:#e6db74\">&#34;ruby-clock&#34;<\/span>, require: <span style=\"color:#66d9ef\">false<\/span>\n<\/span><\/span><\/code><\/pre><\/div><h2 id=\"2-use-the-clock-dsl-to-define-tasks\">2. Use the Clock DSL to Define Tasks<\/h2>\n<p>Port your existing cron tasks into Clock\u2019s DSL. These definitions should go into a file named Clockfile in the root of your project. Here\u2019s an example:<\/p>"},{"title":"How to Test Local Ruby Gems with Ease","link":"https:\/\/jbennett.me\/articles\/how-to-test-local-gems\/","pubDate":"Mon, 25 Nov 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/how-to-test-local-gems\/","description":"<p>Sharing code between Ruby projects is simple when you package the code into <a href=\"https:\/\/guides.rubygems.org\/what-is-a-gem\/\">gems<\/a>. But keeping your local version synced to the development version can sometimes be a hassle.<\/p>\n<p>While deploying updates as formal releases works for production, testing a gem locally with one of your projects requires a slightly different approach. Here\u2019s an easy way to do it without creating unnecessary headaches:<\/p>\n<hr>\n<p>One common way is to point your Gemfile to the local path of your gem:<\/p>"},{"title":"Quick Tips for a Smooth Kamal Setup","link":"https:\/\/jbennett.me\/articles\/quick-tips-for-kamal-setup\/","pubDate":"Thu, 21 Nov 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/quick-tips-for-kamal-setup\/","description":"<p>If you\u2019re upgrading an existing Rails app with Kamal, here are three tips to help you avoid common pitfalls:<\/p>\n<h2 id=\"1-add-the-up-endpoint-to-configroutesrb\">1. Add the <code>\/up<\/code> Endpoint to <code>config\/routes.rb<\/code><\/h2>\n<p>By default, Kamal uses <code>\/up<\/code> to check if the server is ready. Ensure this is defined in your routes:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span>get <span style=\"color:#e6db74\">&#34;up&#34;<\/span> <span style=\"color:#f92672\">=&gt;<\/span> <span style=\"color:#e6db74\">&#34;rails\/health#show&#34;<\/span>, <span style=\"color:#e6db74\">as<\/span>: <span style=\"color:#e6db74\">:rails_health_check<\/span>\n<\/span><\/span><\/code><\/pre><\/div><h2 id=\"2-handle-ssl-redirects-for-up\">2. Handle SSL Redirects for <code>\/up<\/code><\/h2>\n<p>Since the <code>\/up<\/code> check is an HTTP request (not HTTPS), you might encounter 301 redirect errors if <code>config.force_ssl = true<\/code> is set. To fix this, allow the <code>\/up<\/code> endpoint to bypass SSL redirection:<\/p>"},{"title":"Terrible Ideas Incoming","link":"https:\/\/jbennett.me\/articles\/terrible-ideas-incoming\/","pubDate":"Thu, 21 Nov 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/terrible-ideas-incoming\/","description":"<p>People have bad ideas all the time. Sometimes they share them with me, asking if they should be built.<\/p>\n<p>Not to be outdone, I\u2019ve come up with a few terrible ideas of my own.<\/p>\n<h2 id=\"am-i-online\">Am I Online?<\/h2>\n<p>A simple service to check if your internet is working. If it is, the page will load and say, \u201cYes.\u201d If it\u2019s not, the page\u2026 won\u2019t load. Genius, right?<\/p>\n<h2 id=\"password-strength-checker\">Password Strength Checker<\/h2>\n<p>This revolutionary tool allows you to test the strength of your password. Every password gets the same response: <em>\u201cNot yet.\u201d<\/em><br>\n(<em>Too late\u2014someone already <a href=\"https:\/\/neal.fun\/password-game\/\">gamified<\/a> this.<\/em>)<\/p>"},{"title":"17 Must-Have Rails Gems","link":"https:\/\/jbennett.me\/articles\/17-must-have-gems\/","pubDate":"Wed, 20 Nov 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/17-must-have-gems\/","description":"<p>Rails delivers incredible developer productivity, but it doesn\u2019t cover <em>everything<\/em> out of the box. Here are a few gems I reach for in most projects:<\/p>\n<h2 id=\"general-additions\">General Additions<\/h2>\n<ul>\n<li><strong><a href=\"https:\/\/github.com\/collectiveidea\/audited\">audited<\/a>:<\/strong> Track changes to your ActiveRecord models with ease, making it great for building audit trails.<\/li>\n<li><strong><a href=\"https:\/\/github.com\/ankane\/chartkick\">chartkick<\/a>:<\/strong> Simplify creating interactive charts using JavaScript libraries like Chart.js, Highcharts, and Google Charts.<\/li>\n<li><strong><a href=\"https:\/\/github.com\/schpet\/cool_id\">cool_id<\/a>:<\/strong> Generate short, user-friendly IDs for your models, which are perfect for URLs or sharing.<\/li>\n<li><strong><a href=\"https:\/\/github.com\/flippercloud\/flipper\">flipper<\/a>:<\/strong> Feature flags made easy\u2014toggle features for specific users, groups, or environments.<\/li>\n<li><strong><a href=\"https:\/\/github.com\/josefarias\/hotwire_combobox\">hotwire_combobox<\/a>:<\/strong> Build dynamic combo boxes (dropdowns) using Hotwire for smooth, interactive forms.<\/li>\n<li><strong><a href=\"https:\/\/github.com\/jamesmartin\/inline_svg\">inline_svg<\/a>:<\/strong> Use SVGs in your Rails views with full control, ensuring crisp and scalable images.<\/li>\n<li><strong><a href=\"https:\/\/github.com\/basecamp\/local_time\">local_time<\/a>:<\/strong> Format timestamps for a user\u2019s local time zone with zero hassle.<\/li>\n<li><strong><a href=\"https:\/\/github.com\/ddnexus\/pagy\">pagy<\/a>:<\/strong> A fast, lightweight pagination gem that simplifies managing large collections in views.<\/li>\n<li><strong><a href=\"https:\/\/github.com\/excid3\/noticed\">noticed<\/a> + <a href=\"https:\/\/github.com\/pushpad\/web-push\">web-push<\/a>:<\/strong> Streamline building notifications, including real-time push notifications, with a clean interface, including native mobile push notifications.<\/li>\n<li><strong><a href=\"https:\/\/github.com\/scenic-views\/scenic\">scenic<\/a>:<\/strong> Manage database views in Rails as part of your migrations, keeping database logic maintainable.<\/li>\n<li><strong><a href=\"https:\/\/github.com\/calebhearth\/time_for_a_boolean\">time_for_a_boolean<\/a>:<\/strong> Turn booleans into timestamps\u2014know when a flag was flipped, not just its state.<\/li>\n<li><strong><a href=\"https:\/\/github.com\/ViewComponent\/view_component\">view_component<\/a>:<\/strong> Build reusable, testable UI components in Rails for cleaner, modular views.<\/li>\n<\/ul>\n<h2 id=\"development-tools\">Development Tools<\/h2>\n<ul>\n<li><strong><a href=\"https:\/\/github.com\/bkeepers\/dotenv\">dotenv<\/a>:<\/strong> Manage environment variables with a simple .env file, making local setup a breeze.<\/li>\n<li><strong><a href=\"https:\/\/github.com\/thoughtbot\/factory_bot\">factory_bot<\/a>:<\/strong> Write clear, maintainable factories for your tests instead of seeding the database manually.<\/li>\n<li><strong><a href=\"https:\/\/github.com\/faker-ruby\/faker\">faker<\/a>:<\/strong> Generate fake data for your app during development and testing\u2014names, emails, addresses, and more.<\/li>\n<li><strong><a href=\"https:\/\/github.com\/ryanb\/letter_opener\">letter_opener<\/a>:<\/strong> Open emails sent from your app in the browser instead of actually sending them during development.<\/li>\n<li><strong><a href=\"https:\/\/github.com\/rspec\/rspec-rails\">rspec-rails<\/a>:<\/strong> A must-have for writing clean, powerful tests in a BDD style for your Rails app.<\/li>\n<\/ul>\n<p>These tools help me stay productive, build better apps, and reduce the time to get features shipped.<\/p>"},{"title":"Web Apps: The Better, Faster, Cheaper Option","link":"https:\/\/jbennett.me\/articles\/web-apps-better-faster-cheaper\/","pubDate":"Tue, 19 Nov 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/web-apps-better-faster-cheaper\/","description":"<p>Most apps don\u2019t need to be native. Games and features that rely on native-only functionality, like widgets, are valid reasons to go native.<\/p>\n<p>But for most applications, native development doesn\u2019t offer much. Web-based apps let you move faster, fix issues instantly (no waiting for a new version release), and roll out updates on your schedule.<\/p>\n<p>Modern web apps even support features like push notifications and can be &ldquo;installed&rdquo; directly onto users&rsquo; devices.<\/p>"},{"title":"Apple's Golden Goose Is in Trouble \ud83e\udebf","link":"https:\/\/jbennett.me\/articles\/apples-golden-goose\/","pubDate":"Mon, 18 Nov 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/apples-golden-goose\/","description":"<p>Let\u2019s start with the punchline: <strong>Apple is not a good steward of the App Store<\/strong>. Sure, it\u2019s insanely profitable, but that\u2019s not the point. They\u2019ve got a goose that lays golden eggs\u2014and they\u2019re killing it.<\/p>\n<p>That may sound extreme, especially coming from someone without a trillion-dollar balance sheet. But here are three (of many) reasons why it\u2019s true:<\/p>\n<ol>\n<li>Apple doesn\u2019t adequately protect App Store users.<\/li>\n<li>Apple uses its power punitively.<\/li>\n<li>Apple fails to clearly explain its requirements.<\/li>\n<\/ol>\n<h2 id=\"protecting-users\">Protecting Users<\/h2>\n<p>Apple claims the wide-open internet is too dangerous for most users. And honestly, that\u2019s a reasonable argument.<\/p>"},{"title":"\ud83e\udd67 How Apple Takes a Slice of Every Pie","link":"https:\/\/jbennett.me\/articles\/apple-takes-a-slice-of-every-pie\/","pubDate":"Fri, 15 Nov 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/apple-takes-a-slice-of-every-pie\/","description":"<p>The app stores charge developers a 15\u201330% commission for their services. In comparison, most payment processors charge about 3%, and hosting your app independently costs next to nothing. They &ldquo;justify&rdquo; it by claiming they built the platform and therefore deserve a cut.<\/p>\n<p>But let\u2019s flip the script. If developers suddenly pulled all their third-party apps from the platform, wouldn\u2019t customers leave the platform entirely? By that logic, doesn\u2019t Apple owe developers a cut of iPhone profits? (Spoiler: don\u2019t hold your breath waiting for that check.)<\/p>"},{"title":"Are App Stores a Barrier to Innovation?","link":"https:\/\/jbennett.me\/articles\/are-app-stores-a-barrier-to-innovation\/","pubDate":"Thu, 14 Nov 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/are-app-stores-a-barrier-to-innovation\/","description":"<p>Apple and Google artificially stifle the creativity of software developers. While some operating system restrictions are reasonable\u2014like limiting access to your exact location\u2014many of the limitations imposed do not appear justified.<\/p>\n<p>The software on your phone today is, in some ways, similar to software from the &rsquo;90s, and in other ways, completely different. One of the biggest shifts is the concept of sandboxing.<\/p>\n<p>With sandboxing, your application runs as if no other programs are active on the device. This design brings significant benefits for stability and security but requires different treatment for accessing private and shared resources.<\/p>"},{"title":"Are Apple and Google Really on Your Side?","link":"https:\/\/jbennett.me\/articles\/are-app-stores-on-your-side\/","pubDate":"Wed, 13 Nov 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/are-app-stores-on-your-side\/","description":"<p>Apple and Google may not have your best interests at heart. Sure, they rank high in importance\u2014but it\u2019s not always in your favour.<\/p>\n<p>Here are the three main issues I see:<\/p>\n<ol>\n<li><strong>Limitations on Possibility<\/strong><br>\nTheir review and sandboxing policies often limit what developers can accomplish.<\/li>\n<li><strong>Heavy Developer Tax<\/strong><br>\nThe cost of doing business on their platforms is high\u2014sometimes unreasonably so.<\/li>\n<li><strong>Poor App Store Stewardship<\/strong><br>\nThey often fall short when it comes to maintaining and managing the app stores effectively.<\/li>\n<\/ol>\n<hr>\n<p>Stick with me over the next few days for a deep dive and a few hot takes on this topic!<\/p>"},{"title":"Want a Native App Feel? Try This PWA Feature","link":"https:\/\/jbennett.me\/articles\/want-a-native-app-feel\/","pubDate":"Tue, 12 Nov 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/want-a-native-app-feel\/","description":"<p>Progressive Web Apps (PWAs) are giving the web a lot of superpowers that, until recently, were only available in native apps. Of these features, <strong>\u201cAdd to Home Screen\u201d<\/strong> is probably my favourite.<\/p>\n<p>With <strong>\u201cAdd to Home Screen\u201d<\/strong>, users can install a web app just like they would a native app. What\u2019s even more interesting? It\u2019s actually easier to install than a native application!<\/p>\n<p>If you haven\u2019t spent much time looking at PWAs, especially exploring installation, I highly recommend it. They can pack a lot of punch for the investment.<\/p>"},{"title":"A Quick Way to Give Back to the Ruby Community","link":"https:\/\/jbennett.me\/articles\/a-quick-way-to-give-back\/","pubDate":"Mon, 11 Nov 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/a-quick-way-to-give-back\/","description":"<p>My <a href=\"https:\/\/www.linkedin.com\/posts\/simmonli_find-a-mentor-in-the-ruby-community-activity-7260708736090976256-XSeL\">friend Simmon<\/a> recently reminded me of the <a href=\"https:\/\/firstrubyfriend.org\">First Ruby Friend<\/a> program. If you&rsquo;re not familiar, it\u2019s a 6-month mentorship initiative led by Andy Croll, connecting mentors and mentees in the Ruby community.<\/p>\n<p>The only requirement is a willingness to share and to learn.<\/p>\n<p>As a solo developer, I don\u2019t often get the chance to work with early-career developers. This program is an ideal way for anyone with the desire to help to make a real impact.<\/p>"},{"title":"Why Clever Code is a Hidden Trap","link":"https:\/\/jbennett.me\/articles\/why-clever-code-is-a-trap\/","pubDate":"Fri, 08 Nov 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/why-clever-code-is-a-trap\/","description":"<div class=\"text-center\">\n\t<img src=\"https:\/\/jbennett.me\/images\/its-a-trap.jpg\" alt=\"It's a trap meme\" class=\"w-full md:max-w-[30rem] mx-auto\" \/>\n<\/div>\n<p>Building the most clever solution you can is often incredibly risky. Clever code tends to come with hidden complexities and, inevitably, issues. When that happens, fixing things requires even more cleverness, often putting you in a difficult spot.<\/p>\n<p>This is especially true with AI, where you might not fully understand how the underlying model works or why it makes certain decisions. With code that\u2019s too clever, the risk of encountering a problem you can\u2019t untangle skyrockets.<\/p>"},{"title":"Kamal Quick-Fix Guide: Permissions & DB Issues","link":"https:\/\/jbennett.me\/articles\/first-kamal-deploy\/","pubDate":"Thu, 07 Nov 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/first-kamal-deploy\/","description":"<p>I recently did my first Rails deployment using <a href=\"https:\/\/kamal-deploy.org\">Kamal<\/a>. Fortunately, this was a staging deployment for a new app, so I could work out all the kinks worry-free. Here are a few notes for both your benefit and mine (once I inevitably forget!).<\/p>\n<h2 id=\"docker-permissions\">Docker Permissions<\/h2>\n<p>Right off the bat, I ran into a permissions issue when trying to run Docker commands. This was due to the user for my image being set to ubuntu rather than root. As noted in the <a href=\"https:\/\/kamal-deploy.org\/docs\/configuration\/ssh\/\">Kamal documentation<\/a>, you might need to adjust server settings based on your SSH configuration. To resolve this, add the user to the docker group:<\/p>"},{"title":"What Happened to Monday\u2019s & Tuesday\u2019s Emails?","link":"https:\/\/jbennett.me\/articles\/what-happened-to-my-emails\/","pubDate":"Wed, 06 Nov 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/what-happened-to-my-emails\/","description":"<p>So, <a href=\"https:\/\/jbennett.me\/articles\/why-i-lie\/\">Monday&rsquo;s<\/a> and <a href=\"https:\/\/jbennett.me\/articles\/software-tradeoffs\/\">Tuesday&rsquo;s<\/a> emails didn\u2019t go out as planned\u2014Kit\u2019s RSS Feed automation was automatically disabled.<\/p>\n<p>Turns out, this happened because of a server error on my website that generates the feed. My site generator builds a static site, so it shouldn\u2019t need to run any tasks. The last update even deployed successfully.<\/p>\n<hr>\n<p>After some digging, I found the root cause: a scheduled task that regenerates the site for scheduled posts ran out of memory. \ud83d\ude1e\ud83d\ude2e\u200d\ud83d\udca8<\/p>"},{"title":"Software is About Tradeoffs","link":"https:\/\/jbennett.me\/articles\/software-tradeoffs\/","pubDate":"Tue, 05 Nov 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/software-tradeoffs\/","description":"<p>No piece of sufficiently complicated software is perfect. It isn&rsquo;t perfect for you, me, and especially not for everyone collectively.<\/p>\n<p>For instance, I do handwritten digital note-taking. From my software I want:<\/p>\n<ol>\n<li><strong>Layers:<\/strong> Extra flexibility in layout and creation of templates.<\/li>\n<li><strong>Infinite Length:<\/strong> I want to take a single note for a meeting, not being constrained to the length of a notebook.<\/li>\n<li><strong>OCR:<\/strong> Being able to search my hand written notes would be incredibly helpful.<\/li>\n<li><strong>UI Performance:<\/strong> I shouldn&rsquo;t have to wait for the user interface, it shouldn&rsquo;t get in my way.<\/li>\n<\/ol>\n<table>\n  <thead>\n      <tr>\n          <th><\/th>\n          <th style=\"text-align: left\">Layers<\/th>\n          <th style=\"text-align: left\">Infinite<\/th>\n          <th style=\"text-align: left\">OCR<\/th>\n          <th style=\"text-align: left\">UI<\/th>\n      <\/tr>\n  <\/thead>\n  <tbody>\n      <tr>\n          <td><a href=\"https:\/\/www.getnoteful.com\">Noteful<\/a><\/td>\n          <td style=\"text-align: left\">yes<\/td>\n          <td style=\"text-align: left\">no<\/td>\n          <td style=\"text-align: left\">no<\/td>\n          <td style=\"text-align: left\">yes<\/td>\n      <\/tr>\n      <tr>\n          <td><a href=\"https:\/\/www.goodnotes.com\">GoodNotes<\/a><\/td>\n          <td style=\"text-align: left\">no<\/td>\n          <td style=\"text-align: left\">no<\/td>\n          <td style=\"text-align: left\">yes<\/td>\n          <td style=\"text-align: left\">yes<\/td>\n      <\/tr>\n      <tr>\n          <td><a href=\"https:\/\/notability.com\">Notability<\/a><\/td>\n          <td style=\"text-align: left\">no<\/td>\n          <td style=\"text-align: left\">no<\/td>\n          <td style=\"text-align: left\">yes<\/td>\n          <td style=\"text-align: left\">yes<\/td>\n      <\/tr>\n      <tr>\n          <td><a href=\"https:\/\/remarkable.com\">Remarkable Tablet<\/a><\/td>\n          <td style=\"text-align: left\">yes<\/td>\n          <td style=\"text-align: left\">yes<\/td>\n          <td style=\"text-align: left\">no<\/td>\n          <td style=\"text-align: left\">no<\/td>\n      <\/tr>\n  <\/tbody>\n<\/table>\n<p><em>\/me sighs<\/em><\/p>"},{"title":"Why I \u2018Lie\u2019 About Tech \u2013 and How It Helps You","link":"https:\/\/jbennett.me\/articles\/why-i-lie\/","pubDate":"Mon, 04 Nov 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/why-i-lie\/","description":"<p>Software development is complicated. I&rsquo;ve been doing it for 16 years, and I\u2019m still discovering just how much there is to learn. For non-technical business leaders, the tricky part is that while you can\u2019t ignore these complexities, learning everything in depth isn\u2019t practical either.<\/p>\n<p>This is why I lie to you.<\/p>\n<p>Analogies work well to help you understand key concepts quickly and keep us from getting lost in technical weeds. But analogies always fall apart at some point. In a way, they\u2019re a kind of &ldquo;necessary lie&rdquo;\u2014simple and clear, but not always the full truth.<\/p>"},{"title":"Rails 8 Gives You Options\u2014Build on Your Terms","link":"https:\/\/jbennett.me\/articles\/rails-8-gives-you-options\/","pubDate":"Fri, 01 Nov 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/rails-8-gives-you-options\/","description":"<p>Rails 8 makes it easier and more affordable than ever to launch an app optimized for desktop, tablet, and mobile.<\/p>\n<p>You\u2019ll enjoy smoother testing without the need for lengthy app reviews, and you can introduce new features on your schedule\u2014not someone else\u2019s.<\/p>\n<p>Reducing reliance on third-party platforms also means that you\u2019re free to use them in the early stages, with a clear exit strategy when it\u2019s time to scale independently.<\/p>"},{"title":"Exciting Mobile Features Await in Rails 8","link":"https:\/\/jbennett.me\/articles\/rails-8-mobile-improvements\/","pubDate":"Thu, 31 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/rails-8-mobile-improvements\/","description":"<p>I have to be honest\u2014this might be the part of Rails 8 I\u2019m most excited about. Rails 8 offers fantastic support for mobile devices!<\/p>\n<p>One key addition is support for Progressive Web Apps (PWAs), which allows websites to take advantage of native device features more than ever before. With PWAs, a website can:<\/p>\n<ol>\n<li>Be installed on your device\u2019s home screen.<\/li>\n<li>Access geolocation services.<\/li>\n<li>Receive push notifications.<\/li>\n<li>Support offline mode for continued use without connectivity.<\/li>\n<\/ol>\n<p>Rails 8 also includes support for <a href=\"https:\/\/native.hotwired.dev\">Hotwire Native<\/a>, enabling you to build primarily web-based applications with access to native features\u2014like widgets\u2014where needed.<\/p>"},{"title":"New Rails 8 Defaults You\u2019ll Love","link":"https:\/\/jbennett.me\/articles\/new-rails-8-defaults\/","pubDate":"Wed, 30 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/new-rails-8-defaults\/","description":"<p>One of the standout qualities of Ruby on Rails is the well-thought-out choices it makes by default. The key here is &ldquo;by default&rdquo;\u2014you always have the option to adjust these choices when you have a good reason.<\/p>\n<p>Rails 8 introduces three new defaults to enhance development:<\/p>\n<ol>\n<li>Dev Containers<\/li>\n<li>Brakeman Setup<\/li>\n<li>RuboCop Setup<\/li>\n<\/ol>\n<h2 id=\"dev-containers\">Dev Containers<\/h2>\n<p>Dev containers apply Docker concepts used in deployment to your development environment. By bundling everything needed for development into containers, you can develop across Mac, Windows, or Linux machines with minimal setup. This makes it easier than ever to get a new developer up and running quickly on any platform.<\/p>"},{"title":"What Rails 8 Means For Your Hosting","link":"https:\/\/jbennett.me\/articles\/what-rails-8-means-for-your-hosting\/","pubDate":"Tue, 29 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/what-rails-8-means-for-your-hosting\/","description":"<p>A driving theme behind Rails 8 is that it&rsquo;s the \u201canti-PaaS\u201d release. <em>PaaS<\/em> stands for <strong>Platform as a Service<\/strong>, like <a href=\"https:\/\/heroku.com\">Heroku<\/a>, which charge a premium so you don\u2019t have to manage your own infrastructure. Historically, these platforms have been incredibly valuable, but technology has evolved significantly over the past 20 years, and we&rsquo;re now much less dependent on them.<\/p>\n<p>Rails 8 changes the game with two major innovations: <strong>simplified requirements<\/strong> and <strong>simplified deployments<\/strong>.<\/p>"},{"title":"3 Rails 8 Changes Business Owners Need to Know","link":"https:\/\/jbennett.me\/articles\/3-rails-8-changes\/","pubDate":"Mon, 28 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/3-rails-8-changes\/","description":"<p><strong>Big news in the Rails ecosystem!<\/strong> With Rails World behind us, Rails 8 has been officially announced, bringing exciting changes not just for developers but for startups and small businesses as well.<\/p>\n<p>Major updates like this often have real impacts beyond code. Here are three big changes in Rails 8 that are likely to matter to you as a business owner:<\/p>\n<ol>\n<li><strong>Simplified Operations:<\/strong> New tools to make deployments and scaling easier\u2014meaning smoother, more reliable services for your customers.<\/li>\n<li><strong>Enhanced Developer Tooling:<\/strong> Improved workflows that save your developers time (and keep costs down).<\/li>\n<li><strong>Better Mobile Support:<\/strong> Rails 8 brings updates that make mobile user experiences faster and more seamless.<\/li>\n<\/ol>\n<p>This week, I\u2019ll be covering why Rails 8 isn\u2019t just good news for developers\u2014it\u2019s great news for you, too.<\/p>"},{"title":"What Challenges Are You Facing Right Now?","link":"https:\/\/jbennett.me\/articles\/what-are-your-challenges\/","pubDate":"Fri, 25 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/what-are-your-challenges\/","description":"<p>We\u2019ve covered a lot of ground in recent emails, from reducing review bottlenecks to handling unexpected outages. But enough about my stories\u2014I\u2019d love to hear about yours.<\/p>\n<p>What\u2019s the biggest challenge you\u2019re dealing with right now?<\/p>\n<p>Maybe you\u2019re facing tight deadlines and struggling with prioritizing tasks?\nOr trying to keep things running smoothly while making upgrades?\nOr maybe it\u2019s just a matter of getting more done with fewer resources.\nWhatever it is, hit reply and let me know. I\u2019m always looking for ways to help solve real problems, and your experiences might just be the inspiration for my next tip.<\/p>"},{"title":"The Real Cost of Avoiding Downtime","link":"https:\/\/jbennett.me\/articles\/real-cost-of-avoiding-downtime\/","pubDate":"Thu, 24 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/real-cost-of-avoiding-downtime\/","description":"<p>The hosting provider for my client&rsquo;s application recently made a change to their security setup. Since part of their infrastructure would only ever be accessed internally, they switched to using self-signed security certificates.<\/p>\n<p>Our application was designed to verify certificates to mitigate potential security risks, which now became an issue. The solution was a simple configuration change\u2014stop verifying the certificate. But the bigger question is: what could have been done to prevent the outage?<\/p>"},{"title":"The 4 Root Causes of Outages (and How to Fix Them)","link":"https:\/\/jbennett.me\/articles\/four-root-causes-of-outages\/","pubDate":"Wed, 23 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/four-root-causes-of-outages\/","description":"<p>Once an outage is resolved, it\u2019s essential to figure out the root cause and how to prevent it from happening again.<\/p>\n<p>Root cause analysis helps pinpoint what specifically caused the issue. Keep in mind, this isn\u2019t about assigning blame\u2014it\u2019s about identifying what went wrong to improve the system, not identifying someone to point fingers at.<\/p>\n<p>Typically, the root cause falls into one of these categories:<\/p>\n<p><strong>1. Individual Mistake:<\/strong><br>\nWhen an individual makes a mistake, consider how you can make that mistake impossible in the future. For example, create an admin screen instead of allowing direct database access.<\/p>"},{"title":"Maximize Uptime Without Breaking the Bank","link":"https:\/\/jbennett.me\/articles\/maximize-uptime\/","pubDate":"Tue, 22 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/maximize-uptime\/","description":"<blockquote>\n<p>Only you can prevent <del>forest fires<\/del> outages.<\/p><\/blockquote>\n<p>Everyone wants to prevent outages and bugs, but prevention is only half the equation. The other half is cost.<\/p>\n<p>Some prevention techniques have minimal cost or come with additional benefits, like <a href=\"https:\/\/jbennett.me\/glossary#tdd\">test-driven development<\/a>. Others, like having staff on pager duty, are pure cost.<\/p>\n<p>By focusing on cost-effective prevention techniques, you can maximize your return. You\u2019ll never achieve 100% uptime, so instead, focus on the level of reliability that makes sense for your business.<\/p>"},{"title":"Master Server Downtime Issues...","link":"https:\/\/jbennett.me\/articles\/master-server-downtime\/","pubDate":"Mon, 21 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/master-server-downtime\/","description":"<p>At the end of last week, one of my clients experienced some server downtime. Their internal tool would work for a moment, then fail with an error message. This happened frequently but inconsistently.<\/p>\n<p>Unfortunately, I had just left the office for a family trip about five minutes before the first alert came in. I wouldn\u2019t be able to even look at the problem for another eight hours.<\/p>\n<p>Situations like these always raise two important questions: How do we prevent this in the future? And, just as crucially, How do we handle it better next time?<\/p>"},{"title":"The #1 Momentum Drain in Dev Teams","link":"https:\/\/jbennett.me\/articles\/the-number-1-momentum-drain\/","pubDate":"Fri, 18 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/the-number-1-momentum-drain\/","description":"<p>We&rsquo;ve talked about the value of small, daily deploys and how you can achieve that with feature flags. But let me warn you about the most common issue that prevents these from happening: <strong>long review queues<\/strong>.<\/p>\n<p>Most structured development teams follow a process like Kanban, where tasks move from an <em>idea<\/em> column on the left, through <em>ready<\/em>, <em>in progress<\/em>, <em>review<\/em>, and finally <em>complete<\/em>. The review stage is often where work goes to <em>die<\/em>.<\/p>"},{"title":"How to safely deliver daily","link":"https:\/\/jbennett.me\/articles\/how-to-safetly-deliver\/","pubDate":"Thu, 17 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/how-to-safetly-deliver\/","description":"<p>If you&rsquo;re delivering work daily, many of your incremental improvements won\u2019t be ready for public consumption right away. So, how do you release frequently while keeping unfinished work hidden? The answer lies in the magic of <em>Feature Flags<\/em>.<\/p>\n<p>Feature flags allow you to control which features are visible and when. There are many ways to implement them, but I like <a href=\"https:\/\/www.flippercloud.io\">Flipper<\/a> for Rails projects. Most feature flag systems work by providing a toggle, which lets you switch between different versions of code. For instance, you could use it to display version 1 or 2 of a screen based on the flag&rsquo;s state, without needing to change code.<\/p>"},{"title":"How to Tackle One-Day Projects Effectively","link":"https:\/\/jbennett.me\/articles\/how-to-tackle-one-day-projects\/","pubDate":"Wed, 16 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/how-to-tackle-one-day-projects\/","description":"<p>Yesterday, we talked about why one-day projects are awesome. Today, let\u2019s cover how to tackle them.<\/p>\n<p><strong>Size.<\/strong> That\u2019s the big secret. The only way to consistently deliver one-day projects is to make sure they fit within a single day\u2019s work.<\/p>\n<p>This requires breaking down larger tasks into a collection of smaller, manageable units. For example, I worked with a client who had acquired another company and needed to update their internal system to display prices in CAD, USD, and EUR as appropriate. The currency information was scattered throughout their entire system.<\/p>"},{"title":"The Magic of One-Day Projects","link":"https:\/\/jbennett.me\/articles\/the-magic-of-one-day-projects\/","pubDate":"Tue, 15 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/the-magic-of-one-day-projects\/","description":"<blockquote>\n<p>Hope you took time out to celebrate the one true <a href=\"https:\/\/en.wikipedia.org\/wiki\/Thanksgiving_(Canada)\">Thanksgiving<\/a> yesterday.<\/p><\/blockquote>\n<p>The best projects are the ones that last a single day\u2014or even less.<\/p>\n<p>When a project starts and finishes in one day, there\u2019s no time for it to spiral out of control, get bogged down in endless reviews, or conflict with other tasks. You stay focused, you make progress, and you finish.<\/p>\n<p>Aim for one-day projects. If you\u2019re not sure how to do that, come back tomorrow.<\/p>"},{"title":"Why \"Good Enough\" is Great","link":"https:\/\/jbennett.me\/articles\/good-enough\/","pubDate":"Fri, 11 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/good-enough\/","description":"<p>&ldquo;Good Enough&rdquo; gets a bad rap. It shouldn&rsquo;t.<\/p>\n<p>&ldquo;Good enough&rdquo; is one of the biggest drivers of success. Avoiding perfection means you ship today, not six months from now. Shipping today means you start getting feedback right away. And feedback now means you\u2019ll have a better, more refined product in six months\u2014not just a more polished version of what you started with.<\/p>"},{"title":"Why Your MVP Should Feel Unfinished","link":"https:\/\/jbennett.me\/articles\/your-mvp-should-be-unfinished\/","pubDate":"Thu, 10 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/your-mvp-should-be-unfinished\/","description":"<figure class=\"prose-figcaption:mt-0 prose-figcaption:ml-[22px] prose-p:my-[0]\">\n  <blockquote>If you are not embarrassed by the first version of your product, you&rsquo;ve launched to late<\/blockquote>\n  \n  \n    <figcaption>&mdash; Reid Hoffman, LinkedIn<\/figcaption>\n  \n<\/figure>\n\n\n<p>People often make jokes in response to this quote, like releasing a buggy heart defibrillator that doesn&rsquo;t work. But there are two key problems with this comparison:<\/p>\n<ol>\n<li>That&rsquo;s a catastrophic failure, not an embarrassment.<\/li>\n<li>It completely misses the point of the quote.<\/li>\n<\/ol>\n<p>When you&rsquo;re building an <a href=\"https:\/\/jbennett.me\/glossary#mvp\">MVP<\/a>, your goal is to strip away everything that isn&rsquo;t absolutely necessary. This means leaving out many features you\u2019d love to include.<\/p>"},{"title":"Why You Need to Simplify\u2014Today","link":"https:\/\/jbennett.me\/articles\/simplify\/","pubDate":"Wed, 09 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/simplify\/","description":"<p>Often, the best solution is the least complicated solution.<\/p>\n<ul>\n<li>Consultants will suggest &ldquo;comprehensive&rdquo; solutions\u2014there\u2019s a lot of money to be made in those.<\/li>\n<li>Developers will insist on &ldquo;perfecting&rdquo; the software\u2014resulting in a lot of unnecessary hours spent.<\/li>\n<li>Marketers will push for large, multichannel campaigns\u2014leading to a lot of wasted ad spend.<\/li>\n<\/ul>\n<p>Keep things as simple as possible, and reserve complexity for when it\u2019s truly necessary.<\/p>"},{"title":"The Key to Getting Your Project Budget Right","link":"https:\/\/jbennett.me\/articles\/dont-budget\/","pubDate":"Tue, 08 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/dont-budget\/","description":"<p>Budgets for software projects are always right\u2014but also always wrong. How? It depends entirely on the scope you set.<\/p>\n<p>With any software project, there\u2019s a one-week version, a one-month version, and a one-year version. The problem comes when you try to fit the features of a one-year project into a one-month budget.<\/p>\n<p>Instead, approach your project with a clear &ldquo;appetite&rdquo; for time. If you\u2019re willing to spend one month on it, identify the essential features and focus on those. Once the time is up, so is the project\u2014no overextensions or added scope.<\/p>"},{"title":"Are You Applying Best Practices Wrong?","link":"https:\/\/jbennett.me\/articles\/best-practices\/","pubDate":"Mon, 07 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/best-practices\/","description":"<p>Software development &ldquo;best practices&rdquo; generally fall into two categories: universal truths that apply to all projects, and conditional best practices that only make sense in certain contexts.<\/p>\n<p>Projects often run into trouble when they apply best practices at the wrong time.<\/p>\n<p>Take, for example, a well-factored React Native app using Redux for state management. This setup works perfectly when you have a large team of engineers, as it helps them collaborate without stepping on each other&rsquo;s toes\u2014after all, that&rsquo;s why Facebook created it.<\/p>"},{"title":"Why Feature Copying Could Sink Your Product","link":"https:\/\/jbennett.me\/articles\/beware-featuritis\/","pubDate":"Fri, 04 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/beware-featuritis\/","description":"<p>When building software, it\u2019s common to look at competitors to understand industry trends. However, simply copying your competitor\u2019s features can lead to a dangerous trap known as &ldquo;featuritis&rdquo;\u2014an obsession with adding features for the sake of it.<\/p>\n<h2 id=\"the-unknown\">The Unknown<\/h2>\n<p>The two biggest risks of matching competitors feature-for-feature are:<\/p>\n<ol>\n<li>It doesn&rsquo;t make your product unique.<\/li>\n<li>You have no way of knowing if the feature actually provides value.<\/li>\n<\/ol>\n<h2 id=\"why-uniqueness-matters\">Why Uniqueness Matters<\/h2>\n<p>If your product only mirrors what competitors offer, you\u2019re essentially offering the same thing they are. At that point, you\u2019re competing purely on price or marketing, which is a tough place to be.<\/p>"},{"title":"Outsourcing: Unlock Success or Risk Chaos?","link":"https:\/\/jbennett.me\/articles\/outsourcing-risks\/","pubDate":"Thu, 03 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/outsourcing-risks\/","description":"<p>Outsourcing can be a game-changer for your business, giving you access to talent and expertise without the overhead of full-time hires. But it comes with a catch: many business owners lack the technical know-how to manage or properly assess outsourced team members.<\/p>\n<p>In my experience, I&rsquo;ve been brought in to rescue more than one project from outsourced teams. One particularly memorable case? A project that was meant to last 6 months\u2014but was still dragging on after 42 months. The cost of delay, both in time and resources, was enormous.<\/p>"},{"title":"Why Your Project Keeps Growing (and How to Fix It)","link":"https:\/\/jbennett.me\/articles\/scope-creep\/","pubDate":"Wed, 02 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/scope-creep\/","description":"<p>Yesterday, we talked about <a href=\"https:\/\/jbennett.me\/articles\/spaghetti-code\">spaghetti code<\/a>, which, if we&rsquo;re playing the blame game, is usually the developer&rsquo;s fault. Today, let&rsquo;s dive into <strong>scope creep<\/strong>, a problem that\u2019s often a 50\/50 issue between management and developers.<\/p>\n<p>Scope creep happens when a project&rsquo;s size keeps growing beyond the original requirements. It\u2019s insidious because of its slow, gradual nature. If a project\u2019s scope suddenly exploded, it wouldn\u2019t be tolerated. But with scope creep, since each new request seems small and reasonable, it\u2019s hard to pinpoint the exact moment when things went off the rails. There\u2019s no clear \u201cstraw that broke the camel\u2019s back\u201d to blame\u2014it all just adds up over time.<\/p>"},{"title":"Rails World 2024 Recap: Key Business Benefits","link":"https:\/\/jbennett.me\/articles\/rails-world-2024\/","pubDate":"Tue, 01 Oct 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/rails-world-2024\/","description":"<blockquote>\n<p>As we enter Q4, time is running out to meet your end-of-year goals. If you&rsquo;re looking to wrap up any critical initiatives, now&rsquo;s the time. <a href=\"https:\/\/tidycal.com\/jbennett\/book-time-with-jonathan\">Book a call<\/a> to explore how I can help you make the final push.<\/p><\/blockquote>\n<p>Last week, Rails World 2024 took place, and they&rsquo;ve released the <a href=\"https:\/\/www.youtube.com\/watch?v=-cEn_83zRFw\">keynote video<\/a>. It&rsquo;s packed with exciting technical updates, but here are a few key takeaways that matter at the business level.<\/p>"},{"title":"Is Spaghetti Code Wrecking Your Progress?","link":"https:\/\/jbennett.me\/articles\/spaghetti-code\/","pubDate":"Mon, 30 Sep 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/spaghetti-code\/","description":"<p>&ldquo;There&rsquo;s nothing new under the sun&rdquo; applies to software issues too. Some problems stem from how the code was written, and others from how the project is managed. But here&rsquo;s the key takeaway: a handful of issues are responsible for most of the challenges teams face.<\/p>\n<p>This week, we&rsquo;ll cover some of the most common issues, so you can spot them early and, more importantly, avoid them.<\/p>\n<h2 id=\"spaghetti-code\">Spaghetti Code<\/h2>\n<p>Our first culprit is something most of us have encountered: <strong>Spaghetti Code<\/strong>. It&rsquo;s the kind of code that\u2019s so tangled it\u2019s nearly impossible to read, understand, or improve.<\/p>"},{"title":"Team Types: Why One Size Doesn't Fit All","link":"https:\/\/jbennett.me\/articles\/one-size-doesnt-fit-all\/","pubDate":"Fri, 27 Sep 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/one-size-doesnt-fit-all\/","description":"<p>We&rsquo;ve talked about the three types of development teams: <a href=\"https:\/\/jbennett.me\/articles\/black-ops-team\/\">black ops<\/a>, <a href=\"https:\/\/jbennett.me\/articles\/soldier-dev-team\/\">soldiers<\/a>, and <a href=\"https:\/\/jbennett.me\/articles\/the-engineer-team\/\">engineers<\/a>.<\/p>\n<p>In the real world, how these groups tackle problems looks very different. Imagine a black ops team running a zip line to get across a chasm, while soldiers would throw together a sturdy bridge, and an engineering team would take their time and build a bridge that lasts for decades.<\/p>\n<p>But try switching their approaches, and you\u2019d have a disaster. The zip line wouldn\u2019t hold up for long-term use, and a full-blown engineering solution would be overkill if you needed to move fast.<\/p>"},{"title":"The Engineers: Slow, Steady, and Reliable","link":"https:\/\/jbennett.me\/articles\/the-engineer-team\/","pubDate":"Thu, 26 Sep 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/the-engineer-team\/","description":"<p>The final type of <a href=\"https:\/\/jbennett.me\/articles\/three-types-of-development-teams\/\">development team<\/a> to discuss is &ldquo;The Engineers.&rdquo;<\/p>\n<p>The Engineer team has their process down to a science\u2014so much so that they wouldn\u2019t go to the bathroom without filling out a TPS report first! While their approach can feel inefficient due to its meticulousness, it&rsquo;s also highly reliable. This method shines when the budget (time, money, and resources) is generous, but the margin for failure is slim to none. For example, imagine writing the software for a rocket ship\u2014bugs are simply not an option in that environment.<\/p>"},{"title":"Is It Time to Expand Your Dev Team?","link":"https:\/\/jbennett.me\/articles\/soldier-dev-team\/","pubDate":"Wed, 25 Sep 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/soldier-dev-team\/","description":"<p>If you need more structure or scale than a <a href=\"https:\/\/jbennett.me\/articles\/black-ops-team\/\">black ops<\/a> dev team can provide, a regular team of soldiers may suit your needs.<\/p>\n<p>Soldier development teams work together as cohesive units and can typically accomplish much more than a black ops team\u2014though with the added cost of management overhead and infrastructure.<\/p>\n<p>Unlike black ops teams, soldiers have responsibilities that extend beyond <em>just<\/em> completing the mission. There\u2019s an expectation of ongoing support, and long-term maintenance.<\/p>"},{"title":"Black Ops Teams: Are They Right for You?","link":"https:\/\/jbennett.me\/articles\/black-ops-team\/","pubDate":"Tue, 24 Sep 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/black-ops-team\/","description":"<p>Our first <a href=\"https:\/\/jbennett.me\/articles\/three-types-of-development-teams\/\">development team archetype<\/a> is the <strong>Black Ops<\/strong> team.<\/p>\n<p>Black ops development teams are excellent for swooping in, solving problems that no one else can crack, and getting out quickly. They are small, nimble, and hyper-focused, allowing them to accomplish far more than their headcount would suggest.<\/p>\n<p>What sets black ops teams apart is not just their deep expertise, but also what they <strong>don\u2019t<\/strong> do. They don&rsquo;t offer ongoing support. They skip nice-to-have features. Their singular focus is on the primary objective\u2014and nothing else.<\/p>"},{"title":"The 3 Development Team Types You Need to Know","link":"https:\/\/jbennett.me\/articles\/three-types-of-development-teams\/","pubDate":"Mon, 23 Sep 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/three-types-of-development-teams\/","description":"<p>Development teams typically fall into one of three archetypes:<\/p>\n<ul>\n<li><a href=\"https:\/\/jbennett.me\/articles\/black-ops-team\">Black ops<\/a><\/li>\n<li><a href=\"https:\/\/jbennett.me\/articles\/soldier-dev-team\">Soldiers<\/a><\/li>\n<li><a href=\"https:\/\/jbennett.me\/articles\/the-engineer-team\/\">The Engineers<\/a><\/li>\n<\/ul>\n<p>Each serves a vital role, and choosing the right one could be the difference between overcoming an impossible challenge or running into unnecessary roadblocks. No team type is inherently better than another\u2014it all depends on the project.<\/p>\n<p>Over the next few days, we\u2019ll break down these archetypes, exploring their strengths, weaknesses, and how to leverage them effectively for your project\u2019s success. Make sure you don\u2019t miss it!<\/p>"},{"title":"What\u2019s Your Project Really Worth?","link":"https:\/\/jbennett.me\/articles\/whats-your-project-really-worth\/","pubDate":"Fri, 20 Sep 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/whats-your-project-really-worth\/","description":"<p>The return on investment (ROI) of a software project can be complicated, especially if you try to calculate it down to the exact penny. However, this level of detail isn&rsquo;t necessary, and I don&rsquo;t recommend aiming for it.<\/p>\n<p>Instead, focus on getting in the right ballpark. Personally, I focus on the first digit and the number of digits. This approach allows for a reasonable estimate, especially when the project has clear and measurable <a href=\"https:\/\/jbennett.me\/articles\/4-objectives-to-ensure-success\/\">objectives<\/a>.<\/p>"},{"title":"Maximize User Wins\u2014Start Now!","link":"https:\/\/jbennett.me\/articles\/maximize-user-wins\/","pubDate":"Thu, 19 Sep 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/maximize-user-wins\/","description":"<p>When building new systems, products, or features, your ultimate goal should be to <strong>Deliver the Win<\/strong>.<\/p>\n<p>Delivering the win happens when the user experiences their first significant value from using the system. Examples might include:<\/p>\n<ul>\n<li>Sending their first mass email from an email tool<\/li>\n<li>Generating their first report from a CRM<\/li>\n<li>Making their first post on a social app<\/li>\n<li>Publishing their first page on a website builder<\/li>\n<\/ul>\n<p>Delivering the win early helps your users quickly appreciate the tool you&rsquo;ve built, motivating them to discover all the other wins they can achieve with your help.<\/p>"},{"title":"4 Objectives to Ensure Success","link":"https:\/\/jbennett.me\/articles\/4-objectives-to-ensure-success\/","pubDate":"Wed, 18 Sep 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/4-objectives-to-ensure-success\/","description":"<p>Projects without clear objectives typically fail. This failure may be catastrophic\u2014where the entire endeavour is scrapped\u2014or partial, where the project is completed but vastly exceeds time or monetary budgets.<\/p>\n<p>When planning a project, consider these four types of objectives and see how they apply to your work.<\/p>\n<h2 id=\"performance\">Performance<\/h2>\n<p>Performance objectives focus on measurable attributes of your system, often with a time-based element. For example:<\/p>\n<ul>\n<li>The number of widgets produced per hour in a factory<\/li>\n<li>Whether reports have real-time data or are delayed by 24 hours<\/li>\n<li>Webpage load times<\/li>\n<\/ul>\n<p>To set performance objectives, you need baseline numbers before starting the project and must continue measuring throughout the project\u2019s lifecycle.<\/p>"},{"title":"Why Most Projects Fail (and How to Avoid It)","link":"https:\/\/jbennett.me\/articles\/why-most-projects-fail\/","pubDate":"Tue, 17 Sep 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/why-most-projects-fail\/","description":"<p>Why do most projects fail? More often than not, it&rsquo;s due to a common reason: the lack of a clear objective.<\/p>\n<ul>\n<li><strong>Increase revenue:<\/strong> How much? By when? Do we need to maintain margins?<\/li>\n<li><strong>Decrease cancellations:<\/strong> Are we ok with keeping more customers but increasing the number on lower tier plans? Are we ok with increasing our spend on the customer success team?<\/li>\n<li><strong>Build new feature \ud835\udcb3:<\/strong> Why? For whom? What problem should this solve? What are the constraints?<\/li>\n<\/ul>\n<p>When a project is completely open ended, it tends to grow without constraint. When this happens, the project never stops growing, and is never completed.<\/p>"},{"title":"Bad App Ideas: Apple Watch Edition","link":"https:\/\/jbennett.me\/articles\/bad-app-ideas-apple-watch\/","pubDate":"Mon, 16 Sep 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/bad-app-ideas-apple-watch\/","description":"<p>Sometimes there are apps that just shouldn&rsquo;t be made. For today, I&rsquo;m going to focus on the Apple Watch. Here are a few of my  ideas.<\/p>\n<h2 id=\"ai-yoga-instructor\">AI Yoga Instructor<\/h2>\n<p>Having your yoga instructor always with you, just a wrist turn away is a game changer. Sure, you might struggle to look at your wrist while doing a downward dog, but consider it an extra opportunity to work on your balance.<\/p>"},{"title":"Write Bug Reports That Wow Your Team!","link":"https:\/\/jbennett.me\/articles\/write-bug-reports-that-wow-your-team\/","pubDate":"Thu, 12 Sep 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/write-bug-reports-that-wow-your-team\/","description":"<p>Good bug reports are wonderful things. They can make the difference in an issue being quickly resolved, or taking 15 back-and-forth emails.<\/p>\n<p>What makes for a good report? There are a few key elements:<\/p>\n<ol>\n<li>A description of what you expected to happen and what actually happened.<\/li>\n<li>A screenshot or video of the error happening.<\/li>\n<li>A link to the page where the error happened if possible.<\/li>\n<li>An exact copy of any error messages you received.<\/li>\n<li>Any other details you find odd ie <em>this worked on Product A but didn&rsquo;t on Product B<\/em>.<\/li>\n<\/ol>\n<p>Providing these details make it much easier to address any issues that could come up.<\/p>"},{"title":"\ud83d\ude31 \"When will the project be done?\"","link":"https:\/\/jbennett.me\/articles\/when-will-the-project-be-done\/","pubDate":"Tue, 10 Sep 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/when-will-the-project-be-done\/","description":"<p>This is one of the hardest questions in a software project. Most custom software is original, meaning no one has solved this problems in this way before. It is hard to give a fixed deadline when you don&rsquo;t know for certain what you are going to make.<\/p>\n<p>That said, there are some unnecessary reasons projects fall off schedule:<\/p>\n<ol>\n<li>Scope creep<\/li>\n<li>Missing definition of done<\/li>\n<li>Defined features not outcomes<\/li>\n<\/ol>\n<h2 id=\"scope-creep\">Scope Creep<\/h2>\n<p>Scope creep is when a project includes work A, B, and C, but later on X, Y, and Z get slipped in. The problem with scope creep is that the project is never finished.<\/p>"},{"title":"3 Questions You Need to Ask During Your Project","link":"https:\/\/jbennett.me\/articles\/three-focus-questions\/","pubDate":"Fri, 06 Sep 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/three-focus-questions\/","description":"<p>The unfortunate reality is that you know the least about a project on day one. All your plans are based on assumptions and you haven&rsquo;t used the new system yet.<\/p>\n<p>Because of this, you should be using the new system ASAP, while it is being built. When using it, you should be keeping an open mind to revise what is being built.<\/p>\n<p>Some questions you can be asking are:<\/p>\n<ol>\n<li>Does this solution solve the problem?<\/li>\n<li>Is there a better way to solve the problem?<\/li>\n<li>Is it still worth solving the problem this way?<\/li>\n<\/ol>\n<h2 id=\"does-it-solve-the-problem\">Does It Solve the Problem?<\/h2>\n<p>After using the (partial) solution, you may discover that it won&rsquo;t do what you need it to do.<\/p>"},{"title":"Stop Your Project's Zombification Now!","link":"https:\/\/jbennett.me\/articles\/stop-your-projects-zombification\/","pubDate":"Thu, 05 Sep 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/stop-your-projects-zombification\/","description":"<p>Too many software projects die early. Even worse, many of these dead projects end up in zombie like state, slowly shuffling forward. The won&rsquo;t fully die, but they certainly aren&rsquo;t alive.<\/p>\n<p>These projects are a drain on your time and money.<\/p>\n<p>How does this happen?<\/p>\n<p>Usually its due to mismanagement of the project from a technical point of view. How do you avoid zombification? Three suggestions:<\/p>\n<ol>\n<li>Use boring technology<\/li>\n<li>Keep your code &ldquo;DRY&rdquo;<\/li>\n<li>Create seams between third party code<\/li>\n<\/ol>\n<h2 id=\"boring-technology\">Boring Technology<\/h2>\n<p>Boring technology means using standard old technology with a proven track record. These choices are typically reliable, well understood, and less expensive. There is almost certainly a bigger company using the same tool, which means the big, hard, and weird problems have already been solved by someone else.<\/p>"},{"title":"Are You Still Guessing the Best Way Run Your Project?","link":"https:\/\/jbennett.me\/articles\/are-you-still-guessing\/","pubDate":"Wed, 04 Sep 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/are-you-still-guessing\/","description":"<p>The start of a new software project, regardless of being a brand new system or adding on to an existing system, can be precarious. By definition this is the least you will have ever used the new features, it doesn&rsquo;t exist yet, and everything you are basing your choices on are assumptions about how it should work.<\/p>\n<p>For small additions, you are likely going to be right. For larger projects, that&rsquo;s not the case.<\/p>"},{"title":"Are You Botching Your Software Projects?","link":"https:\/\/jbennett.me\/articles\/botching-your-software-projects\/","pubDate":"Tue, 03 Sep 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/botching-your-software-projects\/","description":"<p>Project management of software is different than most other types of projects. This is due to the &ldquo;soft&rdquo; part of software. Well designed software is extremely flexible, which is the primary advantage it has over things that are physical.<\/p>\n<p>This shows up in at least three ways:<\/p>\n<ol>\n<li>We often are wrong in our assumptions, so that should be taken into consideration when planning our projects.<\/li>\n<li>Very few decisions need to be final. Good software should maximize flexibility and allow for changes.<\/li>\n<li>Change is inevitable. Good software will let you make changes in the future that are not compatible with what you expect today. You should be able to make these additions without rewriting everything from scratch.<\/li>\n<\/ol>\n<p>You might operate in a world where placing a pipe in an engineering drawing one foot in the wrong direction is an incredibly costly mistake. The &ldquo;cost&rdquo; of a mistake in most software is so much lower that applying the same degree of rigour is not a good return on your investment.<\/p>"},{"title":"Crank Excel to 11 with custom code","link":"https:\/\/jbennett.me\/articles\/crank-excel-to-11\/","pubDate":"Fri, 30 Aug 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/crank-excel-to-11\/","description":"<p>This week we looked at spreadsheets and how they can be a fantastic long term solution to many business problems.<\/p>\n<p>That said, spreadsheets have limitations and you should keep them in mind. More than just a limitation, there comes a time when it might make sense to let your spreadsheet evolve to its next form, custom software. When you let this happen you can quickly access significant advantages:<\/p>\n<ol>\n<li><strong>Multiple Access:<\/strong> Most modern systems are backed by a server and allow simultaneous access to multiple people. Now you don&rsquo;t have to wait for Bob to submit your TPS report. You can even go so far as to do collaboration where multiple people are able to work on the same area at the same time.<\/li>\n<li><strong>Avoid Regressions:<\/strong> A regression is when you fix something and accidentally un-fix it later on. With testing, this is something you can avoid, increasing your confidence in the system.<\/li>\n<li><strong>Storage:<\/strong> While modern hard drives have nearly infinite storage space, Excel can only handle a million rows of data. This might sound like a lot, but it isn&rsquo;t. As your system grows and is used to manage and monitor more parts of your business, total record count growth can explode. Fortunately modern systems have a large number of options for dealing with this growth, even to the point of a billion new rows of data a day being handle gracefully.<\/li>\n<li><strong>User Interface:<\/strong> One of the greatest benefits of custom software is a custom user interface. This means everything the user sees can be customized. Everything the user needs to see can be laid out nicely, and everything that would be a distraction can be hidden. Unlike Excel, you are not stuck with just a grid of coloured cells!<\/li>\n<\/ol>\n<hr>\n<p>Custom solutions can naturally take a great system built on spreadsheets and take it to the next level. Being mindful of what future options are available can help you to make the right decision at the right time.<\/p>"},{"title":"3 Advantages of Custom Software","link":"https:\/\/jbennett.me\/articles\/three-advantages-of-custom-software\/","pubDate":"Thu, 29 Aug 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/three-advantages-of-custom-software\/","description":"<p>Spreadsheets are the original software prototyping tool. We wouldn&rsquo;t have the other software we have today without it.<\/p>\n<p>But there are always trade-offs involved with using these tools. Custom software will cost more and take longer to implement and an off-the-shelf tool or spreadsheet.<\/p>\n<p>So, what are some of the advantages that custom software brings to the table?<\/p>\n<h2 id=\"1-tests\">1. Tests<\/h2>\n<p>Automated testing lets us move forward with our software with confidence. By defining the functionality of our systems in an executable format we can know that our system does what we want it to do.<\/p>"},{"title":"Can You Really Trust Your Spreadsheet\u2026?","link":"https:\/\/jbennett.me\/articles\/can-you-really-trust-your-spreadsheet\/","pubDate":"Wed, 28 Aug 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/can-you-really-trust-your-spreadsheet\/","description":"<p>Every time I work on migrating a mission critical, spreadsheet based system to a custom developed system, there\u2019s always at least one head scratching discovery. More often than not, the history of the change has been lost to the sands of time.<\/p>\n<p>One discovery stands out as my favourite though\u2026<\/p>\n<hr>\n<p>The spreadsheet I was converting was fairly standard. I pulled out the existing data and formulas. It was going well until I started digging into the formulas related to timesheets and invoicing.<\/p>"},{"title":"3 Excel-lent tips for your Wednesday","link":"https:\/\/jbennett.me\/articles\/3-excel-lent-tips-for-your-wednesday\/","pubDate":"Mon, 26 Aug 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/3-excel-lent-tips-for-your-wednesday\/","description":"<p><em>(Sorry for the subject line, didn\u2019t turn off dad-mode before starting to write\u2026)<\/em><\/p>\n<p>You might have noticed that Excel has about 4,237 features in its ribbon. If you\u2019re anything like me, you probably find that overwhelming. Well today,<\/p>\n<p>I\u2019m going to share just three quick tips, no overwhelm involved.<\/p>\n<h2 id=\"1-easy-math\">1. Easy Math<\/h2>\n<p>If you ever need quick access to basic math on a range of cells, you might not even not to create a formula!<\/p>"},{"title":"Do Spreadsheets Make Your \u2764\ufe0f Swoon?","link":"https:\/\/jbennett.me\/articles\/do-spreadsheets-make-your-heart-swoon\/","pubDate":"Mon, 26 Aug 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/do-spreadsheets-make-your-heart-swoon\/","description":"<p>Excel is magic.<\/p>\n<p>There\u2019s no other tool that\u2019s been as profound for businesses than the humble spreadsheet. Even as a custom software developer, I love me an interesting spreadsheet!<\/p>\n<p>In fact, I go as far as to say that every custom system should start life as a small spreadsheet. Feed it a healthy supply of duct tape and chewing gum and it\u2019s anyones guess into what it might grow up into.<\/p>"},{"title":"Your Appetite Trumps Your Estimate","link":"https:\/\/jbennett.me\/articles\/your-appetite-trumps-your-estimate\/","pubDate":"Fri, 23 Aug 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/your-appetite-trumps-your-estimate\/","description":"<p>Yesterday we talked about some of the downsides of estimates. If we are going to avoid them, how will we plan and hit a deadline. There&rsquo;s a simple, effective solution for that: appetite.<\/p>\n<p>An &ldquo;appetite&rdquo; is simply the amount of time and money you would be happy spending to get to an outcome.<\/p>\n<p>This is a three step process:<\/p>\n<ol>\n<li>Pick a cost (time &amp; money) and ask if you would be happy to have the problem solved at that cost.<\/li>\n<li>Repeat #1 until you find the outer edge of comfort. This is your maximum appetite.<\/li>\n<li>Consider how you might be able to solve the problem within that appetite. This is the proposed scope.<\/li>\n<\/ol>\n<p>Note, #3 isn&rsquo;t &ldquo;promise a timeline and specific implementation&rdquo;. It is most likely that you don&rsquo;t know what to make since you haven&rsquo;t worked the problem yet! What is important is that you have an idea for the approach to take.<\/p>"},{"title":"Stop Trusting Estimates Now: See Why","link":"https:\/\/jbennett.me\/articles\/stop-trusting-estimates\/","pubDate":"Thu, 22 Aug 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/stop-trusting-estimates\/","description":"<p>Please stop asking for granular estimates. The are typically useless, and sometimes harmful!<\/p>\n<p>Let&rsquo;s breakdown the true request and think about what actually is being asked.<\/p>\n<h2 id=\"whats-your-estimate-for-this\">&ldquo;What&rsquo;s your estimate for this?&rdquo;<\/h2>\n<p>This question requires background to answer:<\/p>\n<ol>\n<li>How long did it take the last 5 times you did this? Can we use this as an average?<\/li>\n<li>What&rsquo;s different this time that will alter your estimate?<\/li>\n<li>Is there external pressure pushing you towards a certain result?<\/li>\n<\/ol>\n<p>The big question that is <strong>not<\/strong> on this list is &ldquo;what do you not know that will affect your estimate?&rdquo; This question is significant because it might be the largest gap in your understanding, and you cannot answer it beforehand by definition.<\/p>"},{"title":"One Easy Step for Failure\u2026","link":"https:\/\/jbennett.me\/articles\/one-easy-step-for-failure\/","pubDate":"Wed, 21 Aug 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/one-easy-step-for-failure\/","description":"<p>TLDR: Any task assigned to nobody, or to multiple people is all but certain to fail. If you care about seeing a task or goal completed, it must be assigned to one person. Not zero. Not a group. One. Individual.<\/p>\n<p>Can they delegate it to someone else?<br>\nSure. But they are still <strong>responsible<\/strong>.<\/p>\n<p>Can they change the requirements?<br>\nSure. But they must <strong>justify it<\/strong>.<\/p>\n<p>When no one is responsible, or a group is responsible, the outcome is always the same, nothing happens.<\/p>"},{"title":"The Deep Dark Secret of Priorities\u2026","link":"https:\/\/jbennett.me\/articles\/deep-dark-secret-of-priorities\/","pubDate":"Tue, 20 Aug 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/deep-dark-secret-of-priorities\/","description":"<p>\u2026is that they aren&rsquo;t real. As in &ldquo;priorities&rdquo; isn&rsquo;t a thing, only the singular word &ldquo;priority&rdquo;.<\/p>\n<p>There is what you are currently doing, your priority, and everything else.<\/p>\n<p>Instead of building a 4D chess strategy, just pick the most expediently impactful thing on your list and <strong>only do it<\/strong>. Then do that again. By definition, everything else would be subpar.<\/p>\n<blockquote>\n<p>Etymology background: Priority has latin origins meaning &ldquo;first&rdquo;. Hard to have multiple things in first place, hence the word being singular.<\/p>"},{"title":"Have you tried not having a meeting?","link":"https:\/\/jbennett.me\/articles\/how-to-avoid-wasteful-meetings\/","pubDate":"Mon, 19 Aug 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/how-to-avoid-wasteful-meetings\/","description":"<p>Since meetings, especially large, unstructured ones, are low return on investment, what are some alternatives? This depends on the goals of the meeting:<\/p>\n<h2 id=\"share-status\">Share Status<\/h2>\n<p>The status meeting is the most common meeting, and likely the least necessary! Most project management systems should have something in place to indicate if current projects are behind, on, or ahead of schedule. They should also let you provide more descriptive details as needed. If your project management system doesn\u2019t allow for this, a simple email may provide all that is needed:<\/p>"},{"title":"Backing a single field with multiple inputs","link":"https:\/\/jbennett.me\/articles\/multiparameter-attributes\/","pubDate":"Wed, 31 Jul 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/multiparameter-attributes\/","description":"<div class=\"\">\n    <iframe\n        class=\"w-full aspect-video\"\n        src=\"https:\/\/www.youtube.com\/embed\/LCqyP8WcfNQ\"\n        title=\"YouTube video player\"\n        frameborder=\"0\"\n        allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen><\/iframe>\n<\/div>\n\n<p><strong>TLDR:<\/strong><\/p>\n<ol>\n<li>\n<p>Change the inputs to include an index and optional type ie <code>amount<\/code> =&gt; <code>amount(1i)<\/code><\/p>\n<\/li>\n<li>\n<p>Add a setter method in the model and handle a hash input:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-ruby\" data-lang=\"ruby\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">Transaction<\/span> <span style=\"color:#f92672\">&lt;<\/span> <span style=\"color:#66d9ef\">ApplicationRecord<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#75715e\"># \u2026<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">amount<\/span><span style=\"color:#f92672\">=<\/span>(value)\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">if<\/span> value<span style=\"color:#f92672\">.<\/span>is_a? <span style=\"color:#66d9ef\">Hash<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t\t<span style=\"color:#75715e\"># do stuff with value[1] etc<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">super<\/span>(value)\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#75715e\"># \u2026<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">end<\/span>\n<\/span><\/span><\/code><\/pre><\/div><\/li>\n<li>\n<p>Profit<\/p>\n<\/li>\n<\/ol>"},{"title":"Solid Cache","link":"https:\/\/jbennett.me\/articles\/solid-cache\/","pubDate":"Thu, 25 Jul 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/solid-cache\/","description":"<div class=\"\">\n    <iframe\n        class=\"w-full aspect-video\"\n        src=\"https:\/\/www.youtube.com\/embed\/P21yqPlKZUM\"\n        title=\"YouTube video player\"\n        frameborder=\"0\"\n        allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen><\/iframe>\n<\/div>"},{"title":"How to Setup Your Development Environment","link":"https:\/\/jbennett.me\/articles\/setup-your-development-environment\/","pubDate":"Wed, 10 Jul 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/setup-your-development-environment\/","description":"<div class=\"\">\n    <iframe\n        class=\"w-full aspect-video\"\n        src=\"https:\/\/www.youtube.com\/embed\/4SZerrw_Mvk\"\n        title=\"YouTube video player\"\n        frameborder=\"0\"\n        allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen><\/iframe>\n<\/div>\n\n<p>Using and customizing the tools built into Rails to run your development environments is a huge benefit.<\/p>\n<p>Check out how to can use some standard scripts like bin\/dev and seeds.rb to prepare our system, and dev:prime to prepare the development environment specifically.<\/p>\n<p class=\"text-xl font-semibold\">Transcript:<\/p>\n<p>Having a good system in place for your Rails app to get it up and running can make it a lot easier to get going. There&rsquo;s a few things that are built in and a few things that we can add to a Rails application to make onboarding new people or switching to a new computer just to be a lot easier. Looking at a project that I&rsquo;ve got, a side project, this here has a few things that are kind of built in.<\/p>"},{"title":"Using Super with Dynamic Methods","link":"https:\/\/jbennett.me\/articles\/super-with-dynamic-methods\/","pubDate":"Thu, 04 Jul 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/super-with-dynamic-methods\/","description":"<div class=\"\">\n    <iframe\n        class=\"w-full aspect-video\"\n        src=\"https:\/\/www.youtube.com\/embed\/br05q_6_Tvs\"\n        title=\"YouTube video player\"\n        frameborder=\"0\"\n        allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen><\/iframe>\n<\/div>\n\n<p class=\"text-xl font-semibold\">Transcript:<\/p>\n<p>I was working with the <code>rails-money<\/code> gem and I was trying to do some stuff with inheritance.<\/p>\n<p>I was going to do multi-parameter arguments coming in from the form and with that I wanted to do price, I wanted to override the setter for the price.<\/p>\n<p>So this was something like this.<\/p>\n<p>So I had my class, I was pulling from <code>ActiveRecordBase<\/code>, or <code>ApplicationRecord<\/code>.<\/p>"},{"title":"Getting the Most Out of Test Coverage","link":"https:\/\/jbennett.me\/articles\/test-coverage\/","pubDate":"Thu, 27 Jun 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/test-coverage\/","description":"<p>Having a solid test suite is instrumental to having confidence in your system, but how do you know if your test suite is good?<\/p>\n<p>All metrics for evaluating the quality of a test suite have to be applied carefully; if taken to an extreme blindly, they all can cause harm. Common metrics include:<\/p>\n<ul>\n<li>count of lines of test<\/li>\n<li>maintainability of the test suite<\/li>\n<li>speed of the test suite<\/li>\n<li>how much of the code is run by the test suite<\/li>\n<\/ul>\n<p>This last metric, called coverage, is what we will look at today.<\/p>"},{"title":"More Spaghetti, Less Spaghetti Code","link":"https:\/\/jbennett.me\/articles\/less-spaghetti\/","pubDate":"Mon, 17 Jun 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/less-spaghetti\/","description":"<p>There are few meals I enjoy more than pasta. I would eat it basically in basically any form 8 days a week if given the chance. That said, I loath code that resembles a plate of spaghetti.<\/p>\n<blockquote>\n<p>Spaghetti code is code that is needlessly complex due to its structure. It is the digital equivalent of a Rube Goldberg machine<sup id=\"fnref:1\"><a href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\">1<\/a><\/sup>.<\/p><\/blockquote>\n<p>Spaghetti code is <strong>natural and the default<\/strong> for most organizations. This is not the result of a bad programmer. Spaghetti happens when the normal programming process is cut short:<\/p>"},{"title":"Flaky Tests","link":"https:\/\/jbennett.me\/articles\/flaky-tests\/","pubDate":"Fri, 14 Jun 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/flaky-tests\/","description":"<p>Fixing tests that only fail occasionally can be a huge win for your confidence in your test suite.<\/p>\n<p><a href=\"https:\/\/www.codewithjason.com\">Jason Swett<\/a> laid out the 5 categories of issues that lead to <a href=\"https:\/\/www.codewithjason.com\/what-causes-flaky-tests\/\">flaky tests<\/a>, and we&rsquo;ll look through them here:<\/p>\n<div class=\"\">\n    <iframe\n        class=\"w-full aspect-video\"\n        src=\"https:\/\/www.youtube.com\/embed\/gToCykki7GQ\"\n        title=\"YouTube video player\"\n        frameborder=\"0\"\n        allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen><\/iframe>\n<\/div>"},{"title":"Ideal Customer Profile","link":"https:\/\/jbennett.me\/articles\/ideal-customer-profile\/","pubDate":"Wed, 29 May 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/ideal-customer-profile\/","description":"<p>Too many founders come up with a great idea and start moving on it right away. This is almost always a mistake!<\/p>\n<p>When you have a great idea, you will first want to think about who the customer is. Even better, think of a customer, and talk to them about their problems. We do this because ideas are cheap and easy to come up with. It is almost always true that the hard part of a SaaS is getting in front of the right people.<\/p>"},{"title":"You are the Mechanical Turk","link":"https:\/\/jbennett.me\/articles\/you-are-the-mechanical-turk\/","pubDate":"Mon, 27 May 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/you-are-the-mechanical-turk\/","description":"<p>Sometimes building out <em>any<\/em> type of software to test your idea might not be very effective. In these cases, it might be better to just fake it. A good example of this would be where a complicated report needs to be generated.<\/p>\n<p>One approach to this is called a <a href=\"https:\/\/en.wikipedia.org\/wiki\/Mechanical_Turk\">Mechanical Turk<\/a>. With a Mechanical Turk, you take the order automatically but fulfil the order manually behind the scenes. This means that even a complicated process can largely be set up in a way that appears automatic for the end user.<\/p>"},{"title":"Prototyping for Validation","link":"https:\/\/jbennett.me\/articles\/prototypes\/","pubDate":"Fri, 17 May 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/prototypes\/","description":"<p>Creating custom software is time and cost-prohibitive. In addition, we know the least at the start of the project! This means there is a huge incentive to decrease the initial cost of learning, and the speed at which we learn about the problem we are solving.<\/p>\n<p>One fantastic tool to accomplish this is a prototype.<\/p>\n<p>A prototype is a fake, minimal version of the real thing. We eliminate as much of the complexity as we can so that we can focus on the key problem we are looking to solve. This lets us ignore the irrelevant details and we can more quickly and easily experiment with changes. Always remember, speed is the number one feature of a start-up, so having that is essential to success.<\/p>"},{"title":"Interviews","link":"https:\/\/jbennett.me\/articles\/interviews\/","pubDate":"Wed, 15 May 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/interviews\/","description":"<p>TLDR: read <a href=\"https:\/\/www.amazon.ca\/Mom-Test-customers-business-everyone\/dp\/1492180742\" target=\"_blank\" rel=\"noopener noreferrer\">The Mom Test<\/a> and <a href=\"https:\/\/www.amazon.ca\/Deploy-Empathy-practical-interviewing-customers\/dp\/173744660X\" target=\"_blank\" rel=\"noopener noreferrer\">Deploy Empathy<\/a>.<\/p>\n<p>Oh, you&rsquo;re still here? Ok, I&rsquo;ll give you more.<\/p>\n<p>Talking to past, present, and potential customers will help you understand their problems and confirm if your solution will work for them.<\/p>\n<h2 id=\"the-mom-test\">The Mom Test<\/h2>\n<p>The Mom Test talks about how we should talk to customers. The name comes from the truly terrible validation question &ldquo;Mom, my idea is \ud835\udc99. Is it good?&rdquo; <em>Don&rsquo;t ask this<\/em>!<\/p>"},{"title":"Landing Pages","link":"https:\/\/jbennett.me\/articles\/landing-pages\/","pubDate":"Mon, 13 May 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/landing-pages\/","description":"<p>Validating with <a href=\"https:\/\/jbennett.me\/articles\/paid-early-access\">paid early access<\/a> gives you the benefit of direct payment, but other options can still benefit you indirectly. Today we will look at landing pages, what you get out of them, how your customers benefit, how to make them, and some general guidelines<\/p>\n<h1 id=\"what-are-landing-pages\">What are Landing Pages?<\/h1>\n<p>Landing pages are web pages without navigation and are focused on achieving a single result (call-to-action). For most businesses, this would be something like requesting an info sheet or viewing a product demo video. Typically this will be gated by requiring the user&rsquo;s contact information. For validation purposes, it is common to just log interest. This will primarily be the number of visits to the page and hopefully getting the visitor&rsquo;s email address. It&rsquo;s very common to provide a valuable asset to the visitor and offer &ldquo;to keep them updated&rdquo; on the status of the project.<\/p>"},{"title":"Paid Early Access","link":"https:\/\/jbennett.me\/articles\/paid-early-access\/","pubDate":"Fri, 10 May 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/paid-early-access\/","description":"<p>Asking someone for money without a product to give them might seem counterintuitive but it&rsquo;s something we do every day. The trick is coming up with acceptable terms.<\/p>\n<p>When an order is placed at a fast food restaurant, you pay, and they immediately start making the food. The promise is that you&rsquo;ll only have to wait a couple of minutes for the meal.<\/p>\n<p>With Kickstarter you agree to pre-pay, but only if enough others also pay.<\/p>"},{"title":"Product Validation","link":"https:\/\/jbennett.me\/articles\/product-validation\/","pubDate":"Wed, 08 May 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/product-validation\/","description":"<p>When building a SaaS it&rsquo;s always critical to validate the idea before heavily investing in it. Initial validation won&rsquo;t guarantee success but it increases the odds in two ways:<\/p>\n<ol>\n<li>Forces you to know who your customer is and how you can connect with them.<\/li>\n<li>Allows you to more cheaply confirm if your customer wants what you are planning on selling.<\/li>\n<\/ol>\n<h2 id=\"know-your-customer\">Know Your Customer<\/h2>\n<p>Knowing who your customers are and how you can communicate with them is critical for your business. If you come up with the perfect solution to an expensive problem, but you are unable to communicate that to your customers, they will be unable to purchase from you! That is why you must know your customers and how to get ahold of them.<\/p>"},{"title":"Thoughts on Boxes","link":"https:\/\/jbennett.me\/articles\/thoughts-on-ai\/","pubDate":"Wed, 01 May 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/thoughts-on-ai\/","description":"<p>If you&rsquo;re a parent, I&rsquo;m sure you&rsquo;ve had the experience of getting your three-year-old the perfect and coolest toy in the universe for their birthday. They unwrap it with delight and when they realize what is in the box, their face completely lights up.<\/p>\n<p>Three hours later, you find them in the living room. The toy is ignored. They are playing with the toy box.<\/p>\n<hr>\n<p>I look forward to the day when we put down the AI toy box.<\/p>"},{"title":"Critical SaaS Infrastructure","link":"https:\/\/jbennett.me\/articles\/critical-infrastructure\/","pubDate":"Fri, 26 Apr 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/critical-infrastructure\/","description":"<p>Every SaaS requires a basic amount of infrastructure, with a few critical additions as it grows. Most of this can be avoided, but there are serious downside to skipping out, typically to your customer&rsquo;s satisfaction and confidence in your product.<\/p>\n<section>\n  <h2>Table of Contents<\/h2>\n  \n  \n  \n  \n  <nav id=\"TableOfContents\">\n  <ul>\n    <li><a href=\"#phase-1-early-saas\">Phase 1: Early SaaS<\/a>\n      <ul>\n        <li><a href=\"#scalable-hosting\">Scalable Hosting<\/a><\/li>\n        <li><a href=\"#backups\">Backups<\/a><\/li>\n        <li><a href=\"#application-logging\">Application Logging<\/a><\/li>\n        <li><a href=\"#basic-analytics\">Basic Analytics<\/a><\/li>\n        <li><a href=\"#basic-automation\">Basic Automation<\/a><\/li>\n      <\/ul>\n    <\/li>\n    <li><a href=\"#phase-2-product-market-fit\">Phase 2: Product Market Fit<\/a>\n      <ul>\n        <li><a href=\"#realtime-user-monitoring-rum\">Realtime User Monitoring (RUM)<\/a><\/li>\n      <\/ul>\n    <\/li>\n    <li><a href=\"#phase-3-growth\">Phase 3: Growth<\/a>\n      <ul>\n        <li><a href=\"#performance-monitoring\">Performance Monitoring<\/a><\/li>\n      <\/ul>\n    <\/li>\n  <\/ul>\n<\/nav>\n<\/section>\n\n<h2 id=\"phase-1-early-saas\">Phase 1: Early SaaS<\/h2>\n<h3 id=\"scalable-hosting\">Scalable Hosting<\/h3>\n<p>Traditionally hosting required you to buy a physical machine, hook it to the internet, and keep it updated and secure. This had all the inherent complexity of the task, plus there was a ton of additional complexity to manage the system.<\/p>"},{"title":"Are you going the wrong way?","link":"https:\/\/jbennett.me\/articles\/wrong-way\/","pubDate":"Wed, 24 Apr 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/wrong-way\/","description":"<p>Startups need to be laser-focused on their goals and filter all their activities through that lens. Early on, your focus should be on conversations with customers. This is important because if you build the wrong thing and your customers don&rsquo;t want it, well that means they don&rsquo;t want it.<\/p>\n<figure class=\"prose-figcaption:mt-2 prose-figcaption:ml-[22px]\">\n\t<blockquote>Don't waste your time building things customers don't want<\/blockquote>\n\t<figcaption>&mdash; Jonathan Bennett<\/figcaption>\n<\/figure>\n<p>Building the wrong thing is doubly wasteful. There is the money spent building it, and the calendar time burned on it. Startups typically don&rsquo;t have an excess of either!<\/p>"},{"title":"If You Outsource Wrong, You're Just Burning Your Cash","link":"https:\/\/jbennett.me\/articles\/burning-cash\/","pubDate":"Mon, 22 Apr 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/burning-cash\/","description":"<p>If you are a few years into a 6 month project and you have nothing to show, you aren&rsquo;t saving yourself anything.<\/p>\n<p>Outsourcing can be effective, but you need someone on your side of the table who knows if the other side is doing it right.<\/p>\n<a href=\"https:\/\/tidycal.com\/jbennett\/custom-development-consult\">Let&#39;s talk about it<\/a>"},{"title":"Jenga Tower Software Development","link":"https:\/\/jbennett.me\/articles\/jenga-software-development\/","pubDate":"Fri, 19 Apr 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/jenga-software-development\/","description":"<p>Sometimes software is built &ldquo;naturally&rdquo;. By that I mean it is assembled with duct tape and chewing gum. For testing and prototyping this can be fine, but if you are building a production system, this is not the way.<\/p>\n<p>When you glue together an application, this leads to tried and true problems:<\/p>\n<ol>\n<li><strong>Wack-a-mole debugging:<\/strong> Finally fixed the bug thats been haunting you? Great. Unfortunately that caused two new bugs.<\/li>\n<li><strong>Spooky action at a distance:<\/strong> You make a change on Screen A and something breaks on Screen B.<\/li>\n<li><strong>Speed bumps:<\/strong> Speed is great on day one, but it ain&rsquo;t day one any more. As you add more and more features, things naturally slow down, but in a Jenga project things slow down much sooner and much more quickly.<\/li>\n<\/ol>\n<p>There are great, well known solutions to these problems. Check out <a href=\"https:\/\/jbennett.me\/articles\/test-suites\">TDD<\/a> to prevent reoccurring bugs, and architecture and design patterns to avoid spooky actions and maintain development performance.<\/p>"},{"title":"Environments","link":"https:\/\/jbennett.me\/articles\/environments\/","pubDate":"Wed, 17 Apr 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/environments\/","description":"<p>Environments let you run your complete application with different sets of configurations. This is most frequently used to have a local development environment for building your application and a separate environment for customers to access, typically called production. This allows you to connect to a live Stripe account in production, but a test account while in development for example.<\/p>\n<h2 id=\"why-bother\">Why Bother?<\/h2>\n<p>Fair question. Building your application around a flexible configuration means you can easily change how your application runs without having a separate version of the code. What your configuration could change is endless but, the most common things are:<\/p>"},{"title":"What is CI\/CD","link":"https:\/\/jbennett.me\/articles\/what-is-ci\/","pubDate":"Mon, 15 Apr 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/what-is-ci\/","description":"<p>Continuous Integration (CI) is a practice of constantly merging all the code from every developer into a single main version. This is in contrast to old practices where the work from individuals or teams would only be merged at major milestones, typically when completed. This might occur after weeks or months of work and would lead to a huge amount of work to make sure that it could be done correctly without messing up the work of others. With CI, this might happen multiple times a day.<\/p>"},{"title":"What Does it Mean to \"Deploy\"","link":"https:\/\/jbennett.me\/articles\/what-is-deployment\/","pubDate":"Fri, 12 Apr 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/what-is-deployment\/","description":"<p>Phrases like &ldquo;deploying your code&rdquo; are often thrown around without much explanation. Let&rsquo;s fix that!<\/p>\n<h2 id=\"the-software-development-lifecycle\">The Software Development Lifecycle<\/h2>\n<p>Software is born on a developer&rsquo;s computer, joined in with other developer&rsquo;s work, makes it&rsquo;s way onto test servers, and finally, if everything goes well, ends up on the production server for the entire world to enjoy. These last two steps, going to the test and production server are at the heart of &ldquo;deployment&rdquo;.<\/p>"},{"title":"Test Suites","link":"https:\/\/jbennett.me\/articles\/test-suites\/","pubDate":"Wed, 10 Apr 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/test-suites\/","description":"<p>A test suite is a tool for checking that your system does what you think it should. The good news is that every application has at least one test suite. The bad news is that test suites vary greatly in quality.<\/p>\n<p>Your test site will come in one of three flavours:<\/p>\n<ol>\n<li>User-driven<\/li>\n<li>Manual<\/li>\n<li>Automated<\/li>\n<\/ol>\n<h2 id=\"user-driven-suite\">User-Driven Suite<\/h2>\n<p>User-driven test suites are the default most of the time and are the highest risk. User-driven test suites work by releasing the update, and waiting for users to complain.<\/p>"},{"title":"#1 Startup Metric","link":"https:\/\/jbennett.me\/articles\/number-one-startup-metric\/","pubDate":"Fri, 05 Apr 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/number-one-startup-metric\/","description":"<p>The most important metrics for a startup is not code quality, brand penetration, or getting recognized in magazines. It is the amount of time it takes to learn something about your customer and turn that learning into a change in your business.<\/p>\n<p>Big businesses spend months or year doing studies. You need to listen to your customers and your gut to make changes in days or hours. That is where startups beat the incumbents.<\/p>"},{"title":"Startup Founder Advice","link":"https:\/\/jbennett.me\/articles\/startup-founder-advice\/","pubDate":"Mon, 01 Apr 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/startup-founder-advice\/","description":"<p>I&rsquo;m sure you get a lot of advice, especially if you are a no-technical founder. Here are a few things you can do to make the right decisions to put yourself in a position of &ldquo;great success&rdquo;:<\/p>\n<ol>\n<li><strong>Build something outside your area of expertise:<\/strong> This keeps things exciting and helps make sure you don&rsquo;t lose interest in your work.<\/li>\n<li><strong>Go slow:<\/strong> Take your time and measure things at least 7 times. The smallest of mistakes are likely to doom the company so avoid them <strong>at all costs<\/strong>!<\/li>\n<li><strong>Don&rsquo;t listen to customers:<\/strong> Ageless wisdom from Ford tells us not to listen to our customers. Never. Not even about the problems the are running into. They have nothing to offer us!<\/li>\n<\/ol>\n<blockquote>\n<p>Customers just want faster horses. <strong>Never<\/strong> listen to them!<\/p>"},{"title":"Skip the Native App","link":"https:\/\/jbennett.me\/articles\/skip-the-native-app\/","pubDate":"Mon, 25 Mar 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/skip-the-native-app\/","description":"<p>Most startups shouldn&rsquo;t start with a native app. What you are building most likely doesn&rsquo;t need native <em>only<\/em> features. All you are gaining by going native is increased costs, slower development, and platform risk.<\/p>\n<p>Web Apps are good enough that they cover the general use case and with mobile technologies like <a href=\"https:\/\/turbo.hotwired.dev\/handbook\/native\">Turbo Native<\/a>, and there&rsquo;s an easy escape hatch if you do need something that requires a native app.<\/p>\n<p>Focus on your startup&rsquo;s primary metrics, moving fast and learning from your customers.<\/p>"},{"title":"What is Turbo 8 Morphing?","link":"https:\/\/jbennett.me\/articles\/what-is-turbo-8-morphing\/","pubDate":"Thu, 21 Mar 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/what-is-turbo-8-morphing\/","description":"<p>The pillar addition in Turbo 8 is the adoption of using morphing to update the page. This gives the user benefits of a custom Turbo Stream setup without typically needing to do code up a specific solution. Let&rsquo;s talk through what this looks like and how it works.<\/p>\n<h2 id=\"sample-todo-app\">Sample Todo App<\/h2>\n<p>Thinking through a typical todo app, you would have multiple todo lists, each with todo items, and those todos could be assigned to different people. To complicate things, let&rsquo;s also assume there are a few roll up sections showing complete\/incomplete counts. This could be a total per list, total overall, and totals per user.<\/p>"},{"title":"Turbo 8 Upgrade Guide","link":"https:\/\/jbennett.me\/articles\/turbo-8-upgrade-guide\/","pubDate":"Mon, 18 Mar 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/turbo-8-upgrade-guide\/","description":"<p>Upgrading to Turbo 8 is straightforward for everyone looking to keep up with security updates, and will allow you to take advantage of new features incrementally.<\/p>\n<p>To upgrade, you&rsquo;ll need to adjust your dependencies:<\/p>\n<ol>\n<li>Update Gemfile: <code>gem &quot;turbo-rails&quot;, &quot;~&gt; 2.0&quot;<\/code><\/li>\n<li>Update package.json: <code>&quot;@hotwired\/turbo-rails&quot;: &quot;^8.0.3<\/code><\/li>\n<\/ol>\n<p>Turbo 8 doesn&rsquo;t change much if you are not taking advantage of the new features so this should be a safe upgrade, but double-check with your \n\n\n\n\n\n  \n  \n  \n  \n  \n  \n    <a href=\"https:\/\/jbennett.me\/glossary#test-suite\" title=\"A test suite is an automated set of scripts that test the whole functionality of your code\">test suite<\/a>\n  \n!<\/p>"},{"title":"You Need a Technical Founder","link":"https:\/\/jbennett.me\/articles\/you-need-a-technical-founder\/","pubDate":"Thu, 14 Mar 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/you-need-a-technical-founder\/","description":"<p>Today we get to have story time. Unfortunately, it&rsquo;s a tragedy.<\/p>\n<p>Once upon a time, in a country far, far away (<em>ok, the US is only 20 minutes away<\/em>), there lived a business man, we&rsquo;ll call him Greg to protect the innocent. Greg was having frustrations with the business and couldn&rsquo;t find a tool that solved those problems.<\/p>\n<p>Being the enterprising man he is, he decided to build his own solution. Being a non-technical person he hired and worked with an agency to bring his vision to life. This is usually the right choice for someone in his position. Unfortunately in this case he didn&rsquo;t have the technical expertise to fully qualify the team hired.<\/p>"},{"title":"Apple Still Punching Down in the EU","link":"https:\/\/jbennett.me\/articles\/apple-punching-down\/","pubDate":"Mon, 11 Mar 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/apple-punching-down\/","description":"<p>I have happily used iPhones since getting my 3GS. I got the first iPad, first 12&quot; iPad, first Apple Watch. I run my business on a Mac Studio after upgrading from the iMac Pro. I love my Apple products.<\/p>\n<p>Apple is burning a lot of good will pulling the crap it&rsquo;s pulling in the EU. I understand a lot of the business stuff that they are doing, but the one thing that drives me craziest is how the talk about the 30% developers pay.<\/p>"},{"title":"Custom Software Pricing Models","link":"https:\/\/jbennett.me\/articles\/pricing-custom-software\/","pubDate":"Sat, 09 Mar 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/pricing-custom-software\/","description":"<p>Pricing custom software is hard. Learning and designing everything upfront doesn&rsquo;t work and is inefficient. Not doing any planning is also a recipe for failure. In addition to this, the start of the project is when you know the very least about it.<\/p>\n<p>For pricing software, two models make sense to me: value-based pricing or an ongoing development cost.<\/p>\n<h2 id=\"value-based-pricing\">Value-Based Pricing<\/h2>\n<p>Value-based pricing works well for projects with known, fixed outcomes. The price of the project is set as a portion of the expected benefit for the customer. For instance, I built some reporting automation which was costing about $15,000 annually in direct employee costs, plus indirect costs. Spending about $7,500 was a no-brainer for the client and worked well for me. This paid me well so I wasn&rsquo;t rushing through the job, and since this was a repeat client, I knew I would run into the code so I wanted it to be high quality.<\/p>"},{"title":"Avoid Feature Bloat","link":"https:\/\/jbennett.me\/articles\/avoid-feature-bloat\/","pubDate":"Thu, 07 Mar 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/avoid-feature-bloat\/","description":"<p>The problem with Microsoft Work is its toolbar. It wasn&rsquo;t designed for anybody but for everybody.<\/p>\n<p><img src=\"https:\/\/jbennett.me\/images\/word-toolbar.png\" alt=\"Microsoft Word showing all the toolbar options. Yes all 202 of them.\"><\/p>\n<p>When I design custom software, I make it for <strong>you<\/strong>. It will do less, but that&rsquo;s a feature and its greatest strength.<\/p>\n<p>Unless you have a billion-dollar budget, don&rsquo;t make Microsoft Word. It isn&rsquo;t what you or your business needs.<\/p>"},{"title":"Service Worker Setup","link":"https:\/\/jbennett.me\/articles\/service-worker-setup\/","pubDate":"Tue, 05 Mar 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/service-worker-setup\/","description":"<p>Service workers are a core enabling technology for modern web apps. They enable <a href=\"https:\/\/jbennett.me\/pending\/?term=background%20push\">background push<\/a>, <a href=\"https:\/\/jbennett.me\/pending\/?term=offline%20mode\">offline mode<\/a>, and <a href=\"https:\/\/jbennett.me\/pending\/?term=cached%20assets\">cached assets<\/a>. I&rsquo;ll be covering those in future articles, but for now, let&rsquo;s look at the service worker first. Service workers go through the following stages:<\/p>\n<ol>\n<li><a href=\"#registration\">Registration<\/a><\/li>\n<li><a href=\"#installation\">Installation<\/a><\/li>\n<li><a href=\"#activation\">Activation<\/a><\/li>\n<\/ol>\n<h2 id=\"registration\">Registration<\/h2>\n<p>The website is responsible for letting the browser know what JavaScript to load as a service worker.<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-js\" data-lang=\"js\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">async<\/span> <span style=\"color:#66d9ef\">function<\/span> <span style=\"color:#a6e22e\">registrerServiceWorker<\/span>() {\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">if<\/span> (<span style=\"color:#f92672\">!<\/span><span style=\"color:#a6e22e\">navigator<\/span>.<span style=\"color:#a6e22e\">serviceWorker<\/span>) {\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">return<\/span> <span style=\"color:#66d9ef\">false<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t}\n<\/span><\/span><span style=\"display:flex;\"><span>\t\n<\/span><\/span><span style=\"display:flex;\"><span>\t<span style=\"color:#66d9ef\">try<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">const<\/span> <span style=\"color:#a6e22e\">registration<\/span> <span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">await<\/span> <span style=\"color:#a6e22e\">navigator<\/span>.<span style=\"color:#a6e22e\">serviceWorker<\/span>.<span style=\"color:#a6e22e\">register<\/span>(<span style=\"color:#e6db74\">&#34;\/my-code.js&#34;<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#a6e22e\">console<\/span>.<span style=\"color:#a6e22e\">log<\/span>(<span style=\"color:#e6db74\">&#34;Service worker is registered&#34;<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">return<\/span> <span style=\"color:#a6e22e\">registration<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t} <span style=\"color:#66d9ef\">catch<\/span>(<span style=\"color:#a6e22e\">e<\/span>) {\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#a6e22e\">console<\/span>.<span style=\"color:#a6e22e\">error<\/span>(<span style=\"color:#e6db74\">&#34;Failed to register service worker&#34;<\/span>, <span style=\"color:#a6e22e\">e<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\t\t<span style=\"color:#66d9ef\">return<\/span> <span style=\"color:#66d9ef\">false<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\t}\n<\/span><\/span><span style=\"display:flex;\"><span>}\n<\/span><\/span><\/code><\/pre><\/div><p>The code provides a single JavaScript file which it will load and install. This file can be named anything, but it needs to be in the root folder to avoid <a href=\"#scope\">scope issues<\/a>. It&rsquo;s worth noting that most browsers will ignore caching headers for this file. The browser will do a byte comparison of the files and update the service worker if needed.<\/p>"},{"title":"Noticed v2 Notes","link":"https:\/\/jbennett.me\/articles\/noticed-v2-notes\/","pubDate":"Fri, 01 Mar 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/noticed-v2-notes\/","description":"<p>Noticed v2 is a great update but there are a few things that work differently to make note of:<\/p>\n<ol>\n<li>Purpose of <code>notification_methods<\/code> block<\/li>\n<li>Context for config execution<\/li>\n<\/ol>\n<p>A quick note before jumping in. When you make a subclass of <code>Noticed::Event<\/code>, an additional <a href=\"https:\/\/github.com\/excid3\/noticed\/blob\/d054cc9d3530971ac92a7d4a7237a164814200a7\/app\/models\/concerns\/noticed\/notification_methods.rb#L9\">nested <code>Notification<\/code> class<\/a> is created.<\/p>\n<h2 id=\"1-purpose-of-notification_methods-block\">1. Purpose of <code>notification_methods<\/code> Block<\/h2>\n<p>Because a nested <code>Notification<\/code> class is programatically created, you can&rsquo;t easily access it. That is where the magic of the <code>notification_methods<\/code> block comes in. This block is run in the context of the class allowing you to easily customize and extend the class you didn&rsquo;t directly create. You can see how this is done in the <a href=\"https:\/\/github.com\/excid3\/noticed\/blob\/d054cc9d3530971ac92a7d4a7237a164814200a7\/app\/models\/concerns\/noticed\/notification_methods.rb#L13\"><code>Noticed::NotificaationMethods<\/code><\/a> module.<\/p>"},{"title":"Minimal Impact","link":"https:\/\/jbennett.me\/articles\/minimal-impact\/","pubDate":"Thu, 29 Feb 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/minimal-impact\/","description":"<p class=\"bg-blue-50 px-3 py-2\">Update: Looks like my complaining worked. Apple says <a href=\"https:\/\/www.macrumors.com\/2024\/03\/01\/apple-walks-back-decision-to-disable-eu-web-apps\/\">PWAs are back<\/a>. Glad I could move the needle on that for you.<\/p>\n<p>Apple has been in a long standing argument with the EU over, well just about everything. The most recent spat resulted in a set of pending <a href=\"https:\/\/developer.apple.com\/support\/dma-and-apps-in-the-eu\/\">distribution updates for the App Store<\/a>. I think a healthy debate could be had over if the DMA is good or bad and if Apples compliance is appropriate or not.<\/p>"},{"title":"Doing Time Zones Right","link":"https:\/\/jbennett.me\/articles\/doing-time-zones-right\/","pubDate":"Fri, 09 Feb 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/doing-time-zones-right\/","description":"<p><a href=\"https:\/\/www.youtube.com\/watch?v=-5wpm-gesOY\">Time Zones are the best<\/a> right?<\/p>\n<p>The standard advise is to store all dates in UTC which is a great start. This missing instruction is on the input side of things.<\/p>\n<p>When you add a <code>form.datetime_field<\/code> to your form, this makes a <code>&lt;input type=&quot;datetime_local&quot; \/&gt;<\/code> input which does not include any time zone information when submitted. That means you cannot actually interpret what the time actually is.<\/p>\n<p>To solve this, you need to collect the time zone on the form, or as part of the user account:<\/p>"},{"title":"Chrome PWA Install Prompts","link":"https:\/\/jbennett.me\/articles\/chrome-install-prompt\/","pubDate":"Mon, 05 Feb 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/chrome-install-prompt\/","description":"<p class=\"text-center\"><em><a href=\"#the-code\">TLDR: jump to the code?<\/a><\/em><\/p>\n<p>Chrome offers a fantastic new tool for helping your users install your web app called an install prompt.<\/p>\n<p>Before jumping through the code, let&rsquo;s go over the concept. If your app meets install criteria, the browser will fire on an event. This event has a single use callback which will actually trigger the install prompt for the user.<\/p>\n<p>You will wait for this event and when it comes in you will save the event, and display an appropriate UI to request the install. If the user interacts with the UI, call the install callback. At that point you can do whatever you need to cleanup after yourself.<\/p>"},{"title":"Detecting PWA Mode","link":"https:\/\/jbennett.me\/articles\/detecting-pwa-mode\/","pubDate":"Tue, 30 Jan 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/detecting-pwa-mode\/","description":"<p>Checking if your PWA is running as a normal web page or if it&rsquo;s been installed to the Home Screen or dock is quick and easy from both javscript nad CSS.<\/p>\n<h2 id=\"javascript\">Javascript<\/h2>\n<p>For the javascript side of things, I make a simple function to check the display mode:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-javascript\" data-lang=\"javascript\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">function<\/span> <span style=\"color:#a6e22e\">pwaMode<\/span>() {\n<\/span><\/span><span style=\"display:flex;\"><span>  <span style=\"color:#66d9ef\">return<\/span> window.<span style=\"color:#a6e22e\">mediaMatch<\/span>(<span style=\"color:#e6db74\">&#34;(display-mode: standalone), (display-mode: fullscreen)&#34;<\/span>).<span style=\"color:#a6e22e\">matches<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>}\n<\/span><\/span><\/code><\/pre><\/div><p>This will return true of the app is currently running in standalone or full screen mode, ie you are installed!<\/p>"},{"title":"Getting PWA Installs","link":"https:\/\/jbennett.me\/articles\/getting-pwa-installs\/","pubDate":"Mon, 29 Jan 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/getting-pwa-installs\/","description":"<p>Getting installs of \n\n\n\n\n\n  \n  \n  \n  \n  \n  \n    <a href=\"https:\/\/jbennett.me\/glossary#pwa\" title=\"Progressive Web App\">PWAs<\/a>\n  \n is not easy today, but it is possible! In some ways, its even easier than a native app. People are just not familiar with it.<\/p>\n<p>In case you are unfamiliar with PWA installs, this is a system supported by all modern browsers to add a website to your device&rsquo;s Home Screen or and, and let&rsquo;s the user launch it just like a normal app.<\/p>\n<p>The two big reasons you want your users to install your app are that it:<\/p>"},{"title":"Introducing noticed-web_push","link":"https:\/\/jbennett.me\/articles\/introducing-noticed-web_push\/","pubDate":"Wed, 03 Jan 2024 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/introducing-noticed-web_push\/","description":"<blockquote>\n<p><strong>Update: Sherlocked<\/strong><\/p>\n<p>I&rsquo;m happy to announce that this gem is likely dead. A lot of PWA stuff is going to be build into Rails 8 so the core of this will go away. There will likely be a noticed delivery method that will just use the build in stuff. I&rsquo;m looking to update this gem to give us something very close to the Rails 8 default, but install-able today. I&rsquo;ll keep you posted.<\/p>"},{"title":"DHH WebPush Throwdown","link":"https:\/\/jbennett.me\/articles\/dhh-webpush-throwdown\/","pubDate":"Sat, 30 Dec 2023 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/dhh-webpush-throwdown\/","description":"<p>DHH is adding WebPush to Rails 8. I think you should do it today instead\u2026<\/p>\n<p>Introducing <a href=\"https:\/\/github.com\/jbennett\/noticed-web_push\">noticed-web_push<\/a><\/p>\n<div class=\"\">\n    <iframe\n        class=\"w-full aspect-video\"\n        src=\"https:\/\/www.youtube.com\/embed\/-9KWx7Pj5AM\"\n        title=\"YouTube video player\"\n        frameborder=\"0\"\n        allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen><\/iframe>\n<\/div>\n\n<p>(Yes, I know Action Notifier will likely have one or two things mine doesn&rsquo;t, but that&rsquo;s not the point \ud83d\ude1c)<\/p>"},{"title":"Turbo 8 (Beta) Upgrade Guide","link":"https:\/\/jbennett.me\/articles\/turbo-8-beta-upgrade-guide\/","pubDate":"Fri, 22 Dec 2023 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/turbo-8-beta-upgrade-guide\/","description":"<div class=\"bg-tangerine-50 px-4 py-2 first:prose-p:mt-0 first:prose-p:mb-0\">\n  <a href=\"https:\/\/jbennett.me\/articles\/turbo-8-upgrade-guide\/\">Mar 18, 2024<\/a>: Check out the updated article for the release version\n<\/div>\n<p>If you are already using Turbo 7, switching to Turbo 8, in all it&rsquo;s morphing glory, is painless.<\/p>\n<ol>\n<li>Update the <code>turbo-rails<\/code> gem to the latest beta, <code>2.0.0.pre.beta.1<\/code> as of Dec 22, 2023<\/li>\n<li>Update <code>@hotwired\/turbo-rails<\/code> in package.json to the latest beta, <code>8.0.0-beta.1<\/code> as of Dec 22, 2023<\/li>\n<li>Switch to the new morphing strategy in your layout:<\/li>\n<\/ol>\n<pre tabindex=\"0\"><code class=\"language-erb\" data-lang=\"erb\">&lt;head&gt;\n\t&lt;!-- other head content --&gt;\n\t&lt;%= turbo_refreshes_with method: :morph, scroll: :preserve %&gt;\n\t&lt;%= content_for :head %&gt;\n&lt;\/head&gt;\n<\/code><\/pre><p>This will globally enable the morphing strategy for your application. With this running (and tested), what&rsquo;s your next step? Conduct an audit of all your Turbo Streams templates because you may find that most of them can be safely removed!<\/p>"},{"title":"Understanding the Why Behind Your Coding Tasks","link":"https:\/\/jbennett.me\/articles\/understanding-the-why-behind-your-coding-tasks\/","pubDate":"Fri, 15 Dec 2023 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/understanding-the-why-behind-your-coding-tasks\/","description":"<p>If you just got assigned a ticket and you don&rsquo;t know the following, find out before you start coding:<\/p>\n<ul>\n<li>Who does it help?<\/li>\n<li>How big of a help is it?<\/li>\n<li>How does it help the business?<\/li>\n<li>Why is not doing it, not an option?<\/li>\n<\/ul>\n<p>Any schmuck can type some code and close a ticket. <em>Crafting<\/em> software requires more than that.<\/p>"},{"title":"Types of Dev Teams","link":"https:\/\/jbennett.me\/articles\/types-of-dev-teams\/","pubDate":"Sun, 10 Dec 2023 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/types-of-dev-teams\/","description":"<p>There are three types of development teams:<\/p>\n<ol>\n<li><strong>Mercenaries:<\/strong> Mercenaries come in and get things done. Is it going to be perfect? No. But it will get a foundation test and in place fast!<\/li>\n<li><strong>Agencies:<\/strong> An agency brings experience and a team to bear on your problem. A larger array of skills typically creates a better solution at the cost of time and expense.<\/li>\n<li><strong>In House:<\/strong> If technology is fundamental to your business an in house team is often the best long term solution. Make sure you have a mix of skills present and the ability to manage them.<\/li>\n<\/ol>\n<p>All these team structures are the right choice for some business. Which one is right for yours?<\/p>"},{"title":"Is Custom Software Right for You? 4 Compelling Reasons to Think Twice","link":"https:\/\/jbennett.me\/articles\/is-custom-software-right-for-you-4-compelling-reasons-to-think-twice\/","pubDate":"Wed, 06 Dec 2023 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/is-custom-software-right-for-you-4-compelling-reasons-to-think-twice\/","description":"<p>Custom software development can offer tailored solutions, but it&rsquo;s not always the best choice for every situation. Here are the top four reasons to think twice, with a bonus reason to cap it off:<\/p>\n<h2 id=\"1-other-software-is-good-enough\">1. Other Software is Good Enough<\/h2>\n<p>While the allure of custom software can be strong, it&rsquo;s essential to carefully consider whether existing software can meet your needs. Often, good-enough solutions like Excel or specialized software providers like <a href=\"https:\/\/airtable.com\/\">AirTable<\/a> can save time, money, and potential development pitfalls. Remember, perfection is elusive, but good enough is, well good enough<\/p>"},{"title":"Toronto Ruby Meetup","link":"https:\/\/jbennett.me\/articles\/toronto-ruby-meetup\/","pubDate":"Tue, 28 Nov 2023 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/toronto-ruby-meetup\/","description":"<p>Last Thursday, November 23 was the inaugural <a href=\"https:\/\/toronto-ruby.com\/\">Toronto Ruby meetup<\/a>. Simmon Li invited me up and it was a great time.<\/p>\n<p>Thanks <a href=\"https:\/\/github.com\/meagar\">Matt<\/a> for taking us down all the bunny trails around <code>Integer#even?<\/code> and your obsessive need to correct the internet. I don&rsquo;t appreciate being left hanging on the performance implications though!<\/p>\n<p>I hope to make it up again in January.<\/p>"},{"title":"The Hidden Costs of Custom Software Projects","link":"https:\/\/jbennett.me\/articles\/the-hidden-costs-of-custom-software-projects\/","pubDate":"Thu, 23 Nov 2023 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/the-hidden-costs-of-custom-software-projects\/","description":"<p>Everyone thinks about the cost of the initial build of custom software, but it can extend far beyond that. When accounting for the total cost of custom software you need to consider:<\/p>\n<ol>\n<li>The initial build<\/li>\n<li>Maintenance and security updates<\/li>\n<li>Support<\/li>\n<li>New features and additions<\/li>\n<li>Infrastructure and hosting<\/li>\n<li>Audits in regulated industries<\/li>\n<\/ol>\n<p>Go into your projects with your eyes open so you can make good decisions.<\/p>"},{"title":"Safeguarding Your Custom Software Investment","link":"https:\/\/jbennett.me\/articles\/safeguarding-your-custom-software-investment\/","pubDate":"Tue, 21 Nov 2023 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/safeguarding-your-custom-software-investment\/","description":"<p>Custom software is a significant asset for any business, offering tailored solutions and competitive advantages. However, like all software, it demands continuous maintenance and support to ensure it remains effective and secure. This might seem counterintuitive \u2014 after all, software that remains unchanged doesn&rsquo;t change, right? The unique challenge with software is its dynamic environment; without regular upkeep, software can become obsolete or dysfunctional.<\/p>\n<p>Understanding the different types of maintenance your custom software requires is key to maximizing your current investment. Let&rsquo;s explore the four major categories:<\/p>"},{"title":"When to Make Custom Software","link":"https:\/\/jbennett.me\/articles\/when-to-make-custom-software\/","pubDate":"Tue, 07 Nov 2023 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/when-to-make-custom-software\/","description":"<p>Business leaders ask me when they should commission custom software. That question is easy to answer. These two things must be true:<\/p>\n<ol>\n<li>Nothing else out there does what you need well.<\/li>\n<li>The value the custom software provides exceeds it&rsquo;s lifetime cost.<\/li>\n<\/ol>\n<p>Unfortunately, this leads to additional questions that are harder to answer\u2026<\/p>"},{"title":"Start with a Spreadsheet","link":"https:\/\/jbennett.me\/articles\/start-with-a-spreadsheet\/","pubDate":"Wed, 01 Nov 2023 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/start-with-a-spreadsheet\/","description":"<p>Let&rsquo;s get something straight: I&rsquo;m a big fan of spreadsheets. There&rsquo;s something satisfying about taking an idea, breaking it down, and seeing it laid out in neat rows and columns. You can quickly spot complexities, see where the real value is, and even identify areas that might need a little more attention.<\/p>\n<p>But here&rsquo;s a heads-up for all my fellow business owners: while starting with a spreadsheet is great, it&rsquo;s essential to know when to level up. What begins as a handy tool can, over time, become a crucial part of your operations. And when your business starts depending heavily on a single spreadsheet, things can get a tad dicey.<\/p>"},{"title":"Conditional Formatting w\/ RubyXL","link":"https:\/\/jbennett.me\/articles\/conditional-formatting-w-rubyxl\/","pubDate":"Fri, 20 Oct 2023 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/conditional-formatting-w-rubyxl\/","description":"<p>I was looking to add conditional formatting to an Excel report that I was generating with <a href=\"https:\/\/github.com\/weshatheleopard\/rubyXL\">RubyXL<\/a> and found no documentation on how to do it. Looking at the source code wan&rsquo;t much use as it looked like it was a direct translation from an XML reference.<\/p>\n<p>With that dead end sufficiently explored I turned to the obvious next solution in 2023, <del>Stackoverflow<\/del> ChatGPT. It&rsquo;s first attempt looked pretty good, except for the part where it use classes and methods that don&rsquo;t exist. It&rsquo;s second attempt was no better. It&rsquo;s third try was actually correct, just not very helpful. It recommended I add documentation and submit a pull request. Thanks ChatGPT\u2026<\/p>"},{"title":"Code w\/ Jason Meetup - CRMmy Review","link":"https:\/\/jbennett.me\/articles\/code-with-jason\/","pubDate":"Sun, 17 Sep 2023 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/code-with-jason\/","description":"<p>At last Thursday&rsquo;s <a href=\"#\">Code With Jason Meetup<\/a> I was in the hot seat with <a href=\"#\">CRMmy<\/a>, my CRM side project. We spent most of our time talking through some feature tests, taking them from a single initial stream of consciousness test to a higher quality set of tests.<\/p>\n<div class=\"\">\n    <iframe\n        class=\"w-full aspect-video\"\n        src=\"https:\/\/www.youtube.com\/embed\/ps_1AJlBqOY\"\n        title=\"YouTube video player\"\n        frameborder=\"0\"\n        allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen><\/iframe>\n<\/div>\n\n<p>When I start building out a new feature, I typically start with walking through the navigation and screen I wish were there using feature tests. This ends up being very low level which is hard to work with long term, but it means the requirements are explicit. The feature I was looking at was left at this stage and look like this initially:<\/p>"},{"title":"Fun Times with Caching","link":"https:\/\/jbennett.me\/articles\/fun-times-with-caching\/","pubDate":"Tue, 02 May 2023 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/fun-times-with-caching\/","description":"<p>So I recently was working on a client project and ran into a problem with an admin report page being super slow only on production, oven though it had russian doll caching setup on it.<\/p>\n<p>I checked that caching was enabled on production (<code>Rails.application.config.action_controller.perform_caching == true<\/code>), which it was.<\/p>\n<p>Turns out, when the redis server had been provisioned on Heroku it had the max memory policy of ==noeviction==. This means once the server was full, it wouldn&rsquo;t add anything new to the cache, and Rails would swallow any errors and just do the slow normal render.<\/p>"},{"title":"From Terminals to Web-Based SaaS","link":"https:\/\/jbennett.me\/articles\/from-terminals-to-web-based-saas\/","pubDate":"Fri, 21 Apr 2023 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/from-terminals-to-web-based-saas\/","description":"<p>Software development has come a long way since the days of punch cards and mainframes. Today, small and medium-sized businesses (SMBs) have more options than ever before when it comes to creating custom software solutions for their needs. In this blog post, we&rsquo;ll explore the evolution of software development, from terminals to clients, and how web-based SaaS has brought us full circle back to terminals.<\/p>\n<h2 id=\"the-early-days-terminals-and-mainframes\">The Early Days: Terminals and Mainframes<\/h2>\n<p>In the early days of computing, the primary way to interact with a computer was through a terminal. These were physical devices that looked like typewriters, with a keyboard for input and a screen for output. The terminal would connect to a mainframe computer, which would do all the heavy lifting of processing and storage.<\/p>"},{"title":"How to Beat Your SaaS Competitors: Solving Specific Problems Better","link":"https:\/\/jbennett.me\/articles\/how-to-beat-your-saas-competitors\/","pubDate":"Mon, 17 Apr 2023 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/how-to-beat-your-saas-competitors\/","description":"<p>In the world of SaaS, competition is fierce. Startups and small businesses must constantly strive to stay ahead of the curve and outperform their competitors. But how much better does a SaaS product need to be in order to be competitive?<\/p>\n<p>The answer lies in understanding that different problems can be solved to differing degrees and styles of better. Some solutions are 10x better than the competition, while others may be only 0.1x better, but are different in a way that is significant. For example, a SaaS product may be faster or more intuitive than its competitors. The competition might also be over-serving, providing too many features that customers do not actually need or use.<\/p>"},{"title":"Using Dynamic Partials While Rendering","link":"https:\/\/jbennett.me\/articles\/using-dynamic-partials-while-rendering\/","pubDate":"Fri, 07 Apr 2023 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/using-dynamic-partials-while-rendering\/","description":"<p>One of my favourite &ldquo;magical&rdquo; characteristics of Ruby on Rails is its naming conventions. We don&rsquo;t need to argue about what the name of the database table that will hold information about a widget should be called, it&rsquo;s widgets. Other frameworks, especially really old ones, didn&rsquo;t do that.<\/p>\n<p>Now, Rails is also great at letting you override and tweaks those options, but really, you almost never need to.<\/p>\n<p>This same <a href=\"https:\/\/rubyonrails.org\/doctrine#convention-over-configuration\">convention over configuration<\/a> mindset continues throughout the framework, even to rendering stuff, which is what we are going to be looking at today.<\/p>"},{"title":"The Mistake I Made When Trying to Name a Non-Existent Thing","link":"https:\/\/jbennett.me\/articles\/mistakes-while-naming\/","pubDate":"Fri, 31 Mar 2023 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/mistakes-while-naming\/","description":"<p>Developers must possess the crucial skill of naming things. Oftentimes, I found myself stuck in a problem until I came across the right name which directly led to a solution. Usually, the root cause of the issue is treating two things as one. Once these things are distinguished, each can serve their respective functions, and the solution will follow.<\/p>\n<p>While naming things is valuable, not naming a non-existent thing can be even more valuable.<\/p>"},{"title":"Why Choose Custom Over Low Code","link":"https:\/\/jbennett.me\/articles\/why-choose-custom-over-low-code\/","pubDate":"Mon, 27 Mar 2023 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/why-choose-custom-over-low-code\/","description":"<p>Custom software can be a game-changer for businesses, providing a tailored solution to their specific needs and challenges. In contrast, no-code solutions are often more limited in their capabilities and may not be able to fully meet the unique requirements of a business. In this blog post, we will explore the benefits of custom software and why it can be better than no-code solutions for businesses.<\/p>\n<p>One of the biggest advantages of custom software is its flexibility and scalability. Because it is built specifically for a business, custom software can be designed to adapt and grow as the business does. This means that as the business changes and evolves, the software can be updated and modified to continue meeting its needs. In contrast, no-code solutions are often more rigid and may not be able to keep up with the changing needs of a business.<\/p>"},{"title":"So You've Got Custom Software: A Beginner's Guide to Maintenance","link":"https:\/\/jbennett.me\/articles\/a-beginners-guide-to-software-maintenance\/","pubDate":"Fri, 17 Mar 2023 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/a-beginners-guide-to-software-maintenance\/","description":"<p>As a software developer who specializes in custom business solutions, I understand the importance of not only developing software that meets business needs but also maintaining it over time. In this article, I will discuss why software maintenance is crucial, how much it costs, and how long you need to maintain custom software. I will also explore who should maintain the software, what maintenance entails, and the consequences of neglecting it. Additionally, I will touch on the impact of software maintenance on custom business solutions and how the agile methodology supports it.<\/p>"},{"title":"I Feel the Need\u2026The Need for Productivity","link":"https:\/\/jbennett.me\/articles\/i-feel-the-need-for-productivity\/","pubDate":"Mon, 06 Mar 2023 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/i-feel-the-need-for-productivity\/","description":"<p>Startups can be split into 3 technical categories: doesn\u2019t matter, supper critical, and it depends. Yes, you can build your ebook company on a no-code Wordpress plugin. For your blockchain powered AI startup, you already know what you are using. That final category, that one I find super interesting.<\/p>\n<p>If you have the option of being flexible in the technical choices you make, I think it is hard to go wrong with prioritizing developer productivity.<\/p>"},{"title":"Ruby on Rails Should be the Default","link":"https:\/\/jbennett.me\/articles\/ruby-on-rails-by-default\/","pubDate":"Mon, 06 Mar 2023 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/ruby-on-rails-by-default\/","description":"<p>If you are thinking about making a startup, start with Ruby on Rails. It should be the default. If you disagree with me, you probably know enough that <strong>you should ignore my advice<\/strong>. If you don\u2019t, then use RoR and do the right thing.<\/p>\n<p>Here\u2019s why\u2026<\/p>\n<p>Your start up needs to <strong>move fast<\/strong>, make <strong>changes quickly<\/strong>, and <strong>figure out the problem<\/strong> you are trying to crack. You do not need the latest and greatest framework to do that. In fact, you will likely hinder your progress with the latest and greatest. The newest techs are shiny but unproven!<\/p>"},{"title":"What matters For Your MVP Tech Stack","link":"https:\/\/jbennett.me\/articles\/what-matters-for-your-mvp-tech-stack\/","pubDate":"Sun, 27 Nov 2022 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/what-matters-for-your-mvp-tech-stack\/","description":"<p>I was reading an article by Taj Pelc called <a href=\"https:\/\/www.pelc.cc\/blog\/llne00md73lobf9cf84bneg8571jra\">Are you able to move fast?<\/a> where he rightly recommended constraining your tech stack to help give yourself appropriate constraints and I 100% agree with him, about 95% of the way. \ud83e\udd28<\/p>\n<h2 id=\"um-what\">Um, what?<\/h2>\n<p>I think that these constraints are critical to to a successful MVP and even an early stage business. If you are one of the few companies that exceed this initial starting point, great! Now we can talk about microservices and other technical solutions to the scale you need, but we are in a more informed position and we can look at adding that complexity where you need it.<\/p>"},{"title":"An Oauth Tale","link":"https:\/\/jbennett.me\/articles\/an-oauth-tale\/","pubDate":"Sat, 29 Oct 2022 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/an-oauth-tale\/","description":"<p>So today I get to tell you a story. The instigator in this story is an <strong>evil wizard named Joe<\/strong>. His magical power is the dreaded curse called the \u201cnerd snipe\u201d. He hit the hero of the story, Jonathan, hard with his nerd snipe.<\/p>\n<p>He proposed a \u201csimple twitter tool\u201d that would let him easily post queue\u2019d tweets. There would be a few UX affordances to make this work well, but shouldn\u2019t be anything especially problematic.<\/p>"},{"title":"Twitter v2 API on Rails","link":"https:\/\/jbennett.me\/articles\/twitter-v2-api-on-rails\/","pubDate":"Sat, 29 Oct 2022 00:00:00 +0000","guid":"https:\/\/jbennett.me\/articles\/twitter-v2-api-on-rails\/","description":"<p>I recently setup a new Twitter app to test out some ideas and ran into so fun roadblocks setting up OAuth. Here are a few notes so that you can hopefully come out on the other side with a little extra hair.<\/p>\n<ol>\n<li>Make sure you&rsquo;re not using a developer environment app if you are looking to use OAuth<\/li>\n<li>Make sure you are not using localhost for your callback url. 127.0.0.1 does work.<\/li>\n<li>Make sure you pick &ldquo;Native App&rdquo; for the type of app. The web app option doesn&rsquo;t let you use refresh tokens<\/li>\n<\/ol>\n<p>If you are using Ruby on Rails the omniauth and omniauth-twitter2 gems work great for getting the initial access token.<\/p>"},{"title":"Branch","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p>A branch is a separate line of development within your source code repository. This allows multiple developers work within a single <a href=\"#repository\">repository<\/a>, without ruining the work of other developers.<\/p>"},{"title":"Cache","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p>Not to be confused with cash, cache is a copy of something, usually for the purpose of improving performance. For example, instead of adding up the cost of each line item on an invoice every time we look up the total, we do it once, and then save a copy of that result in a cache.<\/p>\n<p>This can be a huge source of performance improvement, but also leads to bugs if you are not careful. For example, if you forget to clear the total when adding an additional line item, you will not get the outcome you expect!<\/p>"},{"title":"CI\/CD","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p>Continuous Integration (CI) is a practice of constantly merging all the code from every developer into a single main version.<\/p>"},{"title":"contend.faith","link":"https:\/\/jbennett.me\/contend.faith\/","pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":"https:\/\/jbennett.me\/contend.faith\/","description":"<div class=\"min-h-screen bg-[#374554]\">\n  <a href=\"https:\/\/app.contend.faith\" class=\"block max-w-4xl mx-auto pt-16 lg:pt-32 text-white text-center font-semibold font-serif\">\n    <img src=\"https:\/\/jbennett.me\/images\/content.faith-logo-vertical-white.png\" alt=\"contend.faith logo\" class=\"block mb-12\" \/>\n<pre><code>&lt;div class=&quot;text-6xl&quot;&gt;&lt;span class=&quot;sr-only&quot;&gt;contend.faith &lt;\/span&gt;is live!&lt;\/div&gt;\n&lt;div&gt;Sign up at &lt;span class=&quot;underline&quot;&gt;https:\/\/app.contend.faith&lt;\/span&gt;.&lt;\/div&gt;\n<\/code><\/pre>\n  <\/a>\n<\/div>"},{"title":"contend.faith","link":"https:\/\/jbennett.me\/content.faith\/","pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":"https:\/\/jbennett.me\/content.faith\/","description":"<div class=\"min-h-screen bg-[#374554]\">\n  <a href=\"https:\/\/app.contend.faith\" class=\"block max-w-4xl mx-auto pt-16 lg:pt-32 text-white text-center font-semibold font-serif\">\n    <img src=\"https:\/\/jbennett.me\/images\/content.faith-logo-vertical-white.png\" alt=\"contend.faith logo\" class=\"block mb-12\" \/>\n<pre><code>&lt;div class=&quot;text-6xl&quot;&gt;&lt;span class=&quot;sr-only&quot;&gt;contend.faith &lt;\/span&gt;is live!&lt;\/div&gt;\n&lt;div&gt;Sign up at &lt;span class=&quot;underline&quot;&gt;https:\/\/app.contend.faith&lt;\/span&gt;.&lt;\/div&gt;\n<\/code><\/pre>\n  <\/a>\n<\/div>\n<!--\n<script>\nwindow.location = \"https:\/\/app.contend.faith\";\n<\/script>\n-->"},{"title":"CRUD","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p><strong>C<\/strong>reate, <strong>R<\/strong>ead, <strong>U<\/strong>pdate, <strong>D<\/strong>elete is a common term to describe the basic operations of most web applications.<\/p>"},{"title":"Cyclomatic Complexity","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p>Cyclomatic complexity is the count of the number of paths in code. For example, code that displays a result in kilometres or miles could have a cyclomatic complexity of 2. Lower cyclomatic complexity indicates more understandable code.<\/p>"},{"title":"Environments","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p>Environments let you run the application with different configurations. This is most obvious useful with development and production environments. In the development environment you might run with lower security settings to improve performance and have access to a test finances account. In the production environment the system would run with full security settings and have access to the real financial accounts.<\/p>"},{"title":"Feature Flag","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":{}},{"title":"Flaky Test","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p>A flaky test is a test that sometimes fails without any changes in the code or configuration of the project. A silly example of this would be code that generates heads and tails, but only passes if it&rsquo;s heads.<\/p>"},{"title":"ICP","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p>An <strong>I<\/strong>deal <strong>C<\/strong>ustomer <strong>P<\/strong>rofile (ICP) gives a complete picture of the perfect customer for your SaaS. This typically includes demographics like gender, age, and income level, but also should include details about the pains this person is experiencing that you might be able to help with.<\/p>"},{"title":"Idea Planning Workbook","link":"https:\/\/jbennett.me\/idea-planning-workbook\/","pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":"https:\/\/jbennett.me\/idea-planning-workbook\/","description":"<h2 id=\"are-you-sick-of-spinning-your-wheels-on-promising-ideas-that-go-nowhere\">Are you sick of spinning your wheels on promising ideas that go nowhere?<\/h2>\n<ul>\n<li>Do your friends all say they love it, but no one buys it?<\/li>\n<li>Are you worried about the competition doing the same thing, only better?<\/li>\n<li>Do you fear wasting your time on the wrong opportunities?<\/li>\n<li>Are you stuck not knowing the next steps you should take?<\/li>\n<\/ul>\n<h2 id=\"imagine-having-the-roadmap-to-make-your-idea-real\">Imagine having the roadmap to make your idea real<\/h2>\n<ul>\n<li>Imagine knowing what your customers will actually buy<\/li>\n<li>Imagine knowing what will set you up to beat the competition<\/li>\n<li>Imagine knowing which of your ideas you should be investing in<\/li>\n<li>Imagine knowing exactly what to do next<\/li>\n<\/ul>\n<h2 id=\"the-idea-honing-workbook-is-the-answer\">The Idea Honing Workbook is the answer<\/h2>\n<p>My free Idea Honing Workbook will show you how to assess and polish your big idea. You\u2019ll get a blueprint for evaluating your ideas and ensuring you are addressing the primary areas that will make your idea successful.<\/p>"},{"title":"installprompt","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p>installprompt is an experimental Chrome event that makes web app installation simple and easy for end users. When Chrome detects the site could be installed, an event is sent which can be used to trigger the native install prompt. This actually lets web apps be installed easier than a native app!<\/p>"},{"title":"It Depends","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p>The standard answer for any and all questions. Further clarification available upon request.<\/p>"},{"title":"MVP","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p>A <strong>M<\/strong>inimum <strong>V<\/strong>iable <strong>P<\/strong>roduct is the version of a product without any bells and whistles. This is the first version where a user could start getting value from using it.<\/p>"},{"title":"Page is Pending","link":"https:\/\/jbennett.me\/pending\/","pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":"https:\/\/jbennett.me\/pending\/","description":"<p>Sorry, this page is currently a draft. Not only am I working on it, but I&rsquo;ve also made note of your interest.<\/p>\n<script>\n    window.addEventListener(\"DOMContentLoaded\", () => {\n        const urlParams = new URLSearchParams(window.location.search);\n        const term = urlParams.get('term');\n        window.fathom?.trackEvent(`pending term: ${term}`)\n    })\n<\/script>"},{"title":"Podcast One Pager","link":"https:\/\/jbennett.me\/podcast-one-pager\/","pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":"https:\/\/jbennett.me\/podcast-one-pager\/","description":"<p>The following information is provided as a cut-and-paste resource for conference organizers, media professionals, podcast hosts, and other interested parties.<\/p>\n<p>Please feel free to use anything here as-is without checking with me first. If you have additional questions, you can email me directly.<\/p>\n<h2 id=\"short-bio\">Short Bio<\/h2>\n<p>Jonathan Bennett is software consultant who is on a mission to eliminate fragile, mission critical spreadsheets by building robust forward looking solutions. He publishes weekly with insights and updates on industry trends.<\/p>"},{"title":"Polymorphic assocation","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p>A polymorphic relationship in Rails lets a model belong to more than one other model using a single association.<\/p>\n<p>For example, a <code>Note<\/code> can belong to either a <code>Post<\/code>, an <code>Account<\/code>, or any other model, all through the same <code>noteable<\/code> interface.<\/p>"},{"title":"Privacy Policy","link":"https:\/\/jbennett.me\/privacy\/","pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":"https:\/\/jbennett.me\/privacy\/","description":"<p>Thank you for visiting Jonathan Bennett\u2019s website.<\/p>\n<p>Your privacy is important to me. Jonathan Bennett does not disclose any nonpublic personal information about its clients to nonaffiliated third parties except to service or manage the client\u2019s account or as permitted by law and restricts access to client personal information to those employees requiring that information to provide products or services to clients.<\/p>\n<p>Jonathan Bennett collects nonpublic personal information about its clients to serve their investment needs, provide customer service, offer new products and services and comply with legal and regulatory requirements.<\/p>"},{"title":"PWA","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p>Progressive Web Apps are website that are designed to cater to the capabilities of the device they are running on. This means it will work on a lower powered phone, and a high end desktop computer. The features used will vary, but the website will still work.<\/p>"},{"title":"PWAs","link":"https:\/\/jbennett.me\/pwa\/","pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":"https:\/\/jbennett.me\/pwa\/","description":"<div class=\"bg-tangerine-50 px-4 py-2 first:prose-p:mt-0 first:prose-p:mb-0\">\n  Last Updated Feb 8, 2024: Added <a href=\"#url-handlers\">URL Handlers<\/a>\n<\/div>\n<p>The iPhone introduced the world to the mobile web. It was the original &ldquo;sweet solution&rdquo;, but had many downsides compared to native apps.<\/p>\n<p>Limited performance, network access, and software features restricted what was even possible on the web.<\/p>\n<p>But <a href=\"#the-wish-list\">most<\/a> of that has changed.<\/p>\n<p>PWAs and service workers on modern mobile browsers have closed the gap between what&rsquo;s possible on mobile web and mobile native.<\/p>"},{"title":"Repository","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p>A repository is a place to store multiple versions of code, along with comments and other organizational information. GitHub and Gitlab are two common services that provide repository services.<\/p>"},{"title":"SaaS","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p><strong>S<\/strong>oftware <strong>a<\/strong>s <strong>a<\/strong> <strong>S<\/strong>ervice is a software model where you charge a subscription fee to access the software as opposed to a one time feed with upgrades as was the typical case with packaged software. This has many benefits for the customer since they do not need to worry about setup and maintenance, and benefits the business since they will have a stable revenue model.<\/p>"},{"title":"Snowflake Server","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p>Snowflake servers are typically customized servers which have not been properly documented. As such no one really knows what exactly it does, and it is risky to touch or change it.<\/p>"},{"title":"SOP","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p>A <strong>S<\/strong>tandard <strong>O<\/strong>perating <strong>P<\/strong>rocedure (SOP) is a standardized list of steps needed to complete a task. This should be sufficiently thorough such that an individual generally skilled in the relevant areas would be able to complete the task without outside assistance.<\/p>"},{"title":"SPA","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p>Single Page Applications (SPA) are websites that use JavaScript to provide a more interactive user experience. This is typically done by avoiding page navigation and using data loaded directly from the server on the users machine.<\/p>"},{"title":"Spooky Action","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p>Spooky Action <em>(at a distance)<\/em> refers to the common issue where changing code in one module, significantly and unintentionally alters the functionally in some distance, seemingly unrelated code.<\/p>"},{"title":"Strategy Retainer","link":"https:\/\/jbennett.me\/services\/strategy-retainer\/","pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":"https:\/\/jbennett.me\/services\/strategy-retainer\/","description":"<script async data-uid=\"4670b52ce0\" src=\"https:\/\/jonathan-bennett.ck.page\/4670b52ce0\/index.js\"><\/script>\n<p class=\"text-3xl\">You&rsquo;re <strong>behind schedule<\/strong>, <strong>over budget<\/strong>, and every time your team fixes a <strong>bug<\/strong>, two new ones crawl out.<\/p>\n<p class=\"text-3xl\">Imagine a world where you are <strong>on schedule<\/strong>. You <strong>hit your budget<\/strong>. Bugs are a <strong>rare<\/strong> occurrence and they <strong>dealt with<\/strong> swiftly.<\/p>\n<p>The pains you are running into are due to not having the right technical expertise within your c-suite.<\/p>\n<p>I&rsquo;ve spent years working with founders on their technical needs, helping them assess and build their technical teams, all while ensuring their technology aligns with their business goals. Here are the steps to make this happen:<\/p>"},{"title":"TDD","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p><strong>T<\/strong>est <strong>D<\/strong>riven <strong>D<\/strong>evelopment is a technique where you write tests for code that doesn&rsquo;t exist. As you step through each error message, you are lead to solve the next single issue need. Once all the test passes, you have a functional system. These tests can also be used to increase confidence in the system and even perform continuous integration and deployment of your system.<\/p>"},{"title":"Tech Insights Daily","link":"https:\/\/jbennett.me\/tech-insights-daily\/","pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":"https:\/\/jbennett.me\/tech-insights-daily\/","description":"<div class=\"mx-auto max-w-2xl lg:max-w-5xl\">\n  <div class=\"mt-4 mx-auto px-4 max-w-xl space-y-4\">\n    <div class=\"border border-gray-200 px-4 pt-4\"><h2 class=\"font-semibold text-2xl text-center\">Tech Insights Daily<\/h2>\n<h3 class=\"font-semibold text-lg text-center mb-4\">All the tech insights, none of the jargon<\/h3>\n\n<p class=\"mb-4\">In just two minutes a day, you can get actionable insights to understand the tech of your business and be able to move with confidence.<\/p>\n<p class=\"mb-4\">Join me for direct tips and tricks, best practices, and valuable insights into what your tech team needs.<\/p>\n\n<script async data-uid=\"857ab2e339\" src=\"https:\/\/jonathan-bennett.ck.page\/857ab2e339\/index.js\"><\/script>\n\n<p class=\"italic text-sm text-gray-700 text-center mb-4\">\n  Don\u2019t worry\u2014I hate spam, too.<br>\n  I\u2019ll NEVER share your email address with anyone!\n<\/p>"},{"title":"Technical Cofounder Call","link":"https:\/\/jbennett.me\/technical-cofounder-call\/","pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":"https:\/\/jbennett.me\/technical-cofounder-call\/","description":"<div class=\"space-y-4\">\n    <div class=\"border-l-4 border-gray-200 pl-8 text-lg prose\">\n      Talking with Jonathan helped me clarify the real value of my product idea, tripling it\u2019s worth just by discussing the details and helping me keep all of them in mind at once.\n    <\/div>\n  \n    <div class=\"flex gap-x-4 items-center\">\n      \n        <div class=\"w-20 h-20 shrink-0 rounded-full overflow-hidden not-prose\">\n          <img\n            src=\"https:\/\/jbennett.me\/images\/joe-ciskey.jpg\"\n            class=\"w-full block object-cover\"\n            alt=\"Joe Ciskey\" \/>\n        <\/div>\n      \n  \n      <div class=\"first:ml-9\">\n        Joe Ciskey<br>\n        <a href=\"https:\/\/inceptivecss.com\" target=\"_blank\" rel=\"noopener\" class=\"text-fiord-500 hover:text-tangerine-500\">\n          Inceptive Custom Software Solutions\n        <\/a>\n      <\/div>\n    <\/div>\n  <\/div>\n\n\n\n<h2 id=\"not-sure-what-to-do-next-with-your-startup\">Not Sure What to Do Next With Your Startup?<\/h2>\n<p>Your startup is struggling. You need to get answers to technical questions but you aren\u2019t ready to lose the equity it would take to get a technical founder. Even if you were, how to you find and evaluate them?<\/p>"},{"title":"Technical Strategy Retainer","link":"https:\/\/jbennett.me\/technical-strategy-retainer\/","pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":"https:\/\/jbennett.me\/technical-strategy-retainer\/","description":"<p class=\"text-center\"><a href=\"https:\/\/tidycal.com\/jbennett\/book-time-with-jonathan\" class=\"bg-slate-800 !text-white no-underline text-xl px-4 py-2 rounded-full inline-block\">Contact Me Today<\/a><\/p>\n<p>Missing strong technical direction can cost your startup vast sums of capital and time. In this competitive environment the most important factor is how quickly can you learn and make the right moves.<\/p>\n<p>Let\u2019s be clear, you will take wrong steps. That cannot be entirely avoided. What must be avoided is wasting time anticipating the wrong step, and not moving forward with the correct step afterwards. With a strong business oriented technical direction, you can be confident that you are actively moving forward with your startup.<\/p>"},{"title":"Terms & Conditions","link":"https:\/\/jbennett.me\/terms-and-conditions\/","pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":"https:\/\/jbennett.me\/terms-and-conditions\/","description":"<p><em>Last Updated June 21, 2023<\/em><\/p>\n<p>I\u2019m not big on formality, but sometimes it\u2019s best to have a few simple things written down so that we\u2019re all on the same page.<\/p>\n<p>By joining the <strong>Fractional-CTO<\/strong> or <strong>Development Subscription<\/strong> program, you agree to the policies and terms contained in this document.<\/p>\n<h2 id=\"strategic-consulting-audits--roadmapping\">Strategic Consulting, Audits &amp; Roadmapping<\/h2>\n<p>You get unlimited asynchronous access to me via our agreed upon communication channels. My responses may include written documentation, recorded videos or screencasts, code examples, and more.<\/p>"},{"title":"Test Suite","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p>A test suite is an automated tool the helps ensure your application runs as specified. This is a huge benefit as the computer is able to automatically run hundreds or thousands of test in minutes as opposed to just a handful that a human could manually complete.<\/p>\n<p>Please note, this tests for the specification, not necessarily correctness. A poorly written test suite that declares <code>2+2=5<\/code> will pass or fail based on the specification, not based on the rules of math. This is why a good test suite is critical for the confidence of your system.<\/p>"},{"title":"Thanks for Requesting the Idea Planning Workbook","link":"https:\/\/jbennett.me\/idea-planning-workbook-thanks\/","pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":"https:\/\/jbennett.me\/idea-planning-workbook-thanks\/","description":"<p>Congrats on taking the first step in honing your big idea. You can make your own private <a href=\"https:\/\/docs.google.com\/document\/d\/1MPR2ch3q3MaVysnC7mshcF7yqp85bOxFep3KfPlk20M\/edit\">copy of the workbook<\/a>. Just in case you get stuck, you can also see what a <a href=\"https:\/\/docs.google.com\/document\/d\/1Eheo0kydtzplv6I75PPtaFkvv8Dm0np_g2nKAaPK-YM\/edit\">completed workbook<\/a> looks like, assuming your big idea is super premium dog food\u2026<\/p>\n<p>I\u2019m also going to follow up with a few emails over the next week to give you some extra direction. Most people get stuck at some point in this process, which is perfect! We want to get stuck on these problems today, so that we think about them, and clear them out for tomorrow.<\/p>"},{"title":"Webhooks","link":{},"pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":{},"description":"<p>Webhooks allow your server to respond to events that occur outside your system, for example being notified when a Stripe subscription is successfully charged.<\/p>"},{"title":"Who's Tyler?","link":"https:\/\/jbennett.me\/whos-tyler\/","pubDate":"Mon, 01 Jan 0001 00:00:00 +0000","guid":"https:\/\/jbennett.me\/whos-tyler\/","description":"<p>One of my clients has HubSpot for CRM-y stuff and Notion for documents and process. I have my own Notion as well and use OmniFocus for my business todos. My personal reminders are in Reminders.app.<\/p>\n<p>Needless to say, my personal and business world is scattered. It should come as no surprise that when a saw my todo to &ldquo;Follow up with Tyler&rdquo;, my response was &ldquo;Who&rsquo;s Tyler?&rdquo;<\/p>\n<p>The todo was in HubSpot, created on January 4th, the same day &ldquo;Tyler&rdquo; was created \u2014 no other details. \ud83e\udd26\ud83c\udffb\u200d\u2642\ufe0f<\/p>"}]}}