<?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: Brian Bauernschmitt</title>
    <description>The latest articles on DEV Community by Brian Bauernschmitt (@brokeinprod).</description>
    <link>https://dev.to/brokeinprod</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3984319%2F518c1b0d-9c23-4745-8360-33c4b545dc0d.png</url>
      <title>DEV Community: Brian Bauernschmitt</title>
      <link>https://dev.to/brokeinprod</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/brokeinprod"/>
    <language>en</language>
    <item>
      <title>I Thought I Had Three Weeks of Data. One GROUP BY Said Ten Minutes.</title>
      <dc:creator>Brian Bauernschmitt</dc:creator>
      <pubDate>Sun, 30 Aug 2026 04:30:24 +0000</pubDate>
      <link>https://dev.to/brokeinprod/i-thought-i-had-three-weeks-of-data-one-group-by-said-ten-minutes-cel</link>
      <guid>https://dev.to/brokeinprod/i-thought-i-had-three-weeks-of-data-one-group-by-said-ten-minutes-cel</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Thought&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Had&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Three&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Weeks&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Data.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;One&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;GROUP&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;BY&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Said&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Ten&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Minutes."&lt;/span&gt;
&lt;span class="na"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;process&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;was&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"running," Docker said "healthy." One GROUP BY told the truth&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;two five-minute test runs. Silent pipeline failure and the checks that catch it."&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql, docker, debugging, devops&lt;/span&gt;
&lt;span class="na"&gt;canonical_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://brokeinprod.dev/debugging/silent-pipeline-failure-ten-minutes-of-data&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The collection pipeline had been "running since August 5th." Three weeks of one-minute market data, accumulating quietly on my laptop. Then I ran one sanity query before a migration — and watched three weeks shrink to ten minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start here:&lt;/strong&gt; A pipeline that &lt;em&gt;ran&lt;/em&gt; is not a pipeline that's &lt;em&gt;running&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;My setup had no supervisor, it lived in a terminal session, and it wrote to a database inside Docker Desktop on a laptop. Yes, that is three layers of session software, each of which dies silently when you close a lid. The fix is three processes, in order of cheapness: &lt;strong&gt;(1) query the data, not the process&lt;/strong&gt; — &lt;code&gt;max(time)&lt;/code&gt; is a one-line monitoring system; &lt;strong&gt;(2) supervise anything that must survive your absence&lt;/strong&gt; (launchd/systemd, not a terminal tab); &lt;strong&gt;(3) restore-test your backups&lt;/strong&gt; before you need them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;A solo-built market data platform: a Rust binary subscribes to an FX broker's tick stream, aggregates 43 instruments into one-minute bars with microstructure stats (spreads, tradeable counts, mid-price variance), and hands them to a Python consumer that bulk-inserts into Postgres. The database runs in Docker Desktop on my laptop — an explicitly interim home while the permanent hardware was commissioned.&lt;/p&gt;

&lt;p&gt;Historical prices you can backfill anytime. Intra-minute microstructure exists only if you were listening when it happened — every day the stream is down is permanently unrecoverable. So the day the pipeline first wrote bars, I noted it as an epoch event: &lt;em&gt;the clock started August 5th.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then I got busy on other parts of the build. The pipeline was "running."&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;Three weeks later, preparing to migrate the database to its permanent machine, I opened Docker to check on things and got this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;CONTAINER ID   IMAGE                STATUS
a1b2c3d4e5f6   postgres:16-alpine   Up 18 seconds (healthy)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Created 3 months ago. Up &lt;em&gt;18 seconds&lt;/em&gt; — because Docker Desktop had just started, because &lt;em&gt;I&lt;/em&gt; had just started it, because it hadn't been running before I looked. Meanwhile &lt;code&gt;ps aux | grep stream_bars&lt;/code&gt; returned nothing. &lt;/p&gt;

&lt;p&gt;The collector wasn't running either.&lt;/p&gt;

&lt;p&gt;At this point I still believed the optimistic version: the pipeline ran for a good while, then "Docker kept crashing" took it down somewhere along the way. Some data lost, but most safe. The logs even offered a villain — a wall of &lt;code&gt;FATAL: database "db_name" does not exist&lt;/code&gt; errors repeating every five seconds, loud and red and &lt;em&gt;completely irrelevant&lt;/em&gt; (a healthcheck probing a database name that never existed — config drift, harmless to data). The loud error was innocent. The real problem hadn't logged anything, because the real problem was silence. The cause is rarely standing where the symptom is — the same shape as &lt;a href="https://brokeinprod.dev/debugging/tailwind-prose-leaking-into-components" rel="noopener noreferrer"&gt;the &lt;code&gt;prose&lt;/code&gt; leak&lt;/a&gt;, where the broken component was innocent and the context was the culprit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The diagnosis
&lt;/h2&gt;

&lt;p&gt;The migration plan started with a row count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ohlcv_1min&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- 252 bars · 2026-08-05 11:55 UTC → 2026-08-06 03:34 UTC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;252 bars. Across 43 instruments. First red flag: that's ~6 bars &lt;em&gt;per instrument&lt;/em&gt; over what the timestamps framed as 15½ hours — EUR/USD alone should produce hundreds. Second red flag: the window ended August 6th, not "sometime recently." Three weeks of assumed history was already down to fifteen hours. Then the query that ended the mystery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'hour'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ohlcv_1min&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;--  2026-08-05 11:00 |  88&lt;/span&gt;
&lt;span class="c1"&gt;--  2026-08-05 12:00 |   1&lt;/span&gt;
&lt;span class="c1"&gt;--  2026-08-06 03:00 | 163&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And with &lt;code&gt;SELECT DISTINCT time&lt;/code&gt; — ten distinct minutes. Total. Aug 5, 11:55–12:00. Aug 6, 03:30–03:34. Ten minute-bars × ~43 instruments = 252 rows. The arithmetic closed perfectly, and the story it told was not "pipeline crashed after fifteen hours." It was: &lt;strong&gt;the pipeline ran exactly twice, for five minutes each, while I was watching it.&lt;/strong&gt; Two test runs — I started it in a terminal, watched the bars land, felt great, and moved on. The process died when the terminal closed or the laptop slept, both times. There was never a third run.&lt;/p&gt;

&lt;p&gt;Nothing crashed. Nothing failed. Nothing was ever &lt;em&gt;deployed&lt;/em&gt; — and the difference between "I ran it and it worked" and "it is running" is the entire discipline of supervision, of which I had built zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix — three tiers, in order of cheapness
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tier 1 — monitor the data, not the process.&lt;/strong&gt; Every check I could have pointed at the &lt;em&gt;process&lt;/em&gt; (is the PID alive? is the container up?) answers the wrong question. The question is &lt;em&gt;did data arrive?&lt;/em&gt; — and the table itself answers it in one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ohlcv_1min&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- if this isn't recent, nothing else matters&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A bars-per-hour completeness query (the &lt;code&gt;GROUP BY&lt;/code&gt; above) run on a&lt;br&gt;
schedule, with an alert when the current hour is short, would have caught this on day one instead of day twenty-four. It costs nothing. It's the first monitoring primitive the platform now has — born as an autopsy tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 2 — supervise anything that must outlive your attention.&lt;/strong&gt; Terminal sessions, Docker Desktop, anything that starts at login: session software. It dies with the session, silently, and a laptop is a machine &lt;em&gt;designed&lt;/em&gt; to end sessions — lids close, updates reboot, batteries drain.&lt;/p&gt;

&lt;p&gt;The fix is boring and decades old: a real supervisor (launchd on macOS, systemd on Linux) with restart-on-failure, running on hardware whose job is to remain powered. "It's temporary" is not an exemption — my interim setup outlived its first failure by three weeks because nothing was watching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 3 — restore-test the backup, then trust it.&lt;/strong&gt; Before touching anything, &lt;code&gt;pg_dump&lt;/code&gt; the survivors and actually restore the dump somewhere else, counting rows on the far side. A backup that has never been restored is a hope, not a backup. Mine restored clean — 252 rows, exactly as sad as the original.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule of thumb
&lt;/h2&gt;

&lt;p&gt;If a human started it, then a human's logout will kill it. If you haven't queried the data since you deployed it, you don't know it's running — you &lt;em&gt;remember&lt;/em&gt; it running, which is a different thing. And when something breaks: the loudest error in the log is usually a bystander; interrogate the silence instead.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'll confess the part that stings: the epoch note. I wrote down the day the clock started — August 5th, 11:46 UTC — like a foundational ceremony, because for this dataset, history only exists if you're documenting it. Then I spent three weeks not listening to the listener. &lt;/p&gt;

&lt;p&gt;The data I was so careful to timestamp stopped nine minutes after the ceremony, and I found out from a &lt;code&gt;GROUP BY&lt;/code&gt; twenty-four days later. The clock I actually started was the one measuring how long a solo builder can believe in a pipeline nobody is watching. Answer: about three weeks. LOL!&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;The pipeline worked end-to-end, twice, flawlessly. That was the trap — "it works" and "it's running" are different claims, and only one of them shows up in a demo. The database knew the truth the whole time. All I had to do was ask it.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Wasn't the real bug the Docker crashes?&lt;/strong&gt;&lt;br&gt;
No — that's the misdirection for which this post exists. Docker Desktop stopping when the laptop reboots isn't a crash; it's the documented lifecycle of session software. The bug was architectural: no component anywhere owned the job of keeping the pipeline alive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not just check the logs?&lt;/strong&gt;&lt;br&gt;
The consumer's logs died with its terminal. The database logs were full of a loud, harmless red herring (a healthcheck probing a wrong database name). Logs tell you what &lt;em&gt;happened&lt;/em&gt;; only the data tells you what &lt;em&gt;didn't&lt;/em&gt;. &lt;code&gt;max(time)&lt;/code&gt; interrogates the absence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually catches this in production?&lt;/strong&gt;&lt;br&gt;
Freshness checks (&lt;code&gt;max(time)&lt;/code&gt; vs. now) and volume checks (rows per window vs. expectation), scheduled, with alerts. Process supervision keeps things running; data-level checks tell you when "running" stopped meaning "working." You want both, and if you can only have one, take the data check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Did you lose anything irreplaceable?&lt;/strong&gt;&lt;br&gt;
Three weeks of intra-minute microstructure that can never be backfilled — the one data class where absence is permanent. The consolation: it happened during an interim window before the data carried production load, which made it the cheapest possible version of this tuition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;launchd (macOS service management — &lt;code&gt;KeepAlive&lt;/code&gt; is the property at the heart of this post): &lt;a href="https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html" rel="noopener noreferrer"&gt;https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docker Desktop's lifecycle vs. server daemons (why containers "stop on their own" on laptops): &lt;a href="https://docs.docker.com/desktop/" rel="noopener noreferrer"&gt;https://docs.docker.com/desktop/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PostgreSQL &lt;code&gt;pg_dump&lt;/code&gt; custom format + &lt;code&gt;pg_restore&lt;/code&gt; (the restore-test workflow): &lt;a href="https://www.postgresql.org/docs/16/app-pgdump.html" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/16/app-pgdump.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://brokeinprod.dev/debugging/silent-pipeline-failure-ten-minutes-of-data" rel="noopener noreferrer"&gt;brokeinprod.dev&lt;/a&gt; — field notes from production: the error, the cause, the one-line fix.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Tailwind Typography's prose Is Leaking Into Your Components — and the Three-Tier Fix</title>
      <dc:creator>Brian Bauernschmitt</dc:creator>
      <pubDate>Sat, 29 Aug 2026 14:52:35 +0000</pubDate>
      <link>https://dev.to/brokeinprod/tailwind-typographys-prose-is-leaking-into-your-components-and-the-three-tier-fix-2o64</link>
      <guid>https://dev.to/brokeinprod/tailwind-typographys-prose-is-leaking-into-your-components-and-the-three-tier-fix-2o64</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;My CTA button had underlined links and the wrong text color, and no matter which utility class I stacked on it the issues didn't correct. The culprit wasn't the button. It was the &lt;code&gt;prose&lt;/code&gt; class, forty lines up in the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start here:&lt;/strong&gt; The &lt;code&gt;prose&lt;/code&gt; class from &lt;code&gt;@tailwindcss/typography&lt;/code&gt; styles &lt;strong&gt;every matching element beneath it&lt;/strong&gt; — headings, paragraphs, and yes, the &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; inside your custom button component rendered mid-article. That's its job: it's built for HTML that you &lt;em&gt;don't&lt;/em&gt; control and applied to a tree you partially do. I have three fixes, in order of preference: &lt;strong&gt;(1) design your in-article components to match prose's expectations&lt;/strong&gt; so there's no fight; &lt;strong&gt;(2) re-tune prose itself&lt;/strong&gt; with element modifiers (&lt;code&gt;prose-a:…&lt;/code&gt;) on the container; &lt;strong&gt;(3) wrap genuinely custom islands in &lt;code&gt;not-prose&lt;/code&gt;&lt;/strong&gt; — the sledgehammer, with caveats.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;An article layout on a content site: MDX renders inside a wrapper carrying &lt;code&gt;prose&lt;/code&gt; from &lt;code&gt;@tailwindcss/typography&lt;/code&gt; — on this build, exactly this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"prose prose-stone max-w-none font-serif"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MDXRemote&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="err"&gt;…&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is the right call — it's the fastest way to make Markdown-sourced body text beautiful. However, articles aren't only Markdown. This build renders custom components &lt;em&gt;inside&lt;/em&gt; the article body: styled CTA links, product cards, callouts.&lt;/p&gt;

&lt;p&gt;The components came out wrong: link colors overridden toward prose's palette, underlines appearing on things designed not to have them, spacing around the components inheriting article rhythm.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;The confusing part is &lt;em&gt;where you look&lt;/em&gt;. The component's own classes are correct — you can read them right there in the JSX. However, rendered inside the article, the element picks up styling that the component never asked for. The plugin's own issue tracker has this exact report: a button rendered inside prose content, underlined, resisting utility overrides.&lt;/p&gt;

&lt;p&gt;The trap: you debug the component, but the cause is the &lt;em&gt;context&lt;/em&gt; — the same debugging shape as &lt;a href="https://brokeinprod.dev/debugging/nextjs-fs-not-defined-client-components" rel="noopener noreferrer"&gt;the &lt;code&gt;fs is not defined&lt;/code&gt; bug&lt;/a&gt;: correct code, wrong place. &lt;code&gt;prose&lt;/code&gt; works by descendant selectors — &lt;code&gt;.prose&lt;/code&gt; generates rules targeting the elements beneath it (&lt;code&gt;a&lt;/code&gt;, &lt;code&gt;h2&lt;/code&gt;, &lt;code&gt;ul&lt;/code&gt;, &lt;code&gt;img&lt;/code&gt;, …).&lt;/p&gt;

&lt;p&gt;Any matching tag in the subtree is in scope, and your component's markup is, structurally, just more tags in that subtree. &lt;code&gt;prose&lt;/code&gt; cannot tell your hand-built CTA from a Markdown link. This is NOT a bug, but rather it's the feature, meeting markup it wasn't warned about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The diagnosis
&lt;/h2&gt;

&lt;p&gt;The plugin is designed for "HTML that you don't control." The moment you embed components that you &lt;em&gt;do&lt;/em&gt; control inside it, two styling authorities claim the same elements. Now, which one wins any given property is a cascade question (the modern plugin deliberately keeps specificity low via &lt;code&gt;:where()&lt;/code&gt;), but the practical result is consistent: &lt;strong&gt;properties your component doesn't explicitly set get prose's values, and the fight over the ones it does set is decided by CSS order — a place you don't want load-bearing logic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So the fix isn't "win the specificity war." It is deciding, deliberately, which styling authority owns what.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix — three tiers, in order
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tier 1 — match prose's expectations (the fix that isn't a fight).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For components that are &lt;em&gt;content&lt;/em&gt; — inline CTAs, emphasized links — the cleanest move is to design them to cooperate: let them inherit prose's link color and underline, but differentiate with weight or an accent border instead.&lt;/p&gt;

&lt;p&gt;This build runs Tier 1 on both of its content-like components: (1) the inline affiliate link is deliberately bold+underlined in the article's accent, and (2) the CTA button settled on light background, dark text (&lt;code&gt;text-stone-900&lt;/code&gt;), and an accent border — colors with which prose has no quarrel.&lt;/p&gt;

&lt;p&gt;The idea is to swim &lt;em&gt;with&lt;/em&gt; the current, not against it. Zero CSS conflict, and the components read as native to the article — which is what readers trust anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 2 — re-tune prose itself with element modifiers.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you want prose's rules changed everywhere — say, links in your brand color, no underline until hover — set it on the container, once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;article&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"prose prose-a:text-emerald-700 prose-a:no-underline hover:prose-a:underline"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the supported customization channel: you're not fighting with the plugin, but rather you're configuring it. Good for house-style adjustments; however, it's the wrong tool for "this one component is special."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 3 — &lt;code&gt;not-prose&lt;/code&gt; for genuine islands.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For embedded UI that is &lt;em&gt;not&lt;/em&gt; content — a product comparison card, an interactive demo, a form — sandbox it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"not-prose"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ProductCard&lt;/span&gt; &lt;span class="err"&gt;…&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prose styles stop at the boundary, which means that the island styles itself from zero. Two documented caveats: (1) &lt;strong&gt;you can't nest a new &lt;code&gt;prose&lt;/code&gt; region inside a &lt;code&gt;not-prose&lt;/code&gt; block&lt;/strong&gt;, and (2) the island now gets &lt;em&gt;none&lt;/em&gt; of the typographic defaults — every margin and font size inside is your job again. It's the right tool for cards and widgets; it's overkill (and extra work) for a styled link.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule of thumb
&lt;/h2&gt;

&lt;p&gt;Content-like → Tier 1 (cooperate). House-wide preference → Tier 2 (configure). True UI island → Tier 3 (sandbox). If you find yourself stacking &lt;code&gt;!important&lt;/code&gt;-flavored utilities on one element inside an article, you've skipped a tier.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'll confess how I know the fourth option is worse: I took it. Late one evening, I was tired of the cascade fight, so I ripped the typography plugin &amp;gt; out of the build entirely. The buttons looked right — but every heading, paragraph, and list in the article went back to unstyled browser defaults, because that plugin had been quietly doing all of that work. I restored it the next morning and picked a tier instead. Rebuilding Markdown's entire typography by hand to win one button is not a trade; it's a tantrum.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;&lt;code&gt;prose&lt;/code&gt; styles everything under it because that's its job. The fix was never beating it — it was deciding which elements are content, which are chrome, and telling each side its jurisdiction.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why don't my utility classes just win — utilities beat plugins, right?&lt;/strong&gt; &lt;br&gt;
Not reliably here. The plugin keeps its selectors low-specificity on purpose, so outcomes fall to source order and which properties each side sets. Some overrides work, some don't, and it feels random — which is why the answer is jurisdiction (the three tiers), not escalation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I put &lt;code&gt;not-prose&lt;/code&gt; on the component itself instead of a wrapper?&lt;/strong&gt;&lt;br&gt;
Yes — it's a class like any other. However, a wrapper &lt;code&gt;div&lt;/code&gt; is just often cleaner in MDX, and remember the exclusion applies to the element &lt;em&gt;and everything inside it&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this happen with &lt;code&gt;prose&lt;/code&gt; alternatives / my own &lt;code&gt;.markdown&lt;/code&gt; styles?&lt;/strong&gt; &lt;br&gt;
Identically. Any descendant-selector typography system has the same jurisdiction problem with embedded components; the three tiers apply regardless of whose plugin generated the rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a class="mentioned-user" href="https://dev.to/tailwindcss"&gt;@tailwindcss&lt;/a&gt;/typography — README (element modifiers, &lt;code&gt;not-prose&lt;/code&gt;,
the no-nesting caveat): &lt;a href="https://github.com/tailwindlabs/tailwindcss-typography" rel="noopener noreferrer"&gt;https://github.com/tailwindlabs/tailwindcss-typography&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Tailwind — Typography v0.5 announcement (why &lt;code&gt;not-prose&lt;/code&gt; exists, the specificity design): &lt;a href="https://tailwindcss.com/blog/tailwindcss-typography-v0-5" rel="noopener noreferrer"&gt;https://tailwindcss.com/blog/tailwindcss-typography-v0-5&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The original issue — a button inside prose, underlined:
&lt;a href="https://github.com/tailwindlabs/tailwindcss-typography/issues/32" rel="noopener noreferrer"&gt;https://github.com/tailwindlabs/tailwindcss-typography/issues/32&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://brokeinprod.dev/debugging/tailwind-prose-leaking-into-components" rel="noopener noreferrer"&gt;brokeinprod.dev&lt;/a&gt; — field notes from production: the error, the cause, the one-line fix.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tailwindcss</category>
      <category>css</category>
      <category>webdev</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Invalid src prop… hostname is not configured: It's One File, Not Two</title>
      <dc:creator>Brian Bauernschmitt</dc:creator>
      <pubDate>Fri, 21 Aug 2026 01:52:23 +0000</pubDate>
      <link>https://dev.to/brokeinprod/invalid-src-prop-hostname-is-not-configured-its-one-file-not-two-1phf</link>
      <guid>https://dev.to/brokeinprod/invalid-src-prop-hostname-is-not-configured-its-one-file-not-two-1phf</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;src&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;prop…&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;hostname&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;configured:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;It's&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;One&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;File,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Two"&lt;/span&gt;
&lt;span class="na"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;next/image&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;hostname&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;configured&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;means&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;your&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;remote&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;isn't&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;remotePatterns&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;—&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;your&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;CSP.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Here's&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;real&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;one-file&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;fix&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;why."&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextjs, webdev, debugging, react&lt;/span&gt;
&lt;span class="na"&gt;canonical_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://brokeinprod.dev/debugging/next-image-hostname-not-configured&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The upload worked! The database had the real URL. The page still crashed —&lt;br&gt;
because the part of Next.js that renders images had never heard of the host&lt;br&gt;
the part that stores them had just used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start here:&lt;/strong&gt; Add the image host to &lt;code&gt;images.remotePatterns&lt;/code&gt; in &lt;code&gt;next.config.ts&lt;/code&gt; (protocol, hostname, port, pathname), then restart the dev server — the config is read once, at startup. You do &lt;strong&gt;not&lt;/strong&gt; need to touch your CSP — optimized &lt;code&gt;next/image&lt;/code&gt; requests are served same-origin from &lt;code&gt;/_next/image&lt;/code&gt;, so &lt;code&gt;img-src 'self'&lt;/code&gt; already covers them. CSP only enters the picture if you bypass the optimizer (see below).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;I was building the admin side of a portfolio site for an artist — a small CRUD panel where the website owner uploads artwork and video thumbnails instead of editing code. Uploads go to object storage (Vercel Blob), the public URL gets saved in Postgres, and the front end renders everything through Next.js's &lt;code&gt;&amp;lt;Image&amp;gt;&lt;/code&gt; component for automatic resizing and WebP conversion.&lt;/p&gt;

&lt;p&gt;The upload itself worked on the first try. The file landed in storage, the URL came back, and the row saved. Then the page tried to render the image and the whole thing threw a runtime error.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Invalid src prop (https://&amp;lt;your-store&amp;gt;.public.blob.vercel-storage.com/artwork/file.webp)
on `next/image`, hostname "&amp;lt;your-store&amp;gt;.public.blob.vercel-storage.com"
is not configured under images in your `next.config.js`
See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important thing to notice: &lt;strong&gt;the upload didn't fail.&lt;/strong&gt; Storage was fine, the database was fine — you can open the URL in a browser tab and the file is right there. This is &lt;code&gt;next/image&lt;/code&gt; refusing to render a remote host it hasn't been told to trust: a hard runtime error, on purpose, not a broken-image icon. The error even hands you the docs link — which is genuinely the fastest path to the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The diagnosis
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;next/image&lt;/code&gt; isn't an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag; it's a front door to Next's Image Optimization API. By default, every image runs through the optimizer, which fetches the source server-side, resizes it, converts it to a modern format, and produces the result from your own domain. Because the optimizer makes a server-side request to whatever host you give it, an open &lt;code&gt;src&lt;/code&gt; would be an abuse vector — an optimizer that fetches arbitrary URLs is a free proxy wearing your domain name. So Next.js requires an explicit allowlist: &lt;code&gt;images.remotePatterns&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the host of your &lt;code&gt;src&lt;/code&gt; isn't in that list, you get the error above — before a single byte is fetched. The error is the security model working. Your job is to tell it, precisely, which hosts are yours.&lt;/p&gt;

&lt;p&gt;One detail that catches a lot of people off guard: the match is &lt;strong&gt;exact and case-sensitive across every part of the URL&lt;/strong&gt; — protocol, hostname, port, and pathname. &lt;code&gt;https&lt;/code&gt; is not &lt;code&gt;http&lt;/code&gt;. &lt;code&gt;assets.example.com&lt;/code&gt; is not &lt;code&gt;example.com&lt;/code&gt;. A too-shallow pathname (&lt;code&gt;/images/&lt;/code&gt; vs &lt;code&gt;/images/**&lt;/code&gt;) fails just as hard. So if you've added the host and it &lt;em&gt;still&lt;/em&gt; errors, you almost certainly have a part mismatch, not a missing entry.&lt;/p&gt;

&lt;p&gt;And if a tutorial shows &lt;code&gt;images.domains&lt;/code&gt; instead: that's the old way — &lt;strong&gt;deprecated since Next.js 14&lt;/strong&gt; in favor of &lt;code&gt;remotePatterns&lt;/code&gt; (which can scope protocol, port, and path, where &lt;code&gt;domains&lt;/code&gt; can't), and Next.js 16 lists it for removal in a future major version. Still works today with a warning; don't write new config with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Add the host to &lt;code&gt;remotePatterns&lt;/code&gt; in &lt;code&gt;next.config.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;remotePatterns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;your-store&amp;gt;.public.blob.vercel-storage.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/**&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On recent Next.js versions there's a terser shorthand that takes a &lt;code&gt;URL&lt;/code&gt; directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;remotePatterns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://&amp;lt;your-store&amp;gt;.public.blob.vercel-storage.com/**&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then restart the dev server — &lt;code&gt;next.config&lt;/code&gt; is read once at startup and isn't hot-reloaded, so without a restart you'll swear the fix didn't work. Editing the config and watching the same error is the classic second act of this bug.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# stop the dev server, then:&lt;/span&gt;
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Scope the pattern to &lt;em&gt;your&lt;/em&gt; store, not the platform
&lt;/h3&gt;

&lt;p&gt;A word on wildcards, because a pattern you'll see in a lot of posts is subtly wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*.public.blob.vercel-storage.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;// don't do this&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That looks account-scoped. It isn't — it matches &lt;strong&gt;every Vercel Blob store on the platform&lt;/strong&gt;, including anyone else's. If someone can get a URL into your pages, your optimizer will happily fetch and serve images from a stranger's store under your domain. And the fully open &lt;code&gt;hostname: "**"&lt;/code&gt; is worse still: it makes the error vanish and reopens the exact open-proxy problem the allowlist exists to close. Scope the pattern to the narrowest true thing — your exact store hostname and a path prefix.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One more thing about that error message: it was doing me a favor. An image&lt;br&gt;
optimizer that fetches from any URL on the internet is a free proxy wearing&lt;br&gt;
your domain name. The allow-list that interrupted my afternoon is the only&lt;br&gt;
thing standing between "renders my images" and "renders anyone's."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The CSP twist (the part most posts get wrong)
&lt;/h2&gt;

&lt;p&gt;If you run a strict Content-Security-Policy, you might expect to also add the Blob host to your &lt;code&gt;img-src&lt;/code&gt; directive — some posts even frame this error as a "two files" fix. &lt;strong&gt;You usually don't have to&lt;/strong&gt; — and here's the reasoning, because it's worth understanding rather than copy-pasting.&lt;/p&gt;

&lt;p&gt;When optimization is on (the default), the browser never requests the Blob URL directly. It requests &lt;code&gt;/_next/image?url=...&lt;/code&gt; from your own origin, and Next.js fetches the remote source server-side. So as far as the browser's CSP is concerned, the image is same-origin and &lt;code&gt;img-src 'self'&lt;/code&gt; already allows it. That's why this site's CSP never listed the Blob host, yet the images rendered.&lt;/p&gt;

&lt;p&gt;You only need the remote host in &lt;code&gt;img-src&lt;/code&gt; when you've taken the image &lt;em&gt;out&lt;/em&gt; of the optimizer's path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you set &lt;code&gt;unoptimized&lt;/code&gt; on the image or globally,&lt;/li&gt;
&lt;li&gt;you use a custom loader that returns the remote URL,&lt;/li&gt;
&lt;li&gt;you render it with a plain &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag or a CSS &lt;code&gt;background-image&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;or you reference it in an Open Graph / &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tag (those aren't &lt;code&gt;next/image&lt;/code&gt; at all).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the honest rule is: &lt;strong&gt;&lt;code&gt;remotePatterns&lt;/code&gt; is the fix; CSP is a conditional follow-up that only applies if you bypass optimization.&lt;/strong&gt; The two allowlists answer different questions — &lt;code&gt;remotePatterns&lt;/code&gt; is Next's permission to &lt;em&gt;fetch and optimize&lt;/em&gt; the image; &lt;code&gt;img-src&lt;/code&gt; is the browser's permission to &lt;em&gt;display&lt;/em&gt; whatever URL actually ends up in the page. With the default loader, that URL is your own origin, so the browser's question is already answered.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;Set up &lt;code&gt;remotePatterns&lt;/code&gt; the moment you wire in object storage, before the first upload — not after the first crash. It's two minutes of config that turns a confusing runtime error into a non-event. And scope the pattern to the exact host and path prefix you control; the wildcard is a smell, not a shortcut.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The upload worked but the image won't render — why?&lt;/strong&gt;&lt;br&gt;
Different systems. Storage happily accepts and serves the file; &lt;code&gt;next/image&lt;/code&gt; separately refuses to optimize from hosts you haven't allowlisted. A reachable URL proves nothing about the allowlist. Fixing &lt;code&gt;remotePatterns&lt;/code&gt; doesn't touch the upload at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I added the pattern and it still fails. Why?&lt;/strong&gt;&lt;br&gt;
In order of likelihood: you didn't restart the dev server; or the pattern mismatches in one strict part (protocol, subdomain, port, pathname depth). If it works locally but fails in production &lt;em&gt;and&lt;/em&gt; you've bypassed optimization (&lt;code&gt;unoptimized&lt;/code&gt;, custom loader, plain &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;), then — and only then — check your CSP's &lt;code&gt;img-src&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I just set &lt;code&gt;unoptimized&lt;/code&gt; and skip this?&lt;/strong&gt;&lt;br&gt;
You can — per-image or globally — and you give up resizing, format conversion, and blur placeholders. It's an escape hatch for special cases (SVGs, tiny icons), not the fix for an unconfigured host. Note that &lt;code&gt;unoptimized&lt;/code&gt; also puts the remote URL back in the browser's path, so a strict CSP would then need the host in &lt;code&gt;img-src&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do local images need &lt;code&gt;remotePatterns&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
No. &lt;code&gt;remotePatterns&lt;/code&gt; only governs &lt;em&gt;remote&lt;/em&gt; hosts. Images you import or serve from &lt;code&gt;/public&lt;/code&gt; are local and exempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js — &lt;code&gt;next/image&lt;/code&gt; Un-configured Host&lt;/strong&gt; (the page the error links to; explains exact-match rules and the &lt;code&gt;new URL()&lt;/code&gt; shorthand): &lt;a href="https://nextjs.org/docs/messages/next-image-unconfigured-host" rel="noopener noreferrer"&gt;https://nextjs.org/docs/messages/next-image-unconfigured-host&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js — Image component API reference&lt;/strong&gt; (&lt;code&gt;remotePatterns&lt;/code&gt;, the &lt;code&gt;domains&lt;/code&gt; deprecation, loaders, the &lt;code&gt;/_next/image&lt;/code&gt; optimization path): &lt;a href="https://nextjs.org/docs/app/api-reference/components/image" rel="noopener noreferrer"&gt;https://nextjs.org/docs/app/api-reference/components/image&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js — Image Optimization guide&lt;/strong&gt; (how the default loader fetches remote images and serves them from your own server): &lt;a href="https://nextjs.org/docs/app/getting-started/images" rel="noopener noreferrer"&gt;https://nextjs.org/docs/app/getting-started/images&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;{/* AFFILIATE (internal): Bunny.net optional closing aside when machinery fires up.&lt;br&gt;
    Cross-links at publish of the targets: → P-02 (why object storage),&lt;br&gt;
    → P-03 (the CSP companion — 'self' covering /_next/image explained in full),&lt;br&gt;
    → P-06 (cache/restart). SEO title variant for seoTitle field if the site adds&lt;br&gt;
    one: "Fix next/image 'hostname is not configured' in Next.js" */}&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
