<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Richard Lemon</title>
    <description>The latest articles on DEV Community by Richard Lemon (@richardlemon).</description>
    <link>https://dev.to/richardlemon</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3798270%2F7fc64f22-b7f0-471f-9ac1-e15050494121.jpeg</url>
      <title>DEV Community: Richard Lemon</title>
      <link>https://dev.to/richardlemon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/richardlemon"/>
    <language>en</language>
    <item>
      <title>Why I Track HRV Every Morning (And How It Actually Changes My Day)</title>
      <dc:creator>Richard Lemon</dc:creator>
      <pubDate>Mon, 25 May 2026 05:00:00 +0000</pubDate>
      <link>https://dev.to/richardlemon/why-i-track-hrv-every-morning-and-how-it-actually-changes-my-day-3egk</link>
      <guid>https://dev.to/richardlemon/why-i-track-hrv-every-morning-and-how-it-actually-changes-my-day-3egk</guid>
      <description>&lt;h2&gt;HRV is only useful if it changes your day&lt;/h2&gt;

&lt;p&gt;I have tracked HRV every morning for a few years now. Not because I like graphs. Because it is the only number that consistently talks back when I am lying to myself.&lt;/p&gt;

&lt;p&gt;I build web stuff, coach baseball, and play the biohacker hobby game. That combination makes it very easy to run on willpower and caffeine until something breaks. HRV is my early warning system.&lt;/p&gt;

&lt;p&gt;But here is the key point. I do not care about the exact number. I care about what I do differently because of it. No behavior change, no point.&lt;/p&gt;

&lt;h2&gt;The setup: cheap, boring, and consistent&lt;/h2&gt;

&lt;p&gt;I do not treat HRV like a gadget fashion show. I want boring and repeatable.&lt;/p&gt;

&lt;p&gt;My morning routine:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Wake up, bathroom, water.&lt;/li&gt;
  &lt;li&gt;Sit on the same chair.&lt;/li&gt;
  &lt;li&gt;Same 60–90 second HRV reading with a chest strap and phone app.&lt;/li&gt;
  &lt;li&gt;Eyes open, normal breathing, no breathwork tricks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I care about three things only:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;My rolling baseline.&lt;/strong&gt; Roughly the 7–30 day trend.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Today vs that baseline.&lt;/strong&gt; How far up or down.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Direction.&lt;/strong&gt; Is it drifting up, flat, or sagging over a few days.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app gives me a recovery score. That score is just math on top of the HRV number, but it is easier to work with a simple traffic light in my head.&lt;/p&gt;

&lt;h2&gt;The traffic light: green, yellow, red&lt;/h2&gt;

&lt;p&gt;I run my life on a very dumb, very useful mental model.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Green&lt;/strong&gt;: Nervous system looks ready. HRV at or above baseline. Resting heart rate normal.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Yellow&lt;/strong&gt;: Slightly suppressed HRV or resting heart rate a bit elevated, or both. Body is stressed but not falling apart.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Red&lt;/strong&gt;: Noticeably down HRV, sometimes paired with a big bump in resting heart rate, plus I feel like cardboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is it. No decimal worship. I do not care if my RMSSD is 68 or 72. I care if the light is green, yellow, or red and where that sits relative to my baseline.&lt;/p&gt;

&lt;p&gt;The important part is what I decided in advance for each color. If you make rules on the spot you will negotiate yourself into dumb decisions.&lt;/p&gt;

&lt;h2&gt;Green days: permission to go heavy&lt;/h2&gt;

&lt;p&gt;On a green day, I treat HRV clearance as permission. Not as pressure.&lt;/p&gt;

&lt;p&gt;I have three decision buckets that change when I see green: training, work, and recovery inputs.&lt;/p&gt;

&lt;h3&gt;Green training rules&lt;/h3&gt;

&lt;p&gt;Green usually means:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Hard session goes ahead&lt;/strong&gt;: heavy lifts, sprint work, or a tough baseball practice.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Volume is allowed to climb&lt;/strong&gt;: I may add a set or two, or stretch the conditioning a bit.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Experiment window is open&lt;/strong&gt;: if I want to test a new drill, a different sprint pattern, or a new strength block, I try it on a green day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example from last month: I had a planned heavy lower body day with some sprint repeats. HRV was clearly green, slightly above baseline after two nights of solid sleep. Instead of just running the default, I pushed the sprint count by 20 percent and added one heavier top set on squats.&lt;/p&gt;

&lt;p&gt;The session felt good, no grindy reps, and HRV stayed stable the next morning. That told me my body could handle that new load. So I locked that progression in as the new normal for the block.&lt;/p&gt;

&lt;h3&gt;Green work rules&lt;/h3&gt;

&lt;p&gt;Green day equals high-quality cognitive work window.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;I schedule my hardest coding or design work as early as possible.&lt;/li&gt;
  &lt;li&gt;I avoid early calls. I move meetings to the afternoon if I can.&lt;/li&gt;
  &lt;li&gt;If there is a tricky refactor or a gnarly WebGL experiment, this is the day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;HRV does not directly tell me about focus, but there is a pattern. When recovery is solid, my willingness to sit with hard problems is much higher.&lt;/p&gt;

&lt;h3&gt;Green recovery rules&lt;/h3&gt;

&lt;p&gt;When I see green I do &lt;em&gt;not&lt;/em&gt; celebrate with junk behavior. I do the opposite.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Keep caffeine normal, do not double it just because I feel good.&lt;/li&gt;
  &lt;li&gt;Stick to my usual bedtime rather than “rewarding” myself with a late night.&lt;/li&gt;
  &lt;li&gt;Give myself a small recovery boost, like a longer walk in the sun.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Green is where you build new capacity. You can only do that if you do not instantly spend all the extra energy on random nonsense.&lt;/p&gt;

&lt;h2&gt;Yellow days: ego control territory&lt;/h2&gt;

&lt;p&gt;Most days are not green or red. They are slightly off. That is where the traffic light actually earns its keep.&lt;/p&gt;

&lt;p&gt;Yellow is where my ego wants to ignore the data. This is also where I have made the most dumb training mistakes in the past.&lt;/p&gt;

&lt;h3&gt;Yellow training rules&lt;/h3&gt;

&lt;p&gt;On a yellow day I do not cancel training. I change the framing.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Intensity stays, volume drops&lt;/strong&gt;: I still lift heavy or move fast, but I cut sets or total reps.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Or volume stays, intensity drops&lt;/strong&gt;: I keep the number of sets, but I stay well away from failure and keep heart rate lower.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;No new experiments&lt;/strong&gt;: I do not test 1RMs, new sprint distances, or brutal circuits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Concrete example. My HRV is slightly below baseline after a late-night game or a long coding session that turned into a 1 AM situation. I had planned 5 x 5 heavy bench and a bunch of accessory work.&lt;/p&gt;

&lt;p&gt;On a yellow day, I might keep the 5 x 5 but drop the load by 5–10 percent, skip one accessory exercise, and cut conditioning in half. I still show up. The workout still happens. I do not dig a deeper hole.&lt;/p&gt;

&lt;h3&gt;Yellow work rules&lt;/h3&gt;

&lt;p&gt;Yellow days are for progress without heroics.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Ship small pieces. Fix bugs, clean up code, handle admin.&lt;/li&gt;
  &lt;li&gt;Push big decisions to a green day if possible.&lt;/li&gt;
  &lt;li&gt;Be very suspicious of “one more hour” thinking in the evening.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I force a late-night coding binge on a yellow day, HRV usually hits red the next morning. I have seen that pattern enough times to consider it basically deterministic for me.&lt;/p&gt;

&lt;h3&gt;Yellow recovery rules&lt;/h3&gt;

&lt;p&gt;On yellow I start turning dials.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Earlier cutoff for screens and work.&lt;/li&gt;
  &lt;li&gt;A bit more carbs in the evening to support sleep.&lt;/li&gt;
  &lt;li&gt;Move non-urgent life tasks to another day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The idea is simple. Yellow means “you are drifting.” I want to see if I can pull myself back toward green tomorrow without skipping life.&lt;/p&gt;

&lt;h2&gt;Red days: forced humility&lt;/h2&gt;

&lt;p&gt;Red is where HRV saves me from myself. This is the only color where I am willing to scrap the plan completely.&lt;/p&gt;

&lt;p&gt;Red usually comes from one of a few predictable causes for me:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Travel and broken sleep.&lt;/li&gt;
  &lt;li&gt;Back-to-back high intensity days because I ignored a yellow signal.&lt;/li&gt;
  &lt;li&gt;Getting sick, or fighting something off.&lt;/li&gt;
  &lt;li&gt;Two days of very late games or events.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Red training rules&lt;/h3&gt;

&lt;p&gt;On a red day I have a simple default.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;No high-intensity training.&lt;/li&gt;
  &lt;li&gt;LISS cardio only: easy walk, light bike, mobility.&lt;/li&gt;
  &lt;li&gt;If I feel awful, full rest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I already stacked a few red or deep yellow days in a row, I usually remove any training pressure completely. I let the system reset. Every time I ignored that, HRV punished me with a longer slump and worse sleep.&lt;/p&gt;

&lt;p&gt;One specific pattern: if HRV tanks and resting heart rate is up, and &lt;em&gt;perceived effort&lt;/em&gt; spiked on what should have been an easy session the day before, I do not negotiate. I shut it down for that day. That combo almost always means something is off globally, not just “I am a bit tired.”&lt;/p&gt;

&lt;h3&gt;Red work rules&lt;/h3&gt;

&lt;p&gt;On red days I lower the ambition floor.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Cancel non-essential meetings if I can.&lt;/li&gt;
  &lt;li&gt;Move deep work to another day. Only do it if I somehow feel unusually sharp despite the score.&lt;/li&gt;
  &lt;li&gt;Knock out simple, mechanical tasks and stop pretending I will architect a whole new feature set in that state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I had to get over the guilt of this. But every time I pretend I am fine on a red day, I pay for it later with sloppy code and more time spent fixing mistakes.&lt;/p&gt;

&lt;h3&gt;Red recovery rules&lt;/h3&gt;

&lt;p&gt;Red is where I go heavier on interventions.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Hard stop on screens at least an hour before bed.&lt;/li&gt;
  &lt;li&gt;Light dinner, no late heavy meals.&lt;/li&gt;
  &lt;li&gt;Walks instead of workouts, sunlight early in the day.&lt;/li&gt;
  &lt;li&gt;If possible, a short nap, but capped so I do not wreck the night.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also use red days as an audit. Why am I here? Was it a one-off travel day or have I been stacking small stupid decisions for a week?&lt;/p&gt;

&lt;h2&gt;Where HRV actually changed my behavior&lt;/h2&gt;

&lt;p&gt;HRV itself did not change much in my life on day one. The traffic light and pre-committed rules did.&lt;/p&gt;

&lt;p&gt;Here are a few specific shifts that stuck.&lt;/p&gt;

&lt;h3&gt;I stopped pretending sleep does not matter&lt;/h3&gt;

&lt;p&gt;Before HRV, I treated sleep like a flexible variable. If I wanted to code late or watch a game, I did it. I would “catch up” later.&lt;/p&gt;

&lt;p&gt;HRV showed me something simple. Two short nights do more damage than one really short night. The second late night pushes the nervous system into a state where the next day is always worse than it feels in the moment.&lt;/p&gt;

&lt;p&gt;Green-to-red swings after stacked short nights convinced me to protect at least five nights a week. I still have late nights, just fewer in a row.&lt;/p&gt;

&lt;h3&gt;I stopped proving toughness to no one&lt;/h3&gt;

&lt;p&gt;Most of the athletes I coach, and most developers I know, have the same problem. We mistake stubbornness for discipline.&lt;/p&gt;

&lt;p&gt;HRV gave me a clean excuse to stop that. If the score is red and the trend has been sliding for three days, I do not get bonus points for “pushing through.” I get slower progress and more nagging injuries.&lt;/p&gt;

&lt;p&gt;It took a while, but now if the traffic light is red and I still feel the itch to max out something, I treat that as a bug in my behavior, not a sign of commitment.&lt;/p&gt;

&lt;h3&gt;I started planning weeks, not days&lt;/h3&gt;

&lt;p&gt;Tracking HRV showed me that what I do on Monday is still echoing around on Thursday.&lt;/p&gt;

&lt;p&gt;So now I design weeks around likely traffic lights:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Monday and Thursday are likely green: main heavy training and hardest coding.&lt;/li&gt;
  &lt;li&gt;Tuesday and Friday are more flexible: if I see yellow, those become lighter work or technique days.&lt;/li&gt;
  &lt;li&gt;Weekend is the buffer: travel, games, or family stuff that may push HRV around.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not perfect. Life rarely respects my plans. But thinking in terms of green / yellow / red probability has made my training blocks far more stable.&lt;/p&gt;

&lt;h2&gt;HRV is not a boss, it is a negotiating partner&lt;/h2&gt;

&lt;p&gt;I do not outsource decisions to HRV. I use it as one more voice in the room.&lt;/p&gt;

&lt;p&gt;Sometimes I override it. For example, if it is a once-a-year event or competition, I am going to show up and do the thing, even on a red day. I just go in with my eyes open. I expect a recovery tax afterwards and I plan around it.&lt;/p&gt;

&lt;p&gt;Most days though, the traffic light is enough to keep my ego in check and my training productive.&lt;/p&gt;

&lt;h2&gt;If you want to steal this system&lt;/h2&gt;

&lt;p&gt;If you want to copy anything here, copy the decision loop, not the tech.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Pick &lt;strong&gt;one&lt;/strong&gt; HRV method. Keep it boring and consistent.&lt;/li&gt;
  &lt;li&gt;Track for a few weeks without changing anything just to see your baseline.&lt;/li&gt;
  &lt;li&gt;Define your own clear green, yellow, red thresholds in that app or in a notebook.&lt;/li&gt;
  &lt;li&gt;Write down rules for each color &lt;em&gt;before&lt;/em&gt; you see the next score.&lt;/li&gt;
  &lt;li&gt;Commit to following those rules for a month and see what happens to performance and mood.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I think most people obsess over gadgets and miss the point. HRV is not about who has the highest number. It is about having a simple, brutally honest mirror that you agree to listen to every morning.&lt;/p&gt;

&lt;p&gt;That is why I still track it. Not because I care about today’s exact milliseconds. Because it changes what I do at 9 AM.&lt;/p&gt;

</description>
      <category>devjournal</category>
      <category>mentalhealth</category>
      <category>productivity</category>
      <category>watercooler</category>
    </item>
    <item>
      <title>The Make.com Modules I Actually Use Every Week (And The Ones I Killed)</title>
      <dc:creator>Richard Lemon</dc:creator>
      <pubDate>Sat, 23 May 2026 22:00:00 +0000</pubDate>
      <link>https://dev.to/richardlemon/the-makecom-modules-i-actually-use-every-week-and-the-ones-i-killed-4j62</link>
      <guid>https://dev.to/richardlemon/the-makecom-modules-i-actually-use-every-week-and-the-ones-i-killed-4j62</guid>
      <description>&lt;h2&gt;The problem with my Make.com setup&lt;/h2&gt;

&lt;p&gt;I use Make.com a lot. Client work, my own projects, experiments, tracking weird biohacking data, even baseball stuff. It started simple. Then, like every tool I enjoy, it turned into a zoo.&lt;/p&gt;

&lt;p&gt;Too many scenarios. Too many modules. Too many runs where I had to ask: “Why did this even fire?”&lt;/p&gt;

&lt;p&gt;A few months ago I did a hard audit of all my Make scenarios. Not a theoretical one. I looked at what ran weekly, what broke, what I ignored, and what quietly ate time and operations without giving anything back.&lt;/p&gt;

&lt;p&gt;This is the result. The Make modules that actually earn their place in my stack every week. And the ones I stopped using because they were just clever complexity.&lt;/p&gt;

&lt;h2&gt;Ground rules for staying in my stack&lt;/h2&gt;

&lt;p&gt;I started by setting a few brutal rules. A module or pattern had to tick at least one of these boxes to survive:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;It removes a recurring manual task&lt;/strong&gt; I was doing at least weekly.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;It surfaces information I actually act on&lt;/strong&gt; within 24 hours.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;It is easy to debug at 23:30&lt;/strong&gt; when a client pings me.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a module only existed because it was “cool” or because some YouTube tutorial said it was “powerful”, it went on the chopping block.&lt;/p&gt;

&lt;p&gt;With that, here is what survived.&lt;/p&gt;

&lt;h2&gt;HTTP modules: boring, essential, everywhere&lt;/h2&gt;

&lt;p&gt;If Make.com removed the HTTP modules tomorrow, half my setup would die. HTTP is my default way to talk to anything that does not have a decent Make app, or has one but I do not trust it.&lt;/p&gt;

&lt;p&gt;The ones I use every week:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;HTTP → Make a request&lt;/strong&gt; For hitting custom webhooks, internal tools, small APIs I host on cheap servers.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Webhook → Custom webhook&lt;/strong&gt; For receiving events from my own apps and client projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example from my actual stack. I have a tiny Go API that aggregates baseball practice data, sleep metrics, and a couple of biomarker logs. Make calls this API via HTTP every morning at 06:00, does a bit of mapping, and posts a summary into a private Slack channel I call &lt;code&gt;#body-log&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I used to run this through three different native integrations. It broke often. I was constantly re-authing things. Replaced everything with one HTTP module, one custom webhook, and a small script on my server. Zero regrets.&lt;/p&gt;

&lt;p&gt;Module verdict: &lt;strong&gt;HTTP stays. Always.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;Routers + filters: my actual “no-code if/else”&lt;/h2&gt;

&lt;p&gt;I tried to avoid routers for a while. They felt heavy and visually noisy. Then I realised I was trying to fake routers with duplicated scenarios and filters at the start. That was worse.&lt;/p&gt;

&lt;p&gt;Now I use routers and filters in almost every scenario that survives longer than a week.&lt;/p&gt;

&lt;p&gt;Typical weekly pattern. I have a master “events in my world” scenario. It listens to webhooks from:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;My portfolio contact form&lt;/li&gt;
  &lt;li&gt;Client project error hooks&lt;/li&gt;
  &lt;li&gt;New newsletter signups&lt;/li&gt;
  &lt;li&gt;Some very opinionated monitoring pings I run on my infra&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One router. Four paths. Each path has a very clear filter like “&lt;code&gt;type = 'contact_form'&lt;/code&gt;” or “&lt;code&gt;severity &amp;gt;= 'error'&lt;/code&gt;”. The outputs hit different Slack channels, different Notion databases, and in one case an SMS if it is client-critical.&lt;/p&gt;

&lt;p&gt;The important part. I stopped doing clever nested routers. One level only. If I need more logic than that, it goes into my code, not into Make.&lt;/p&gt;

&lt;p&gt;Module verdict: &lt;strong&gt;Routers stay, but only one level deep.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;Slack: my notification bus, not my database&lt;/h2&gt;

&lt;p&gt;Slack is my broadcast layer. Not my brain. Not my storage. If a scenario ends in Slack, I either need to react or at least see it once. Otherwise it should not go there.&lt;/p&gt;

&lt;p&gt;Modules that run every week:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Slack → Create a message&lt;/strong&gt; For system alerts, new leads, and health summaries.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Slack → Create a scheduled message&lt;/strong&gt; For repeating nudges. Example: a weekly prompt to do a quick retrospective on projects and training.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One specific flow. When someone fills in a project inquiry on my site, Make receives the webhook, tags it based on budget and timeline, pushes it into a Notion CRM database, then posts a short summary to &lt;code&gt;#inbox&lt;/code&gt; with a direct “Reply in under 12 hours” reminder tag.&lt;/p&gt;

&lt;p&gt;I used to push every possible event into Slack. Deploy hooks. CI results. New followers. Half of that turned into noise. I killed an entire category of Slack notifications and did not miss them once.&lt;/p&gt;

&lt;p&gt;Module verdict: &lt;strong&gt;Slack stays, but only for things I can act on quickly.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;Notion: slow, slightly painful, still very useful&lt;/h2&gt;

&lt;p&gt;I have a love-hate relationship with Notion as an API target. It is not fast. It is sometimes weird with rate limits. But it sits in the middle of how I plan work and content, so I tolerate it.&lt;/p&gt;

&lt;p&gt;Every week I use:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Notion → Create a database item&lt;/strong&gt; Anything that should live longer than a notification.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Notion → Update a database item&lt;/strong&gt; For syncing status from other tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One concrete example. For this blog, I draft in Markdown locally. When I push a new post into my Git repo, a small server-side hook hits Make via webhook. Make then creates or updates a Notion page in my “Content Log” database with:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Title&lt;/li&gt;
  &lt;li&gt;Slug&lt;/li&gt;
  &lt;li&gt;Status (Draft, Scheduled, Live)&lt;/li&gt;
  &lt;li&gt;URL&lt;/li&gt;
  &lt;li&gt;Technical notes (stack, integrations, experiments)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I used to manage that inside Notion directly with templates and properties and a whole ceremony. Now Make is the bridge from my real workflow (git, editor, deploy) into my “remember this later” tool.&lt;/p&gt;

&lt;p&gt;What I stopped doing. I used to sync every little metric into Notion. Newsletter subscriber counts. Basic analytics. It felt like a quantified self dashboard. I never looked at it. Now I only store artefacts and decisions, not vanity numbers.&lt;/p&gt;

&lt;p&gt;Module verdict: &lt;strong&gt;Notion stays, but only for durable records, not dashboards.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;Google Sheets: my scratchpad and debug console&lt;/h2&gt;

&lt;p&gt;I think Google Sheets is underrated as a Make companion. Not as a database, but as a scratchpad.&lt;/p&gt;

&lt;p&gt;Weekly modules:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Google Sheets → Add a row&lt;/strong&gt; For lightweight logging and temporary reports.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Google Sheets → Get a cell / Get a row&lt;/strong&gt; For quick lookups when a real database would be overkill.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example. During a recent experiment on sleep and training volume, I patched together data from several APIs that did not agree on timestamps or time zones. Before building a nice pipeline, I sent everything to a Google Sheet first. One flat sheet. One row per event. Timestamps, scores, tags.&lt;/p&gt;

&lt;p&gt;I used that as a human-readable debug view while I iterated on the mapping logic in Make. Once I trusted the pipeline, I replaced the sheet with a proper HTTP call into my API. The sheet scenario is still there, just turned off. It is my “turn this on for debugging” escape hatch.&lt;/p&gt;

&lt;p&gt;Module verdict: &lt;strong&gt;Sheets stays as my visible logbook, not as core storage.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;Text &amp;amp; data transformers: the unsung heroes&lt;/h2&gt;

&lt;p&gt;If a Make scenario feels complicated, it is usually because I forgot that Make has a lot of little transformer modules and functions that remove the need for code.&lt;/p&gt;

&lt;p&gt;Every week I use:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Text parser and regex-related functions&lt;/strong&gt; For pulling IDs out of URLs, cleaning labels, normalising tags.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Formatter → Date and time&lt;/strong&gt; For making timestamps actually readable and consistent.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Array → Aggregate / Iterator&lt;/strong&gt; For reshaping messy API responses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One real scenario. My monitoring setup sends JSON payloads with long nested structures. Make receives them, then I use “Iterator” to walk through failing checks, clean each label with a Text function, and then aggregate them back into a compact Slack message.&lt;/p&gt;

&lt;p&gt;I used to firehose the raw JSON into Slack. No one read it. Not even me. Now the message is human-sized and I only link to the raw payload when I actually need to debug.&lt;/p&gt;

&lt;p&gt;Module verdict: &lt;strong&gt;These stay. They make other modules simpler.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;The modules I stopped using regularly&lt;/h2&gt;

&lt;p&gt;Now the fun part. The tools I actively moved away from. Some of them are great on paper, just not for how I work.&lt;/p&gt;

&lt;h3&gt;Gmail &amp;amp; email modules as the primary trigger&lt;/h3&gt;

&lt;p&gt;I used to have a bunch of scenarios that started with “New email in Gmail” or “Watch emails”. They tried to be smart. Auto-labeling. Auto-forwarding. Auto-creating tasks.&lt;/p&gt;

&lt;p&gt;The problem. Email is already messy. Adding an automation layer that sometimes works and sometimes does nothing makes it worse. I also do not want to give every automation direct access to my entire inbox.&lt;/p&gt;

&lt;p&gt;What I do instead. Email is almost always the &lt;strong&gt;output&lt;/strong&gt; now, not the input. If something critical happens in a system that is not wired into my attention stack, I send myself a short email. That is it.&lt;/p&gt;

&lt;p&gt;Module verdict: &lt;strong&gt;Gmail triggers are out. Email stays as occasional output only.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;Make Data Store as a hidden database&lt;/h3&gt;

&lt;p&gt;Make has a built-in Data Store. On paper it is convenient. Throw key-value pairs in there. Persist things. Done.&lt;/p&gt;

&lt;p&gt;In practice, I found it dangerous. It is invisible from the outside, hard to migrate, and a bit too easy to rely on for critical data. I had one scenario where I used Data Store to track daily task limits per client. When I rebuilt that scenario a year later, I forgot the Data Store even existed and almost wiped it.&lt;/p&gt;

&lt;p&gt;Now I keep state either in my own API, in a real database, or in something like Notion if the stakes are low. Data should live somewhere I can back up, inspect, and move without Make in the middle.&lt;/p&gt;

&lt;p&gt;Module verdict: &lt;strong&gt;Data Store is out for me, except for throwaway experiments.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;Scheduling heavy logic directly inside Make&lt;/h3&gt;

&lt;p&gt;I went through a phase where I tried to make Make my central scheduler. Complex recurring jobs. Custom repeat rules. User-specific offsets. You can do it. I did. It was fragile.&lt;/p&gt;

&lt;p&gt;Make is great at “When X happens, do Y”. It is less great at being a full-on job scheduler for dozens of different rules and calendars. Especially once you start mixing time zones.&lt;/p&gt;

&lt;p&gt;Now I keep scheduling logic in my own services. They decide &lt;strong&gt;when&lt;/strong&gt; to call Make, usually via webhook. Make just does the orchestration work.&lt;/p&gt;

&lt;p&gt;Module verdict: &lt;strong&gt;Advanced scheduling inside Make is out. I trust my own code for that.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;Over-using search modules as if Make was my search engine&lt;/h3&gt;

&lt;p&gt;Make has lots of “Search” modules. Search rows. Search pages. Search messages. I abused them early on.&lt;/p&gt;

&lt;p&gt;Pattern looked like this. Scenario triggers. Then: “Search X by Y” to find a record. Then update it. Worked fine at small scale. Terrible once you grow a bit. Slow. Expensive. Painful to debug when matching goes wrong.&lt;/p&gt;

&lt;p&gt;These days I try to design flows so I always pass IDs around explicitly. When something is created, I keep the identifier and store it in the other system right away. No more “guess which row” logic.&lt;/p&gt;

&lt;p&gt;Module verdict: &lt;strong&gt;Search is a fallback now, not a default.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;How I decide if a new module earns a place&lt;/h2&gt;

&lt;p&gt;Every time I add a new module or app into my Make world, I run it through a simple checklist.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Can I explain this scenario in one sentence?&lt;/strong&gt; If I cannot, it is already too complex.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Can I debug it half-asleep?&lt;/strong&gt; If an error email at midnight would confuse future me, I simplify it.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Can I rebuild this without Make?&lt;/strong&gt; If the answer is no, I am locking too much into one tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Make is fantastic glue. It is not my app platform. It is the thing that connects my apps, my scripts, and the buckets where I keep data.&lt;/p&gt;

&lt;p&gt;The modules that survived this audit all share one thing. They do boring, reliable work. HTTP moves data. Routers branch logic. Slack shouts when something matters. Notion stores decisions. Sheets gives me a visual log. Everything else is optional.&lt;/p&gt;

&lt;p&gt;If your Make account feels loud and untrustworthy, I would start there. Open your scenario list. Sort by last run. Keep what actually runs weekly and makes your life easier. Kill the rest. You will not miss as much as you think.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>nocode</category>
      <category>productivity</category>
      <category>tooling</category>
    </item>
    <item>
      <title>CSS :has() Selector: The Layout Trick I Wish I Knew 5 Years Ago</title>
      <dc:creator>Richard Lemon</dc:creator>
      <pubDate>Sat, 23 May 2026 05:00:00 +0000</pubDate>
      <link>https://dev.to/richardlemon/css-has-selector-the-layout-trick-i-wish-i-knew-5-years-ago-4mbh</link>
      <guid>https://dev.to/richardlemon/css-has-selector-the-layout-trick-i-wish-i-knew-5-years-ago-4mbh</guid>
      <description>&lt;h2&gt;CSS :has() is not just a fancy :parent&lt;/h2&gt;

&lt;p&gt;
When &lt;code&gt;:has()&lt;/code&gt; started popping up in specs and tweets, I mentally filed it under “cool, but not for shipping work.” I was wrong.
&lt;/p&gt;

&lt;p&gt;
Now it is in Chrome, Safari, Edge, and Firefox. I use it in real projects. It has removed entire JavaScript files and a pile of &lt;code&gt;.is-active&lt;/code&gt; classes that I was embarrassed to maintain.
&lt;/p&gt;

&lt;p&gt;
If you are a working frontend dev, the shorthand is this: &lt;code&gt;:has()&lt;/code&gt; turns CSS from “style what is there” into “style this thing &lt;strong&gt;if it contains&lt;/strong&gt; that thing”. That one capability changes layout, state, and validation flows.
&lt;/p&gt;

&lt;p&gt;
I will walk through three places where it made a real difference for me:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Parent styling without JS&lt;/li&gt;
  &lt;li&gt;Sibling state UIs without wiring events&lt;/li&gt;
  &lt;li&gt;Form validation UI that reacts to the DOM, not a framework&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
All of this shipped with zero additional JavaScript.
&lt;/p&gt;

&lt;h2&gt;Quick mental model of :has()&lt;/h2&gt;

&lt;p&gt;
The syntax looks like a pseudo class on a selector:
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.card:has(img.hero) {
  /* styles here */
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Read it as: “select &lt;code&gt;.card&lt;/code&gt; elements that &lt;strong&gt;have&lt;/strong&gt; a descendant &lt;code&gt;img.hero&lt;/code&gt; somewhere inside.” It is a conditional filter on the left side of the selector.
&lt;/p&gt;

&lt;p&gt;
You can also scope it more tightly:
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.tabs:has(&amp;gt; .tab.is-active) {
  /* direct children only */
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Or use it with relational selectors like siblings:
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.field:has(+ .field--error) {
  /* this .field is followed by an error field */
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Once that clicks, you start seeing places to remove JS.
&lt;/p&gt;

&lt;h2&gt;1. Parent styling in a content-heavy project&lt;/h2&gt;

&lt;p&gt;
First real use: a content-heavy marketing site for a biohacking brand I work with. Editors can drop components in any order with a CMS. Sometimes a card has an image, sometimes it is text-only. The layout should adapt.
&lt;/p&gt;

&lt;p&gt;
Previously I solved this with modifier classes from the CMS, or a hydration script that scans the DOM and adds classes like &lt;code&gt;.card--with-media&lt;/code&gt;. Boring, fragile, and slightly gross.
&lt;/p&gt;

&lt;p&gt;
With &lt;code&gt;:has()&lt;/code&gt; I deleted that script.
&lt;/p&gt;

&lt;h3&gt;Image-aware cards&lt;/h3&gt;

&lt;p&gt;
The card markup is boring on purpose:
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;article class="card"&amp;gt;
  &amp;lt;img class="card__media" src="hero.jpg" alt=""&amp;gt;
  &amp;lt;div class="card__body"&amp;gt;
    &amp;lt;h2&amp;gt;Title&amp;lt;/h2&amp;gt;
    &amp;lt;p&amp;gt;Some text...&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/article&amp;gt;

&amp;lt;article class="card"&amp;gt;
  &amp;lt;div class="card__body"&amp;gt;
    &amp;lt;h2&amp;gt;Another Card&amp;lt;/h2&amp;gt;
    &amp;lt;p&amp;gt;Text-only card.&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/article&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Now the CSS decides layout based on presence of media.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.card {
  display: grid;
  gap: 1rem;
}

.card:has(.card__media) {
  grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
  align-items: center;
}

.card:not(:has(.card__media)) {
  padding: 2rem;
  background: #111;
  color: #eee;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Result: if marketing drops in an image, the card becomes a two-column layout. If not, it becomes a full-width text block. No new class. No CMS configuration. No JS.
&lt;/p&gt;

&lt;p&gt;
I like this because the markup stays semantic and dumb. The layout is a true function of the content, which is what CSS was always supposed to do but rarely could at the parent level.
&lt;/p&gt;

&lt;h3&gt;Auto-promoting “hero” sections&lt;/h3&gt;

&lt;p&gt;
Same project. Editors could add a &lt;code&gt;.section&lt;/code&gt; stack: some had a prominent CTA, some were just copy. If a section had a primary CTA, design wanted extra padding and a gradient background.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;section class="section"&amp;gt;
  &amp;lt;h2&amp;gt;Get early access&amp;lt;/h2&amp;gt;
  &amp;lt;p&amp;gt;Short description.&amp;lt;/p&amp;gt;
  &amp;lt;a class="btn btn--primary" href="#"&amp;gt;Join the beta&amp;lt;/a&amp;gt;
&amp;lt;/section&amp;gt;

&amp;lt;section class="section"&amp;gt;
  &amp;lt;h2&amp;gt;What you get&amp;lt;/h2&amp;gt;
  &amp;lt;p&amp;gt;More text...&amp;lt;/p&amp;gt;
&amp;lt;/section&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
With &lt;code&gt;:has()&lt;/code&gt; I treat any section with a primary button as a pseudo hero.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.section {
  padding: 2rem 1.5rem;
  background: #050505;
}

.section:has(.btn--primary) {
  padding: 4rem 1.5rem;
  background: radial-gradient(circle at top, #2f80ed, #050505);
  color: #fff;
}

.section:has(.btn--primary) h2 {
  font-size: 2.25rem;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
That tiny selector replaced a custom “hero” block type in the CMS that content editors kept misusing. I stopped explaining “use the hero component for this” and just let the CSS infer intent from presence of a primary CTA.
&lt;/p&gt;

&lt;p&gt;
You can do similar things with &lt;code&gt;:has(video)&lt;/code&gt;, &lt;code&gt;:has(.badge--new)&lt;/code&gt;, etc. It is a good fit for messy CMS content where you want layout to respond to what your editors actually do, not what the schema designer hoped they would do.
&lt;/p&gt;

&lt;h2&gt;2. Sibling state UIs without event listeners&lt;/h2&gt;

&lt;p&gt;
Second use case: stateful UIs that I used to wire up with click handlers. Tabs, disclosure panels, navigation highlights, that stuff.
&lt;/p&gt;

&lt;p&gt;
Yes, you can still do it in JS. But if the state is already visible in the DOM, &lt;code&gt;:has()&lt;/code&gt; lets CSS own more of the behavior. That means less code, fewer states to sync, and fewer bugs.
&lt;/p&gt;

&lt;h3&gt;Tabs powered by :target and :has()&lt;/h3&gt;

&lt;p&gt;
On a little side project for baseball drills, I built a tabbed interface where each tab is actually a link to an anchor. I wanted a sticky tab bar that changes style when any tab content is active.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;div class="tabs"&amp;gt;
  &amp;lt;nav class="tabs__nav"&amp;gt;
    &amp;lt;a href="#hitting"&amp;gt;Hitting&amp;lt;/a&amp;gt;
    &amp;lt;a href="#pitching"&amp;gt;Pitching&amp;lt;/a&amp;gt;
    &amp;lt;a href="#fielding"&amp;gt;Fielding&amp;lt;/a&amp;gt;
  &amp;lt;/nav&amp;gt;

  &amp;lt;section id="hitting" class="tabs__panel"&amp;gt;...&amp;lt;/section&amp;gt;
  &amp;lt;section id="pitching" class="tabs__panel"&amp;gt;...&amp;lt;/section&amp;gt;
  &amp;lt;section id="fielding" class="tabs__panel"&amp;gt;...&amp;lt;/section&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
The panels show / hide with a regular &lt;code&gt;:target&lt;/code&gt; trick.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.tabs__panel {
  display: none;
}

.tabs__panel:target {
  display: block;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Old me would now add JS to toggle classes on the nav. Instead I lean on &lt;code&gt;:has()&lt;/code&gt;.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.tabs {
  border-bottom: 1px solid #333;
}

.tabs__nav a {
  padding: .5rem 1rem;
  text-decoration: none;
  color: #888;
}

.tabs__nav a:is(:hover, :focus-visible) {
  color: #fff;
}

/* highlight the active tab label */
.tabs__nav a[href^="#"] {
  position: relative;
}

.tabs:has(#hitting:target) .tabs__nav a[href="#hitting"],
.tabs:has(#pitching:target) .tabs__nav a[href="#pitching"],
.tabs:has(#fielding:target) .tabs__nav a[href="#fielding"] {
  color: #fff;
  font-weight: 600;
}

/* make the whole tabs block look active if any panel is targeted */
.tabs:has(.tabs__panel:target) {
  border-color: #2f80ed;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
I am not pretending this scales to 50 tabs. For most content UIs, 3 to 5 tabs is realistic. Writing those few selectors is still cheaper than adding a tab manager, handling history state, and worrying about hydration.
&lt;/p&gt;

&lt;p&gt;
The key pattern is: some child panel already has state via &lt;code&gt;:target&lt;/code&gt; or &lt;code&gt;[aria-selected="true"]&lt;/code&gt;. Let &lt;code&gt;:has()&lt;/code&gt; bubble that state up to parents and siblings.
&lt;/p&gt;

&lt;h3&gt;Accordion with native &amp;lt;details&amp;gt; and :has()&lt;/h3&gt;

&lt;p&gt;
I use &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; a lot. It is surprisingly powerful with &lt;code&gt;:has()&lt;/code&gt;. On a settings panel I wanted the container to visually compress when no section was open, then expand once any accordion entry was open.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;section class="settings"&amp;gt;
  &amp;lt;details class="settings__item"&amp;gt;
    &amp;lt;summary&amp;gt;Profile&amp;lt;/summary&amp;gt;
    &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;
  &amp;lt;/details&amp;gt;
  &amp;lt;details class="settings__item"&amp;gt;
    &amp;lt;summary&amp;gt;Privacy&amp;lt;/summary&amp;gt;
    &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;
  &amp;lt;/details&amp;gt;
&amp;lt;/section&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
CSS:
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.settings {
  padding: 1rem;
  border-radius: .75rem;
  border: 1px solid #333;
  max-height: 60vh;
  overflow: auto;
  transition: box-shadow .2s ease, border-color .2s ease;
}

.settings:has(.settings__item[open]) {
  border-color: #2f80ed;
  box-shadow: 0 16px 40px rgba(0, 0, 0, .55);
}

.settings__item + .settings__item {
  border-top: 1px solid #222;
}

.settings__item summary {
  cursor: pointer;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Once any &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; is open, the whole settings block feels “in focus”. No JS to listen for the toggle event, no syncing of &lt;code&gt;.is-active&lt;/code&gt; classes. The HTML already has &lt;code&gt;[open]&lt;/code&gt;. CSS reacts.
&lt;/p&gt;

&lt;h2&gt;3. Form validation UI with zero JavaScript&lt;/h2&gt;

&lt;p&gt;
The biggest win for me: form UI that uses &lt;code&gt;:has()&lt;/code&gt; with built-in browser validation. No client-side validation library. No “touched” state juggling.
&lt;/p&gt;

&lt;p&gt;
On my own site I revamped a contact form and a simple experiment log form. I wanted:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Parent field wrappers that highlight error or success&lt;/li&gt;
  &lt;li&gt;Inline messages that only show when actually invalid&lt;/li&gt;
  &lt;li&gt;Submit button that changes state based on form validity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
Browser validation already tracks validity. The DOM knows. &lt;code&gt;:has()&lt;/code&gt; lets CSS hook into that.
&lt;/p&gt;

&lt;h3&gt;Field states from input validity&lt;/h3&gt;

&lt;p&gt;
Markup:
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;form class="form" novalidate&amp;gt;
  &amp;lt;div class="field"&amp;gt;
    &amp;lt;label&amp;gt;
      Email
      &amp;lt;input type="email" name="email" required&amp;gt;
    &amp;lt;/label&amp;gt;
    &amp;lt;p class="field__error"&amp;gt;Please enter a valid email.&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;

  &amp;lt;div class="field"&amp;gt;
    &amp;lt;label&amp;gt;
      Message
      &amp;lt;textarea name="message" minlength="10" required&amp;gt;&amp;lt;/textarea&amp;gt;
    &amp;lt;/label&amp;gt;
    &amp;lt;p class="field__error"&amp;gt;Write at least 10 characters.&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;

  &amp;lt;button type="submit"&amp;gt;Send&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
You can bind field styling to the input inside, purely with CSS.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.field {
  margin-bottom: 1.5rem;
}

.field input,
.field textarea {
  width: 100%;
  padding: .6rem .75rem;
  border-radius: .4rem;
  border: 1px solid #444;
  background: #050505;
  color: #eee;
}

.field__error {
  display: none;
  margin-top: .35rem;
  font-size: .8rem;
  color: #ff6b6b;
}

/* highlight when invalid and touched (using :user-invalid where supported) */
.field:has(input:user-invalid),
.field:has(textarea:user-invalid) {
  color: #ff6b6b;
}

.field:has(input:user-invalid) input,
.field:has(textarea:user-invalid) textarea {
  border-color: #ff6b6b;
  box-shadow: 0 0 0 1px rgba(255, 107, 107, .6);
}

.field:has(input:user-invalid) .field__error,
.field:has(textarea:user-invalid) .field__error {
  display: block;
}

/* success state */
.field:has(input:user-valid),
.field:has(textarea:user-valid) {
  color: #4caf50;
}

.field:has(input:user-valid) input,
.field:has(textarea:user-valid) textarea {
  border-color: #4caf50;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
No custom event handlers. The browser decides when the input is valid or invalid. CSS uses &lt;code&gt;:has()&lt;/code&gt; to move that state to the wrapper and the message.
&lt;/p&gt;

&lt;p&gt;
If you want broader support than &lt;code&gt;:user-invalid&lt;/code&gt;, you can fall back to &lt;code&gt;:invalid&lt;/code&gt; and accept that some browsers show the state earlier.
&lt;/p&gt;

&lt;h3&gt;Form-level feedback and submit button state&lt;/h3&gt;

&lt;p&gt;
Now zoom out one level. The entire &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element also exposes validity via &lt;code&gt;:valid&lt;/code&gt; and &lt;code&gt;:invalid&lt;/code&gt;. Combine that with &lt;code&gt;:has()&lt;/code&gt; and your submit button can react.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.form button[type="submit"] {
  padding: .7rem 1.25rem;
  border-radius: .4rem;
  border: none;
  background: #333;
  color: #aaa;
  cursor: not-allowed;
  transition: background .15s ease, color .15s ease, transform .05s;
}

/* any invalid field keeps button in "disabled" style */
.form:has(:invalid) button[type="submit"] {
  background: #333;
  color: #777;
}

/* all fields valid, button goes live */
.form:has(:valid) button[type="submit"] {
  background: #2f80ed;
  color: #fff;
  cursor: pointer;
}

.form:has(:valid) button[type="submit"]:active {
  transform: translateY(1px);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
If you want to actually disable the button, you still need a tiny bit of JS to toggle the &lt;code&gt;disabled&lt;/code&gt; attribute. I usually do not bother for simple forms; the button just looks inactive until the browser considers the form valid.
&lt;/p&gt;

&lt;p&gt;
The nice part is that the logic lives where it belongs. The browser enforces constraints. CSS reads that state. JS, if present at all, sends the request and displays a toast.
&lt;/p&gt;

&lt;h2&gt;4. Layout tweaks based on children, not breakpoints&lt;/h2&gt;

&lt;p&gt;
One more pattern that has crept into my “default toolkit”: adjusting layout based on how many items a container has.
&lt;/p&gt;

&lt;p&gt;
On my baseball drills page, each drill has one or more tags. I wanted single-tag drills to show the tag inline next to the title, and multi-tag drills to move them into a separate row. Doing that in JS felt silly.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;article class="drill"&amp;gt;
  &amp;lt;header class="drill__header"&amp;gt;
    &amp;lt;h3 class="drill__title"&amp;gt;Front toss&amp;lt;/h3&amp;gt;
    &amp;lt;div class="drill__tags"&amp;gt;
      &amp;lt;span class="tag"&amp;gt;Hitting&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/header&amp;gt;
&amp;lt;/article&amp;gt;

&amp;lt;article class="drill"&amp;gt;
  &amp;lt;header class="drill__header"&amp;gt;
    &amp;lt;h3 class="drill__title"&amp;gt;Relay race&amp;lt;/h3&amp;gt;
    &amp;lt;div class="drill__tags"&amp;gt;
      &amp;lt;span class="tag"&amp;gt;Fielding&amp;lt;/span&amp;gt;
      &amp;lt;span class="tag"&amp;gt;Conditioning&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/header&amp;gt;
&amp;lt;/article&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
With &lt;code&gt;:has()&lt;/code&gt; and the &lt;code&gt;:nth-child()&lt;/code&gt; selector you can treat the two cases differently.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.drill__header {
  display: flex;
  gap: .5rem;
  align-items: baseline;
  flex-wrap: wrap;
}

/* one tag only: keep inline */
.drill__tags:has(.tag:nth-child(1):last-child) {
  order: 0;
}

/* more than one tag: push tags to next line */
.drill__tags:has(.tag:nth-child(2)) {
  flex-basis: 100%;
  order: 1;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
No JavaScript counting nodes. No data attributes. Just “if there is at least a second tag, change layout”. If product decides to add a third or fourth tag, the CSS keeps working.
&lt;/p&gt;

&lt;h2&gt;Reality check: performance and support&lt;/h2&gt;

&lt;p&gt;
I am not going to pretend &lt;code&gt;:has()&lt;/code&gt; is free. The browser has to do more work, because selectors now depend on what is inside elements and how that changes.
&lt;/p&gt;

&lt;p&gt;
My take after profiling a few real pages: do not go wild with global &lt;code&gt;*:has(...)&lt;/code&gt; selectors. Scope them. Prefer direct children or close relationships.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/* Bad idea */
*:has(.error) { ... }

/* Reasonable */
.form:has(.field__error) { ... }

/* Even better */
.form:has(.field &amp;gt; .field__error) { ... }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Support is good now. Chrome, Edge, Safari, Firefox all ship &lt;code&gt;:has()&lt;/code&gt;. Old Safari versions are the main risk. If you work on something critical for a weird enterprise fleet, check caniuse and add progressive enhancement.
&lt;/p&gt;

&lt;p&gt;
Most of my patterns above fail gracefully. You lose a highlight or a layout tweak, not core functionality. That is a good bar to aim for.
&lt;/p&gt;

&lt;h2&gt;How I think about :has() now&lt;/h2&gt;

&lt;p&gt;
I used to reach for JavaScript whenever a parent needed to know about a child, or a sibling needed to react to state. That felt normal. It also created a lot of glue code that did not age well.
&lt;/p&gt;

&lt;p&gt;
Now my filter is simple:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Is the state already visible in the DOM? (attribute, pseudo class, anchor, etc.)&lt;/li&gt;
  &lt;li&gt;Can that state reasonably drive styling only?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
If the answer is yes, I try &lt;code&gt;:has()&lt;/code&gt; first. JS comes later, if at all.
&lt;/p&gt;

&lt;p&gt;
Five years ago I was writing tab managers and form validators by hand. I would not go back. &lt;code&gt;:has()&lt;/code&gt; is the layout trick that finally lets CSS act on the structure we already have, instead of the utility classes we wish we had planned better.
&lt;/p&gt;

&lt;p&gt;
If you have a component that keeps growing event listeners and state flags, look at the HTML for five minutes. There is a decent chance &lt;code&gt;:has()&lt;/code&gt; can take some of that weight off.
&lt;/p&gt;

</description>
    </item>
    <item>
      <title>The Real Cost Of Self‑Hosting Ghost On DigitalOcean In 2026</title>
      <dc:creator>Richard Lemon</dc:creator>
      <pubDate>Fri, 22 May 2026 05:00:00 +0000</pubDate>
      <link>https://dev.to/richardlemon/the-real-cost-of-self-hosting-ghost-on-digitalocean-in-2026-18dh</link>
      <guid>https://dev.to/richardlemon/the-real-cost-of-self-hosting-ghost-on-digitalocean-in-2026-18dh</guid>
      <description>&lt;h1&gt;The Real Cost Of Self‑Hosting Ghost On DigitalOcean In 2026&lt;/h1&gt;

&lt;p&gt;I like owning my stack. I also like not waking up to surprise invoices.&lt;/p&gt;

&lt;p&gt;So for my own Ghost installs I stopped guessing and ran the actual 2026 numbers for self‑hosting on DigitalOcean, then compared them against Ghost Pro. Below is the messy, honest version. Dollars, not vibes.&lt;/p&gt;

&lt;h2&gt;Quick context: what I actually run&lt;/h2&gt;

&lt;p&gt;I am not running some theoretical benchmark blog that gets 3 hits a month.&lt;/p&gt;

&lt;p&gt;The setup I am talking about here:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Ghost 5.x, Node 18, Ubuntu 22.04&lt;/li&gt;
  &lt;li&gt;One production blog, one staging instance on the same box&lt;/li&gt;
  &lt;li&gt;~25k pageviews / month&lt;/li&gt;
  &lt;li&gt;Light membership use, no paid newsletter yet&lt;/li&gt;
  &lt;li&gt;Image-heavy posts, but nothing crazy like a photography portfolio&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Traffic is modest but not toy-level. Good enough to stress a $6 droplet and make bandwidth and backups real numbers.&lt;/p&gt;

&lt;h2&gt;DigitalOcean pricing reality in 2026&lt;/h2&gt;

&lt;p&gt;DigitalOcean did their usual small bumps. In early 2026, the realistic entry point for a Ghost blog that you do not hate maintaining looks like this.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Droplet&lt;/strong&gt;: 1 vCPU, 2GB RAM, 50GB SSD — &lt;strong&gt;$12 / month&lt;/strong&gt;
&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Automatic backups&lt;/strong&gt; (20% of droplet price) — &lt;strong&gt;~$2.40 / month&lt;/strong&gt;
&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Spaces object storage&lt;/strong&gt; (optional but smart) — &lt;strong&gt;$5 / month&lt;/strong&gt; for 250GB + 1TB transfer&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Domain&lt;/strong&gt; — &lt;strong&gt;$10–$15 / year&lt;/strong&gt; (I will call it $1 / month)&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Transactional email&lt;/strong&gt; (Mailgun, Postmark, SES) — &lt;strong&gt;$10 / month&lt;/strong&gt; tier is the practical floor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are retail prices, not affiliate-optimistic numbers. You can shave a few dollars if you are aggressive. You can also blow past them with zero effort.&lt;/p&gt;

&lt;h2&gt;Baseline self‑host setup: what you actually pay&lt;/h2&gt;

&lt;p&gt;Let me walk through the stack I recommend and what it costs month to month.&lt;/p&gt;

&lt;h3&gt;Droplet: how small can you go?&lt;/h3&gt;

&lt;p&gt;You &lt;em&gt;can&lt;/em&gt; run Ghost on a 1GB droplet. I did this for a while. It was fine until it was not.&lt;/p&gt;

&lt;p&gt;Anything beyond a few thousand pageviews or one heavy Node task and you suddenly meet your old friend: swapping and 5+ second response times.&lt;/p&gt;

&lt;p&gt;So I treat 2GB as the actual minimum in 2026.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;DigitalOcean Basic droplet 1 vCPU / 2GB / 50GB SSDPrice: &lt;strong&gt;$12 / month&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This comfortably runs:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Nginx (reverse proxy)&lt;/li&gt;
  &lt;li&gt;One Ghost instance, plus a small staging instance&lt;/li&gt;
  &lt;li&gt;Certbot for SSL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you expect real traffic spikes, just skip straight to 2 vCPU / 4GB at $24 / month. Anything above that and you are deep into 
“I should probably just pay Ghost Pro” territory.&lt;/p&gt;

&lt;h3&gt;Backups: the boring expensive part&lt;/h3&gt;

&lt;p&gt;There are two backup concerns:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Server-level: DigitalOcean automatic droplet backups&lt;/li&gt;
  &lt;li&gt;App-level: Ghost content exports, database dumps, theme repo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I pay for DigitalOcean backups because I like sleeping.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Droplet: $12&lt;/li&gt;
  &lt;li&gt;Automatic backups: 20% of droplet price = &lt;strong&gt;$2.40&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total so far: &lt;strong&gt;$14.40 / month&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On top of that I push database dumps to a private GitHub repo and occasionally export Ghost JSON manually. That is basically free, just time.&lt;/p&gt;

&lt;h3&gt;Storage: local disk vs Spaces&lt;/h3&gt;

&lt;p&gt;Ghost stores uploaded images on disk by default. On a 50GB droplet this is fine for a while. Then you hit 60% disk usage and start cleaning your own mess.&lt;/p&gt;

&lt;p&gt;I think DO Spaces is the saner option.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;DigitalOcean Spaces: 250GB storage + 1TB bandwidthPrice: &lt;strong&gt;$5 / month&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Host images and media there. Front Ghost through a CDN endpoint. You keep the droplet disk clean and predictable.&lt;/p&gt;

&lt;p&gt;You can skip Spaces initially, but I would not architect around permanent local disk storage unless your blog is text-only.&lt;/p&gt;

&lt;h3&gt;Bandwidth: the thing you forget until you pay&lt;/h3&gt;

&lt;p&gt;DigitalOcean gives you bundled outbound traffic with each droplet. For the 2GB plan that is usually around 2–3TB. My Ghost blog with 25k monthly pageviews sits safely under 100GB outbound per month.&lt;/p&gt;

&lt;p&gt;So realistically:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Outbound traffic from droplet&lt;/strong&gt;: effectively included, &lt;strong&gt;$0&lt;/strong&gt; unless you go viral&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Spaces bandwidth&lt;/strong&gt;: first 1TB included in that $5&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you spike to Hacker News front page levels, DO overage is around $0.01–$0.02 per GB. That is still cheaper than the stress of an AWS bill. Just know that “unlimited” is not a thing here.&lt;/p&gt;

&lt;h3&gt;Transactional email: the hidden required line item&lt;/h3&gt;

&lt;p&gt;Ghost needs email for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Signup and login magic links&lt;/li&gt;
  &lt;li&gt;Member notifications&lt;/li&gt;
  &lt;li&gt;Newsletter sending&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your droplet will not deliver email reliably. You need a provider.&lt;/p&gt;

&lt;p&gt;The realistic minimal plans in 2026:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Mailgun Foundation: $15 / month for 50k emails&lt;/li&gt;
  &lt;li&gt;Postmark: $15 / month for 10k emails&lt;/li&gt;
  &lt;li&gt;AWS SES: cheap, but you trade cost for AWS overhead and deliverability tuning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you only send a small member newsletter, SES can keep this under $5, but you will pay with your time. I put a line item of &lt;strong&gt;$10 / month&lt;/strong&gt; here as a middle ground.&lt;/p&gt;

&lt;h3&gt;Domain and DNS&lt;/h3&gt;

&lt;p&gt;Domain names are boring, but they count.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;.com domain: $10–15 / yearAmortised: &lt;strong&gt;$1 / month&lt;/strong&gt;
&lt;/li&gt;
  &lt;li&gt;DNS: Cloudflare free tier is fine for most Ghost blogs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So domain plus DNS is basically $1.&lt;/p&gt;

&lt;h2&gt;Self‑hosting total: realistic monthly cost&lt;/h2&gt;

&lt;p&gt;Let me put that together for a single production Ghost blog, plus staging, on DigitalOcean in 2026:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Droplet 2GB: &lt;strong&gt;$12.00&lt;/strong&gt;
&lt;/li&gt;
  &lt;li&gt;Droplet backups: &lt;strong&gt;$2.40&lt;/strong&gt;
&lt;/li&gt;
  &lt;li&gt;Spaces (media + 1TB transfer): &lt;strong&gt;$5.00&lt;/strong&gt;
&lt;/li&gt;
  &lt;li&gt;Transactional email (rough average): &lt;strong&gt;$10.00&lt;/strong&gt;
&lt;/li&gt;
  &lt;li&gt;Domain: &lt;strong&gt;$1.00&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Total monthly cash cost: $30.40&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Call it &lt;strong&gt;$30–35 / month&lt;/strong&gt; for a robust, no-drama Ghost setup for a dev who knows their way around a terminal.&lt;/p&gt;

&lt;p&gt;You can get it under $20 if you:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Drop DO backups and roll your own&lt;/li&gt;
  &lt;li&gt;Skip Spaces and keep media on local disk&lt;/li&gt;
  &lt;li&gt;Use AWS SES exclusively and stay under their free/cheap thresholds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are that person, you probably know your risk tolerance already.&lt;/p&gt;

&lt;h2&gt;What Ghost Pro actually costs in 2026&lt;/h2&gt;

&lt;p&gt;Ghost Pro pricing moves, but the structure is steady.&lt;/p&gt;

&lt;p&gt;At the time of writing, realistic tiers look roughly like this:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Starter&lt;/strong&gt;: around &lt;strong&gt;$9–11 / month&lt;/strong&gt; billed yearlyOne site, limited members, shared infrastructure&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Creator&lt;/strong&gt;: around &lt;strong&gt;$25–30 / month&lt;/strong&gt; billed yearlyHigher member limits, better performance&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Team / Business&lt;/strong&gt;: &lt;strong&gt;$50+ / month&lt;/strong&gt; for serious membership and publication use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prices vary slightly with annual vs monthly billing and how many members you have, but this is close enough for a decision.&lt;/p&gt;

&lt;p&gt;Important bit: Ghost Pro includes a bunch of stuff you had separate line items for on DigitalOcean:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Hosting and scaling&lt;/li&gt;
  &lt;li&gt;Automatic updates and security patches&lt;/li&gt;
  &lt;li&gt;Backups&lt;/li&gt;
  &lt;li&gt;Email infrastructure for newsletters&lt;/li&gt;
  &lt;li&gt;CDN for assets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You still pay for your domain name. Everything else is bundled.&lt;/p&gt;

&lt;h2&gt;Self‑hosting vs Ghost Pro: the raw comparison&lt;/h2&gt;

&lt;p&gt;If you strip away features and ideology, the question is simple. For a typical developer blog, is it cheaper to self‑host Ghost or pay Ghost Pro?&lt;/p&gt;

&lt;h3&gt;Scenario 1: small personal blog&lt;/h3&gt;

&lt;p&gt;You write a few posts a month. 5k pageviews. Maybe a small free member list.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Ghost Pro Starter&lt;/strong&gt;: about $9–11 / month (plus $1 for domain)&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Self‑host on DO (minimum)&lt;/strong&gt;:&lt;/li&gt;
  &lt;ul&gt;
    &lt;li&gt;1GB droplet: $6&lt;/li&gt;
    &lt;li&gt;No DO backups: $0&lt;/li&gt;
    &lt;li&gt;No Spaces: $0&lt;/li&gt;
    &lt;li&gt;SES for transactional + small newsletter: $2–3&lt;/li&gt;
    &lt;li&gt;Domain: $1&lt;/li&gt;
  &lt;/ul&gt;
&lt;/ul&gt;

&lt;p&gt;Best case self‑host cash cost: &lt;strong&gt;$9–10 / month&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So the money is roughly the same. Ghost Pro starts to look very reasonable unless you just like running servers.&lt;/p&gt;

&lt;h3&gt;Scenario 2: serious blog with modest traffic&lt;/h3&gt;

&lt;p&gt;This is closer to my setup. 20k–50k pageviews, some members, regular newsletters.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Ghost Pro Creator&lt;/strong&gt;: about $25–30 / month (plus $1 domain)&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Self‑host realistic&lt;/strong&gt;:&lt;/li&gt;
  &lt;ul&gt;
    &lt;li&gt;2GB droplet: $12&lt;/li&gt;
    &lt;li&gt;DO backups: $2.40&lt;/li&gt;
    &lt;li&gt;Spaces: $5&lt;/li&gt;
    &lt;li&gt;Mailgun/Postmark/SES combo: $10&lt;/li&gt;
    &lt;li&gt;Domain: $1&lt;/li&gt;
  &lt;/ul&gt;
&lt;/ul&gt;

&lt;p&gt;Self‑host cash cost: &lt;strong&gt;~$30–35 / month&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Now self‑hosting is actually &lt;em&gt;more&lt;/em&gt; expensive in pure dollars than Ghost Pro Creator, if you configure it like a responsible adult.&lt;/p&gt;

&lt;h3&gt;Scenario 3: membership business&lt;/h3&gt;

&lt;p&gt;If you are running a paid publication and making actual money, the pricing conversation changes.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Ghost Pro Business&lt;/strong&gt;: $50–100+ / month, depending on member count&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Self‑host&lt;/strong&gt;:&lt;/li&gt;
  &lt;ul&gt;
    &lt;li&gt;2–4GB droplet: $24–48&lt;/li&gt;
    &lt;li&gt;Backups: $5–10&lt;/li&gt;
    &lt;li&gt;Spaces: $5–10&lt;/li&gt;
    &lt;li&gt;Email: $10–30 (volume)&lt;/li&gt;
    &lt;li&gt;Monitoring / extras: $5–10&lt;/li&gt;
  &lt;/ul&gt;
&lt;/ul&gt;

&lt;p&gt;You can probably keep this around &lt;strong&gt;$50–80 / month&lt;/strong&gt; if you know what you are doing. So you might save a little compared to the higher Ghost Pro tiers.&lt;/p&gt;

&lt;p&gt;But at this point you have a business. Personally, I would not want my subscription revenue running on a stack where I am the single point of failure, unless infrastructure is my hobby.&lt;/p&gt;

&lt;h2&gt;The cost nobody lists: your time&lt;/h2&gt;

&lt;p&gt;Money is the easy comparison. Time is ugly to quantify and more important.&lt;/p&gt;

&lt;p&gt;Here is my rough time log for a self‑hosted Ghost blog over a year:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Initial setup and config: 2–4 hoursDigitalOcean droplet, Nginx, SSL, Ghost install, mail provider, DNS&lt;/li&gt;
  &lt;li&gt;Updates and maintenance: 1 hour / monthGhost upgrades, OS updates, certificate renewals, package updates&lt;/li&gt;
  &lt;li&gt;Random incidents: 2–5 hours / yearDisk nearly full, email DNS issue, unexpected 500s, one bad migration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total: roughly &lt;strong&gt;14–20 hours / year&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you value your time at $50 / hour (which is low for many devs), that is &lt;strong&gt;$700–1000 / year&lt;/strong&gt; of time cost. Spread across 12 months and you just silently added $60–80 to your “cheap” $30 / month self‑hosting bill.&lt;/p&gt;

&lt;p&gt;On Ghost Pro my time log is basically:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Initial theme setup: 1–2 hours&lt;/li&gt;
  &lt;li&gt;Zero hours per month on infra&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the real gap. Ghost Pro makes infrastructure disappear. Self‑hosting gives you toys to play with and things to fix.&lt;/p&gt;

&lt;h2&gt;Why I still self‑host (sometimes)&lt;/h2&gt;

&lt;p&gt;Given all that, why bother?&lt;/p&gt;

&lt;p&gt;For me there are three reasons:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Full control&lt;/strong&gt;. I want to tinker with Nginx configs, custom logging, weird A/B testing scripts, or external services at the server level.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Learning value&lt;/strong&gt;. I like using my own sites as a lab. Self‑hosting is part of that.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Multiple projects&lt;/strong&gt;. If I stack several side projects on the same DO account, the per‑site cost drops a lot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I only had &lt;em&gt;one&lt;/em&gt; serious blog, and I wanted it to just work, I would pick Ghost Pro without hesitation. The numbers are simply better once you include time and all the extra services.&lt;/p&gt;

&lt;h2&gt;When self‑hosting actually makes sense financially&lt;/h2&gt;

&lt;p&gt;There is one situation where self‑hosting wins hard.&lt;/p&gt;

&lt;p&gt;If you already pay for DigitalOcean or another VPS for other projects, and you are comfortable running multiple apps on one machine, then the marginal cost of adding Ghost is tiny.&lt;/p&gt;

&lt;p&gt;Example from my actual setup:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;One 2 vCPU / 4GB droplet at $24&lt;/li&gt;
  &lt;li&gt;Running: a small API, a side project dashboard, and my Ghost blog&lt;/li&gt;
  &lt;li&gt;Backups: ~$5&lt;/li&gt;
  &lt;li&gt;Spaces: $5 shared across projects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Spread across three projects, the &lt;em&gt;per‑project&lt;/em&gt; cost is around $12–15 / month. That beats Ghost Pro easily.&lt;/p&gt;

&lt;p&gt;You still pay with time, but now the cost per site is more justifiable.&lt;/p&gt;

&lt;h2&gt;Practical recommendation&lt;/h2&gt;

&lt;p&gt;If you are a developer thinking about Ghost, here is my blunt take based on the numbers above.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;If you want &lt;strong&gt;one blog and no hassle&lt;/strong&gt;, use Ghost Pro Starter or Creator. Your total cost will land around $10–30 / month and your time cost is effectively zero.&lt;/li&gt;
  &lt;li&gt;If you want &lt;strong&gt;to learn infra or already live in DO/AWS land&lt;/strong&gt;, then self‑hosting is fun and defensible. Expect to pay $20–35 / month plus a few hours a quarter.&lt;/li&gt;
  &lt;li&gt;If you are building &lt;strong&gt;a paid publication&lt;/strong&gt; and you are not a backend engineer, I think self‑hosting is a bad business decision. Pay Ghost. Focus on content and growth, not SSL configs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The myth that self‑hosting Ghost is “basically five bucks” was maybe true for a toy blog on a tiny droplet with no backups in 2018.&lt;/p&gt;

&lt;p&gt;In 2026, if you want something stable and production‑grade on DigitalOcean, the &lt;strong&gt;real&lt;/strong&gt; monthly cost looks a lot like Ghost Pro. Sometimes higher. The only real question is whether you would rather spend your budget on servers or on your own time.&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>devops</category>
      <category>node</category>
      <category>opensource</category>
    </item>
    <item>
      <title>My Full Make.com Blueprint For Automating a Sheets OpenAI Ghost Content Pipeline</title>
      <dc:creator>Richard Lemon</dc:creator>
      <pubDate>Thu, 21 May 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/richardlemon/my-full-makecom-blueprint-for-automating-a-sheets-openai-ghost-content-pipeline-h7d</link>
      <guid>https://dev.to/richardlemon/my-full-makecom-blueprint-for-automating-a-sheets-openai-ghost-content-pipeline-h7d</guid>
      <description>&lt;h2&gt;Why I stopped hand-assembling content&lt;/h2&gt;

&lt;p&gt;I got tired of copy pasting my way through content.&lt;/p&gt;

&lt;p&gt;Idea in Notion. Outline in Docs. Draft in ChatGPT. Formatting in Ghost. Images in Figma. Every post felt like a manual QA job instead of building something once and letting it run.&lt;/p&gt;

&lt;p&gt;So I pushed the whole thing into Make.com.&lt;/p&gt;

&lt;p&gt;Now a single row in Google Sheets turns into a drafted or published Ghost post. Titles, meta fields, tags, even a first-pass excerpt. All wired together with OpenAI in the middle.&lt;/p&gt;

&lt;p&gt;This is the exact blueprint. Not theory. This is the scenario that runs my real content pipeline.&lt;/p&gt;

&lt;h2&gt;The high-level architecture&lt;/h2&gt;

&lt;p&gt;The stack is simple on purpose:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Google Sheets&lt;/strong&gt; as the content control panel.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Make.com&lt;/strong&gt; to orchestrate everything.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;OpenAI&lt;/strong&gt; to generate and clean up text.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Ghost&lt;/strong&gt; (self-hosted) as the CMS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One sheet row = one post. Status and flags in that row decide if Make should ignore it, draft it, or publish it.&lt;/p&gt;

&lt;p&gt;I keep the Make scenario focused. One job. Turn structured data in Sheets into structured content in Ghost.&lt;/p&gt;

&lt;h2&gt;The Google Sheet schema&lt;/h2&gt;

&lt;p&gt;Here is the actual column setup from my sheet. No fluff.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;code&gt;A: id&lt;/code&gt; – internal ID, just an incrementing number.&lt;/li&gt;
  &lt;li&gt;
&lt;code&gt;B: status&lt;/code&gt; – &lt;code&gt;idea&lt;/code&gt;, &lt;code&gt;ready&lt;/code&gt;, &lt;code&gt;generated&lt;/code&gt;, &lt;code&gt;published&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;
&lt;code&gt;C: title&lt;/code&gt; – human-written title seed.&lt;/li&gt;
  &lt;li&gt;
&lt;code&gt;D: slug&lt;/code&gt; – usually auto-generated in Make if empty.&lt;/li&gt;
  &lt;li&gt;
&lt;code&gt;E: brief&lt;/code&gt; – 2–4 bullet outline or a short paragraph.&lt;/li&gt;
  &lt;li&gt;
&lt;code&gt;F: target_keywords&lt;/code&gt; – comma separated SEO terms.&lt;/li&gt;
  &lt;li&gt;
&lt;code&gt;G: tone&lt;/code&gt; – e.g. &lt;code&gt;direct, informal, first-person&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;
&lt;code&gt;H: word_count_goal&lt;/code&gt; – rough length like &lt;code&gt;1200&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;
&lt;code&gt;I: tags&lt;/code&gt; – comma separated Ghost tags.&lt;/li&gt;
  &lt;li&gt;
&lt;code&gt;J: generated_html&lt;/code&gt; – where I store the body HTML from OpenAI.&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;K: meta_title&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;L: meta_description&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;M: excerpt&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;
&lt;code&gt;N: publish_flag&lt;/code&gt; – &lt;code&gt;draft&lt;/code&gt; or &lt;code&gt;publish&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;
&lt;code&gt;O: ghost_post_id&lt;/code&gt; – for updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I do not trust LLMs to guess intent. So I give them a tight brief and flags.&lt;/p&gt;

&lt;h2&gt;The Make.com scenario: the main flow&lt;/h2&gt;

&lt;p&gt;I run a single Make scenario that does all of this:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Polls Google Sheets for new &lt;code&gt;ready&lt;/code&gt; rows.&lt;/li&gt;
  &lt;li&gt;Generates content and meta fields via OpenAI.&lt;/li&gt;
  &lt;li&gt;Writes the generated content back into the sheet.&lt;/li&gt;
  &lt;li&gt;Creates or updates a Ghost post.&lt;/li&gt;
  &lt;li&gt;Updates status and IDs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I will walk through every module and what it actually does. No hand waving.&lt;/p&gt;

&lt;h2&gt;Module 1: Google Sheets “Search Rows”&lt;/h2&gt;

&lt;p&gt;Trigger: I run this scenario on a schedule. Every 15 minutes works for my pace.&lt;/p&gt;

&lt;p&gt;First module is a &lt;strong&gt;Google Sheets &amp;gt; Search Rows&lt;/strong&gt; module.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Spreadsheet&lt;/strong&gt;: my &lt;code&gt;Content Pipeline&lt;/code&gt; sheet.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Sheet&lt;/strong&gt;: &lt;code&gt;Posts&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Filter&lt;/strong&gt;: &lt;code&gt;status = ready&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Limit&lt;/strong&gt;: I cap at 3 rows per run. I prefer small batches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This returns an array of rows that are ready to be turned into drafts.&lt;/p&gt;

&lt;p&gt;Each row carries all the columns I mentioned earlier.&lt;/p&gt;

&lt;h2&gt;Module 2: Iterator&lt;/h2&gt;

&lt;p&gt;I drop an &lt;strong&gt;Iterator&lt;/strong&gt; right after Search Rows.&lt;/p&gt;

&lt;p&gt;No magic here. It splits the list of rows into separate bundles so I can process each post independently.&lt;/p&gt;

&lt;p&gt;The iterator just takes the array of rows from Sheets and iterates over &lt;code&gt;values[]&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;Module 3: Data cleanup with Text &amp;amp; Tools&lt;/h2&gt;

&lt;p&gt;I do some pre-processing before I hit OpenAI.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Trim whitespace from things like &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;slug&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Normalize commas in &lt;code&gt;tags&lt;/code&gt; and &lt;code&gt;target_keywords&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Generate a fallback slug if the cell is empty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice this is a couple of &lt;strong&gt;Tools &amp;gt; Text aggregator / Text parser&lt;/strong&gt; modules and one &lt;strong&gt;Set variable&lt;/strong&gt; module.&lt;/p&gt;

&lt;p&gt;Example: to generate the slug I use a simple Make formula in a &lt;strong&gt;Set variable&lt;/strong&gt; module:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;lowercase(
  replace(
    replace(
      replace(
        stripDiacritics(title);
        " "; "-"
      );
      "--"; "-"
    );
    "[^a-z0-9-]"; ""
  )
)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If the slug column in Sheets is already filled, I keep it. Otherwise this kicks in.&lt;/p&gt;

&lt;h2&gt;Module 4: OpenAI “Create a chat completion” for the article body&lt;/h2&gt;

&lt;p&gt;Now the fun part.&lt;/p&gt;

&lt;p&gt;I use the &lt;strong&gt;OpenAI &amp;gt; Create a chat completion&lt;/strong&gt; module. Currently on &lt;code&gt;gpt-4.1&lt;/code&gt; but I keep this modular so I can swap models.&lt;/p&gt;

&lt;p&gt;I feed it a structured prompt that merges sheet data and my own style instructions. Here is the actual system + user content simplified to fit here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;System message&lt;/strong&gt; (in the module):&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;You are Richard Lemon, a creative web experience developer.
Write opinionated first-person blog posts.
No em dashes. Short paragraphs (max 2-3 lines).
Direct voice, practical, specific.
Use HTML with &amp;lt;h2&amp;gt;, &amp;lt;p&amp;gt;, &amp;lt;ul&amp;gt;, &amp;lt;li&amp;gt;, &amp;lt;pre&amp;gt;.
Do not add &amp;lt;html&amp;gt; or &amp;lt;body&amp;gt; tags.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;User message&lt;/strong&gt; template:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Write a blog post with the following constraints:

Title: {{title}}
Brief: {{brief}}
Target keywords: {{target_keywords}}
Tone: {{tone}}
Word count target: {{word_count_goal}}

Rules:
- Write in first person as if I actually did the thing.
- Be concrete and technical where useful.
- Use only HTML tags for structure (&amp;lt;h2&amp;gt;, &amp;lt;p&amp;gt;, &amp;lt;ul&amp;gt;, &amp;lt;li&amp;gt;, &amp;lt;pre&amp;gt;, &amp;lt;code&amp;gt;).
- No introduction fluff, start with context or action.
- No conclusion heading.

Return only the HTML body.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The output is &lt;strong&gt;pure HTML&lt;/strong&gt;. I do not want Markdown here because Ghost accepts HTML just fine and I like having precise control.&lt;/p&gt;

&lt;h2&gt;Module 5: OpenAI for SEO meta + excerpt&lt;/h2&gt;

&lt;p&gt;I split generation into two calls instead of asking one model response to do everything.&lt;/p&gt;

&lt;p&gt;Second &lt;strong&gt;OpenAI &amp;gt; Create a chat completion&lt;/strong&gt; module takes the title, target keywords, and a short summary of the generated HTML and creates meta fields plus an excerpt.&lt;/p&gt;

&lt;p&gt;System message:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;You create SEO metadata for blog posts.
Always stay under the specified character limits.
Return JSON only.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;User message:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Post title: {{title}}
Target keywords: {{target_keywords}}
Body HTML (truncated): {{body_preview}}

Generate:
- meta_title: max 60 chars, must include at least one keyword.
- meta_description: 120-155 chars, natural and specific.
- excerpt: 1-2 sentences, no clickbait.

Return JSON like this:
{
  "meta_title": "...",
  "meta_description": "...",
  "excerpt": "..."
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I then parse that JSON with a &lt;strong&gt;JSON &amp;gt; Parse JSON&lt;/strong&gt; module.&lt;/p&gt;

&lt;p&gt;Could I compress this into one call? Sure. I tried that. The meta fields got worse and harder to constrain. So I split it.&lt;/p&gt;

&lt;h2&gt;Module 6: Write everything back to Google Sheets&lt;/h2&gt;

&lt;p&gt;At this point I have:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Cleaned title and slug.&lt;/li&gt;
  &lt;li&gt;Generated HTML body.&lt;/li&gt;
  &lt;li&gt;Meta title, description, and excerpt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before touching Ghost I push this back into the sheet.&lt;/p&gt;

&lt;p&gt;I use &lt;strong&gt;Google Sheets &amp;gt; Update a Row&lt;/strong&gt; with:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Column J&lt;/strong&gt; → &lt;code&gt;generated_html&lt;/code&gt; from OpenAI.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Column K&lt;/strong&gt; → &lt;code&gt;meta_title&lt;/code&gt; from JSON.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Column L&lt;/strong&gt; → &lt;code&gt;meta_description&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Column M&lt;/strong&gt; → &lt;code&gt;excerpt&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Column D&lt;/strong&gt; → final slug.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Column B&lt;/strong&gt; → set to &lt;code&gt;generated&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why write back before publishing?&lt;/p&gt;

&lt;p&gt;Because I like a manual checkpoint. I can read the generated HTML in Sheets, tweak the brief or title, flip status back to &lt;code&gt;ready&lt;/code&gt; if I want a regen.&lt;/p&gt;

&lt;p&gt;This effectively makes Sheets my content staging area while Ghost stays clean.&lt;/p&gt;

&lt;h2&gt;Module 7: Filter on publish_flag&lt;/h2&gt;

&lt;p&gt;Not every generated post should go live.&lt;/p&gt;

&lt;p&gt;I add a &lt;strong&gt;filter&lt;/strong&gt; between the Sheets update and the Ghost modules:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code&gt;publish_flag = "draft" OR publish_flag = "publish"&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If &lt;code&gt;publish_flag&lt;/code&gt; is empty or something else, the scenario stops for that row. It will still update the generated fields, but nothing touches Ghost.&lt;/p&gt;

&lt;p&gt;When I want a post to move to Ghost I set &lt;code&gt;publish_flag&lt;/code&gt; to &lt;code&gt;draft&lt;/code&gt; or &lt;code&gt;publish&lt;/code&gt; in the sheet.&lt;/p&gt;

&lt;h2&gt;Module 8: Router for create vs update in Ghost&lt;/h2&gt;

&lt;p&gt;I use Ghost both for new posts and edits triggered from Sheets.&lt;/p&gt;

&lt;p&gt;So I drop a &lt;strong&gt;Router&lt;/strong&gt; with two branches:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Branch 1: Create&lt;/strong&gt; when &lt;code&gt;ghost_post_id&lt;/code&gt; is empty.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Branch 2: Update&lt;/strong&gt; when &lt;code&gt;ghost_post_id&lt;/code&gt; is not empty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This lets me tweak a title or tags in Sheets, set &lt;code&gt;publish_flag&lt;/code&gt; again, and push an update to the existing Ghost post.&lt;/p&gt;

&lt;h2&gt;Module 9: Ghost “Create a post” (branch 1)&lt;/h2&gt;

&lt;p&gt;On the create branch I use the &lt;strong&gt;Ghost &amp;gt; Create a post&lt;/strong&gt; module.&lt;/p&gt;

&lt;p&gt;Here is how I map things:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Title&lt;/strong&gt; → Sheet &lt;code&gt;title&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Slug&lt;/strong&gt; → final slug from Module 3.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;HTML&lt;/strong&gt; → &lt;code&gt;generated_html&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Excerpt&lt;/strong&gt; → AI generated excerpt.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Meta title&lt;/strong&gt; → meta_title.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Meta description&lt;/strong&gt; → meta_description.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Tags&lt;/strong&gt; → split tags column on comma, trim each value.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For &lt;strong&gt;status&lt;/strong&gt; I map directly from &lt;code&gt;publish_flag&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;code&gt;draft&lt;/code&gt; → Ghost draft.&lt;/li&gt;
  &lt;li&gt;
&lt;code&gt;publish&lt;/code&gt; → Ghost published.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ghost returns a post object with an &lt;code&gt;id&lt;/code&gt;. I capture that and write it back to the sheet.&lt;/p&gt;

&lt;h2&gt;Module 10: Google Sheets “Update a Row” with ghost_post_id&lt;/h2&gt;

&lt;p&gt;Immediately after the create call I drop another &lt;strong&gt;Google Sheets &amp;gt; Update a Row&lt;/strong&gt; module.&lt;/p&gt;

&lt;p&gt;It takes the &lt;code&gt;id&lt;/code&gt; from Ghost and stores it in column &lt;code&gt;O: ghost_post_id&lt;/code&gt;. I also set:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;code&gt;status = published&lt;/code&gt; if publish_flag was &lt;code&gt;publish&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;
&lt;code&gt;status = generated&lt;/code&gt; if publish_flag was &lt;code&gt;draft&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps my spreadsheet the single source of truth. I can filter ideas, generated drafts, and published content without logging into Ghost.&lt;/p&gt;

&lt;h2&gt;Module 11: Ghost “Update a post” (branch 2)&lt;/h2&gt;

&lt;p&gt;On the update branch, I use &lt;strong&gt;Ghost &amp;gt; Update a post&lt;/strong&gt; with the stored &lt;code&gt;ghost_post_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I do not blindly overwrite everything. Here is my mapping logic:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Always update &lt;strong&gt;HTML&lt;/strong&gt; from &lt;code&gt;generated_html&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Update &lt;strong&gt;title&lt;/strong&gt; and &lt;strong&gt;slug&lt;/strong&gt; only if the sheet values changed.&lt;/li&gt;
  &lt;li&gt;Update &lt;strong&gt;meta fields&lt;/strong&gt; if they exist in Sheets. Otherwise keep Ghost values.&lt;/li&gt;
  &lt;li&gt;Tags are rebuilt from the &lt;code&gt;tags&lt;/code&gt; column every time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Make that means I use &lt;strong&gt;if&lt;/strong&gt; expressions in the field mapping. For example:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;if(
  empty(sheet_meta_title);
  ghost.meta_title;
  sheet_meta_title
)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I apply a similar pattern for slug and description.&lt;/p&gt;

&lt;h2&gt;Module 12: Error handling and logging&lt;/h2&gt;

&lt;p&gt;I do not like silent failures.&lt;/p&gt;

&lt;p&gt;So I added a simple error path using Make’s built-in error handlers:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;If Ghost or OpenAI throws an error, I catch it in a separate route.&lt;/li&gt;
  &lt;li&gt;Then I write the error message into a &lt;code&gt;errors&lt;/code&gt; column in Sheets.&lt;/li&gt;
  &lt;li&gt;I also ping myself with a &lt;strong&gt;Gmail &amp;gt; Send an email&lt;/strong&gt; module when something breaks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing fancy. Enough to see when the pipeline has issues without watching Make all day.&lt;/p&gt;

&lt;h2&gt;Why I structured it this way&lt;/h2&gt;

&lt;p&gt;I tried a few alternative versions before landing on this flow.&lt;/p&gt;

&lt;p&gt;What I did &lt;strong&gt;not&lt;/strong&gt; like:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Single gigantic OpenAI prompt that tried to output HTML, meta, title variants, tag suggestions, everything. It was brittle.&lt;/li&gt;
  &lt;li&gt;Direct Sheets to Ghost without writing generated content back first. Harder to see what the model produced.&lt;/li&gt;
  &lt;li&gt;No &lt;code&gt;publish_flag&lt;/code&gt;, so everything went straight to draft in Ghost. That got noisy fast.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What works better for me now:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Sheets is the control tower. All fields visible. Easy to override.&lt;/li&gt;
  &lt;li&gt;Make is the transport layer. No business logic hidden in Ghost.&lt;/li&gt;
  &lt;li&gt;OpenAI is a dumb transformer that takes structured inputs and outputs structured HTML and metadata.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The end result is that I spend time on outlines and ideas, not formatting posts or fighting CMS UI.&lt;/p&gt;

&lt;h2&gt;How this feels in daily use&lt;/h2&gt;

&lt;p&gt;Here is my actual workflow on a random Tuesday.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;I open the &lt;code&gt;Posts&lt;/code&gt; sheet and dump three new ideas as rows with rough titles and briefs.&lt;/li&gt;
  &lt;li&gt;I set &lt;code&gt;status = ready&lt;/code&gt; and leave &lt;code&gt;publish_flag&lt;/code&gt; empty.&lt;/li&gt;
  &lt;li&gt;Make picks them up, runs OpenAI, writes back generated HTML and meta, sets &lt;code&gt;status = generated&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;I skim the HTML in Sheets. If a post feels off, I tweak the brief or title, flip status back to &lt;code&gt;ready&lt;/code&gt; and let it regenerate.&lt;/li&gt;
  &lt;li&gt;Once I like a post, I set &lt;code&gt;publish_flag = draft&lt;/code&gt; or &lt;code&gt;publish&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Next run, Make pushes it into Ghost accordingly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I still edit inside Ghost sometimes. Especially for code-heavy posts or things where I want to insert images manually.&lt;/p&gt;

&lt;p&gt;But 80 percent of the grunt work is automated. Which is the point.&lt;/p&gt;

&lt;h2&gt;If you want to copy this&lt;/h2&gt;

&lt;p&gt;If you are already using Make, you can rebuild this in an afternoon.&lt;/p&gt;

&lt;p&gt;The three non-negotiables in my experience:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Use Sheets as a real schema, not a random dumping ground.&lt;/li&gt;
  &lt;li&gt;Keep OpenAI prompts tight and specific. One job per call.&lt;/li&gt;
  &lt;li&gt;Always round trip data back into Sheets so you can see what the system did.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From there you can layer on extras. Social post generation, image generation, cross-posting to other platforms. I keep those in separate scenarios so this core pipeline stays boring and reliable.&lt;/p&gt;

&lt;p&gt;That is the full blueprint I run right now. It will probably change again in a month. But the core idea stays the same. Treat your content as structured data, not as a CMS typing session.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>nocode</category>
      <category>openai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>My Minimalist iPhone + Lightroom Photo Workflow (Like Reusable Components)</title>
      <dc:creator>Richard Lemon</dc:creator>
      <pubDate>Wed, 20 May 2026 06:42:19 +0000</pubDate>
      <link>https://dev.to/richardlemon/my-minimalist-iphone-lightroom-photo-workflow-like-reusable-components-30ei</link>
      <guid>https://dev.to/richardlemon/my-minimalist-iphone-lightroom-photo-workflow-like-reusable-components-30ei</guid>
      <description>&lt;h2&gt;Why I treat photos like UI components&lt;/h2&gt;

&lt;p&gt;I build web experiences for a living, so my brain is wired around components. Small pieces. Clear inputs. Predictable outputs.&lt;/p&gt;

&lt;p&gt;When I started taking photography more seriously, I fell into the same trap most devs do. Gear research. Camera reviews. Lens rabbit holes. Editing suite comparison charts with way too many checkmarks.&lt;/p&gt;

&lt;p&gt;Meanwhile, my camera roll was a mess and my Instagram was dead.&lt;/p&gt;

&lt;p&gt;So I did what I always do when a hobby starts to feel like work. I stripped the whole thing down until it felt more like React props than creative suffering.&lt;/p&gt;

&lt;p&gt;Right now my photography workflow is: iPhone camera + Lightroom on my phone. That is it. No DSLR body. No plugin zoo. No 45-minute “mood crafting” sessions per photo.&lt;/p&gt;

&lt;p&gt;Everything is built around a component mindset. Reusable pieces. Minimal configuration. Fast feedback. Just enough control to make it mine.&lt;/p&gt;

&lt;h2&gt;The constraint: photography as a side-quest, not a second job&lt;/h2&gt;

&lt;p&gt;I have a few non‑negotiables for creative hobbies.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;I can start in under 10 seconds.&lt;/li&gt;
  &lt;li&gt;I can ship in under 5 minutes.&lt;/li&gt;
  &lt;li&gt;The results look consistent enough to feel intentional.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of those break, my interest drops. That is true for side projects, for training plans, and for photography.&lt;/p&gt;

&lt;p&gt;I also know myself. If I carry a big camera bag, I will start thinking in “photo session mode” instead of “notice stuff and shoot” mode. That kills the fun for me.&lt;/p&gt;

&lt;p&gt;So I made a rule: if it does not fit in my pocket, it does not exist in my workflow.&lt;/p&gt;

&lt;p&gt;That leaves me with the device I already use to check stats, answer clients, and record baseball drills. The iPhone.&lt;/p&gt;

&lt;h2&gt;The capture layer: keeping the raw input simple&lt;/h2&gt;

&lt;p&gt;In front‑end, if the data that comes into your components is garbage, no clever UI will save it. Same with photos. If the input is bad, Lightroom is just a colorful coping mechanism.&lt;/p&gt;

&lt;p&gt;I do not shoot RAW on my phone.&lt;/p&gt;

&lt;p&gt;Yes, RAW is technically better. Yes, there is more dynamic range. I have shot RAW. I can tell the difference. I also know it bloats storage and slows down every step that comes after.&lt;/p&gt;

&lt;p&gt;For a mobile, Instagram‑first flow, I would rather optimize the system than chase theoretical quality. So I stick to HEIC or JPEG from the stock camera app.&lt;/p&gt;

&lt;p&gt;My “componentized” capture settings look like this:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Photo mode only&lt;/strong&gt;. No Pro, no filters, no Portrait for Instagram work. Portrait mode looks nice on the phone then falls apart in editing with weird edges and fake blur.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Grid enabled&lt;/strong&gt;. I treat the grid like a layout system. Rule of thirds, leading lines, horizon sanity check. Same as a CSS grid overlay for a new layout.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Exposure control&lt;/strong&gt;. I tap to focus and slide down slightly to underexpose. Phones love bright, flat images. I prefer a bit of headroom and mood.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Lens discipline&lt;/strong&gt;. I mostly stick to the main wide lens. The ultra‑wide is a special‑purpose component for architecture or big interiors. Digital zoom is basically a bug, so I “disable” it mentally.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also have a couple of mental capture presets. Not camera presets. Just tiny rules I repeat.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;“Find the clean background”&lt;/strong&gt;. Before I raise the phone, I quickly scan behind the subject. I move one or two steps to clean up lines and remove clutter. This is the same thinking as simplifying a DOM tree before styling.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;“Commit to one light direction”&lt;/strong&gt;. I walk around the subject until I can clearly see where the light is coming from. I shoot primarily from that side. This keeps shadows intentional instead of random.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is it. No half‑hearted HDR toggles. No live filters. Clean input.&lt;/p&gt;

&lt;h2&gt;Lightroom as a component library, not a blank canvas&lt;/h2&gt;

&lt;p&gt;Lightroom can be a black hole. Infinite sliders. Panels inside panels. If you treat it like a design tool, you will happily tweak a single photo for 30 minutes. Then you will burn out and stop posting.&lt;/p&gt;

&lt;p&gt;I treat Lightroom mobile like a component library.&lt;/p&gt;

&lt;p&gt;Components have props and sensible defaults. You are not redesigning a button every time. You pick a variant. You override a couple of values. You ship.&lt;/p&gt;

&lt;p&gt;My Lightroom setup uses the same idea.&lt;/p&gt;

&lt;h2&gt;Building my “preset components” from actual use&lt;/h2&gt;

&lt;p&gt;I did not download a random preset pack from some influencer. I am not against presets, I just do not like trusting a black box that was tuned for Bali sunsets when I am shooting Dutch winter light.&lt;/p&gt;

&lt;p&gt;Instead, I shot for a week in my normal routine. Commute. Lunch walks. Baseball field. Home. Every time I edited a photo in Lightroom, I forced myself to notice patterns.&lt;/p&gt;

&lt;p&gt;Typical slider changes for me:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Exposure slightly down, contrast slightly up.&lt;/li&gt;
  &lt;li&gt;Highlights down, shadows slightly up.&lt;/li&gt;
  &lt;li&gt;Whites up a bit, blacks down a bit.&lt;/li&gt;
  &lt;li&gt;Vibrance up, saturation almost untouched.&lt;/li&gt;
  &lt;li&gt;A small S‑curve in the tone curve panel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also realized I almost always desaturate yellow and raise orange luminance, especially for skin. Greens get tamed a bit. Sky blues get a tiny saturation bump.&lt;/p&gt;

&lt;p&gt;That observation process is boring but important. It is like scanning your commits over a week and realizing you always add the same utility classes. That is your implicit design system trying to exist.&lt;/p&gt;

&lt;p&gt;After a few days I created my first real preset. I applied my “default” edit to one photo, then saved those settings as a preset in Lightroom mobile. I named it something aggressively uncreative like &lt;code&gt;RL_Base_Daylight&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That preset is now my default component. It handles 70 percent of my shots.&lt;/p&gt;

&lt;h2&gt;Preset variants, like component variants&lt;/h2&gt;

&lt;p&gt;One preset is not enough, but ten are too many. I treat presets like button variants. A few solid ones. Clear scenarios. No overlap.&lt;/p&gt;

&lt;p&gt;Right now I use four Lightroom presets regularly:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Base Daylight&lt;/strong&gt;. For most outdoor daytime shots. Neutral, slightly contrasty, moderate saturation, restrained greens.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Soft Overcast&lt;/strong&gt;. Slightly warmer temp, reduced clarity, gentler contrast. Built for grey Dutch skies that kill depth.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Indoor Warm&lt;/strong&gt;. For home or coffee shops. Temp pushed a bit cooler to fight orange LEDs, plus more noise reduction and less clarity.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Field Night&lt;/strong&gt;. For baseball training under floodlights. More aggressive noise control, cooler white balance, lifted shadows to pull detail out of dark uniforms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each preset bakes in everything except crop and local adjustments. White balance is “soft opinionated” so I still tweak it per shot. Same idea as default props that can be overridden.&lt;/p&gt;

&lt;p&gt;This is the core of the workflow. Pick a preset like you pick a component variant. Then only touch the necessary knobs.&lt;/p&gt;

&lt;h2&gt;The micro‑flow: editing a single photo&lt;/h2&gt;

&lt;p&gt;Here is the actual sequence I use for a photo I plan to post. No theory. Just the real steps.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
&lt;strong&gt;Pick a favorite&lt;/strong&gt;. In the Photos app I quickly star or favorite 3–5 candidates from a moment. I only import those into Lightroom. I do not invite my entire camera roll to the party.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Import into Lightroom mobile&lt;/strong&gt;. One tap from the share sheet. I keep albums very loose: &lt;em&gt;Daily&lt;/em&gt;, &lt;em&gt;Field&lt;/em&gt;, &lt;em&gt;Travel&lt;/em&gt;. Enough to find things, not enough to become a second job.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Apply a preset&lt;/strong&gt;. I start with Base Daylight or one of the variants. No raw sliders yet. Just a single preset tap.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Fix crop first&lt;/strong&gt;. I correct horizon, straighten buildings, and tighten the frame. Cropping first is like fixing container width before adjusting typography.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Tune light&lt;/strong&gt;. I nudge exposure, then check shadows and highlights. If I spend more than 30 seconds here, I restart from the preset and force a smaller adjustment.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Tune color&lt;/strong&gt;. Small tweaks to temperature and tint. Maybe HSL on orange / yellow if skin tones look off. I try to never open the full HSL panel unless something clearly bothers me.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Detail and effects&lt;/strong&gt;. A bit of sharpening, lens correction if needed, and sometimes a tiny vignette. No artificial grain. I think fake grain is overrated on phone photos.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Export to camera roll&lt;/strong&gt;. Highest resolution JPEG back to Photos. Then straight to Instagram.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That whole cycle usually takes under two minutes. If it does not, the photo is not worth it for Instagram. I archive it and move on.&lt;/p&gt;

&lt;h2&gt;Batching photos like UI states&lt;/h2&gt;

&lt;p&gt;One nice thing about Lightroom presets is that they behave like the default state for a group of similar shots. I try to think of “sets” of images the same way I think about button states.&lt;/p&gt;

&lt;p&gt;Say I shot 10 photos at the field on a cloudy evening. Those are clearly one group. Same light, same environment, similar subject.&lt;/p&gt;

&lt;p&gt;My mini‑batch flow:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Import all 10 into a &lt;em&gt;Field&lt;/em&gt; album.&lt;/li&gt;
  &lt;li&gt;Pick my favorite and edit it fully, using the Field Night preset as a base.&lt;/li&gt;
  &lt;li&gt;Copy the settings in Lightroom and paste them to the other 9.&lt;/li&gt;
  &lt;li&gt;Only adjust exposure and crop on each of the others.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the entire set has a consistent look and took maybe 10–15 minutes. I do not love batch editing as a creative act, but as a system design problem it feels honest and efficient.&lt;/p&gt;

&lt;h2&gt;Constraints that keep it fun&lt;/h2&gt;

&lt;p&gt;A workflow is only good if you actually use it repeatedly. I have a few constraints that keep this photo thing in the hobby zone instead of the productivity zone.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;No laptop editing&lt;/strong&gt;. If I cannot finish the edit on my phone, I do not post it. I spend all day in front of a bigger screen already.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;No “someday” folders&lt;/strong&gt;. If an image does not get edited the same day or the next morning, it goes to the archive with zero guilt. No backlog to manage.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Instagram only&lt;/strong&gt;. I am not trying to print gallery pieces. My target medium is a small glowing rectangle people scroll past during coffee. That simplifies choices a lot.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;One crop rule&lt;/strong&gt;. I edit for a single aspect ratio per session. Usually 4:5 vertical for Instagram. I do not make web, story, and print crops for the same photo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those sound strict, but they are just guardrails. They stop me from turning a relaxing walk into a production pipeline.&lt;/p&gt;

&lt;h2&gt;Why this feels like front‑end work (in a good way)&lt;/h2&gt;

&lt;p&gt;The fun part for me is how much this resembles front‑end thinking once you strip away all the drama around “art”.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Inputs&lt;/strong&gt;. Light, subject, environment. Same as data and API shape.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Components&lt;/strong&gt;. Presets and small editing patterns. Like buttons, cards, text styles.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;System&lt;/strong&gt;. The capture → edit → export flow. Like your build tooling and deployment script.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Constraints&lt;/strong&gt;. Pocketable gear, Instagram output, phone‑only edits. Same as targeting a specific device range or performance budget.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once I started to see it that way, it stopped being overwhelming. I do not need the “best” lens any more than I need the “best” JavaScript framework for a static product page.&lt;/p&gt;

&lt;p&gt;I need a small, opinionated stack that I actually use on a Tuesday when I am tired and just want to post a photo of late‑night batting practice.&lt;/p&gt;

&lt;h2&gt;What I would change next&lt;/h2&gt;

&lt;p&gt;This workflow is not sacred. I treat it like a codebase. Small pull requests. No rewrites.&lt;/p&gt;

&lt;p&gt;A few things I am experimenting with:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;One black and white preset&lt;/strong&gt;. For rare cases where color distracts. I want a single, bold look, not 8 moody variations.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;A “story” preset&lt;/strong&gt;. Slightly higher contrast and clarity to survive Instagram compression on stories.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Shooting a tiny bit more RAW&lt;/strong&gt;. Very selectively. Maybe one RAW frame per interesting scene when I know I might want a print later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of these start slowing down the main flow, they get reverted. Same rules as any healthy component library. If a new component adds more cognitive load than value, it goes.&lt;/p&gt;

&lt;h2&gt;Keeping a hobby frictionless&lt;/h2&gt;

&lt;p&gt;I like building complex things at work. I like keeping my hobbies aggressively simple.&lt;/p&gt;

&lt;p&gt;iPhone camera. Lightroom mobile. A handful of presets I actually understand. A capture habit that fits around coaching, building products, and generally living a normal day.&lt;/p&gt;

&lt;p&gt;If photography currently feels heavy or guilt‑shaped for you, try cutting it until it feels closer to shipping a small UI component. Limited props. Clear output. Low drama.&lt;/p&gt;

&lt;p&gt;It is supposed to be fun.&lt;/p&gt;

</description>
      <category>devjournal</category>
      <category>ios</category>
      <category>productivity</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
