<?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: Rachid</title>
    <description>The latest articles on DEV Community by Rachid (@rachido_sama).</description>
    <link>https://dev.to/rachido_sama</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%2F3978239%2Fa8e109b1-7865-4cb7-97f0-7ecac664e6b3.jpg</url>
      <title>DEV Community: Rachid</title>
      <link>https://dev.to/rachido_sama</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rachido_sama"/>
    <language>en</language>
    <item>
      <title>The Astro Image Gotcha That Broke Our Production Build and How We Fixed It</title>
      <dc:creator>Rachid</dc:creator>
      <pubDate>Thu, 30 Jul 2026 23:07:05 +0000</pubDate>
      <link>https://dev.to/rachido_sama/the-astro-image-gotcha-that-broke-our-production-build-and-how-we-fixed-it-566d</link>
      <guid>https://dev.to/rachido_sama/the-astro-image-gotcha-that-broke-our-production-build-and-how-we-fixed-it-566d</guid>
      <description>&lt;p&gt;We ship a bilingual (French/Arabic) marketplace site built on Astro — static output, hundreds of location plus service pages, and a few thousand professional profile pages, each with a photo pulled from Firebase Storage. Nothing exotic. Then one day the production build just... died. No warning during local dev, no failing test, no lint error. npm run build exited non-zero, and the stack trace pointed at astro:assets.&lt;/p&gt;

&lt;p&gt;The cause, once I dug in: one profile's photo URL had gone stale. A Firebase Storage download token had rotated, so a request that used to return a JPEG now returned a 403. That's it. One dead URL, out of a few thousand, took the entire build down with it.&lt;/p&gt;

&lt;p&gt;That surprised me, because I assumed Astro's image pipeline would fail soft on a bad remote image — log a warning, skip it, move on. It doesn't. Here's why, and what we did about it.&lt;/p&gt;

&lt;p&gt;Why &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt; can't catch this&lt;/p&gt;

&lt;p&gt;Astro's &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt; component and the underlying getImage() function both hand off to an Image Service, whose job is to transform() the image — resize it, convert format, whatever your config says. That's the part you can hook into and customize.&lt;/p&gt;

&lt;p&gt;The problem is that transform() only ever receives an image that's already been fetched, as a raw buffer. The actual network request for a remote src happens earlier inside Astro's own internal loadRemoteImage step — whixh isn't part of the public, pluggable Image Service API. There's no supported hook that sits between "here's a remote URL" and "here's the fetched buffer" where you can intercept a failed request and decide what to do about it.&lt;/p&gt;

&lt;p&gt;So from the framework's point of view, a 403 or a timeout on a remote image isn't a "handle this gracefully" situation — it's a fatal error during static generation. If even one image URL in your dataset is dead, the whole build stops.&lt;/p&gt;

&lt;p&gt;The two-minute fix (and why it's not enough)&lt;/p&gt;

&lt;p&gt;The fastest way out is obvious: swap &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt; for a plain &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt; tag. That sidesteps Astro's build-time fetch-and-transform step entirely, because a plain &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt; just emits the URL as-is and lets the browser deal with it at runtime.&lt;/p&gt;

&lt;p&gt;That got our deploys unblocked immediately, but it's a real trade-off, not a fix — you lose automatic resizing, format conversion (WebP/AVIF), and the responsive srcset handling that's the entire reason to use &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt; in the first place. Fine as a stopgap. Not something to leave in place.&lt;/p&gt;

&lt;p&gt;The actual fix: check before you build, not during&lt;/p&gt;

&lt;p&gt;The real problem wasn't that Astro handles failures badly — it's that we were handing Astro URLs we hadn't verified. So we moved the verification earlier, into a prebuild step that runs before Astro ever touches an image.&lt;/p&gt;

&lt;p&gt;The script is small: collect every unique remote image URL your pages will reference, HEAD-check each one with a timeout, and write the ones that fail to a JSON manifest.&lt;/p&gt;

&lt;p&gt;js&lt;br&gt;
// scripts/validate-image-urls.mjs&lt;br&gt;
import fs from "node:fs";&lt;/p&gt;

&lt;p&gt;const TIMEOUT_MS = 8000;&lt;br&gt;
const CONCURRENCY = 10;&lt;/p&gt;

&lt;p&gt;async function checkUrl(url) {&lt;br&gt;
  const controller = new AbortController();&lt;br&gt;
  const timer = setTimeout(() =&amp;gt; controller.abort(), TIMEOUT_MS);&lt;br&gt;
  try {&lt;br&gt;
    const res = await fetch(url, { method: "HEAD", signal: controller.signal });&lt;br&gt;
    return res.ok;&lt;br&gt;
  } catch {&lt;br&gt;
    return false; // timeout, DNS failure, connection reset — all treated as "bad"&lt;br&gt;
  } finally {&lt;br&gt;
    clearTimeout(timer);&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Two design decisions mattered more than the code itself:&lt;/p&gt;

&lt;p&gt;Fail closed per URL, fail open in aggregate. If a single URL times out or errors, treat it as bad — that's the safe default for one image. But if, say, 70% of all URLs fail in the same run, that's not "most of our images died overnight," that's almost certainly the checker itself being broken — offline CI runner, DNS hiccup, rate limiting. In that case the script leaves the existing manifest untouched rather than blanking every photo on the site. A partial network failure in your validation step shouldn't be able to nuke your production content.&lt;/p&gt;

&lt;p&gt;Hook it in with prebuild, not an Astro integration. We wired this through npm's own lifecycle ("prebuild": "node scripts/validate-image-urls.mjs" in package.json), which runs automatically before build. We deliberately skipped writing it as an Astro integration hook — it's simpler, it's fully decoupled from Astro's internal build order, and it'd survive a framework migration untouched.&lt;/p&gt;

&lt;p&gt;On the component side, it's a one-line check:&lt;/p&gt;

&lt;h2&gt;
  
  
  astro
&lt;/h2&gt;

&lt;p&gt;import { isBadImageUrl } from "../utils/badImageUrls";&lt;/p&gt;

&lt;h2&gt;
  
  
  const showFallback = isBadImageUrl(profile.thumbnail);
&lt;/h2&gt;

&lt;p&gt;{showFallback&lt;br&gt;
  ? /* render an initials avatar instead &lt;em&gt;/ null&lt;br&gt;
  : /&lt;/em&gt; render the optimized Image component with profile.thumbnail as the source */ null}&lt;/p&gt;

&lt;p&gt;Known-bad URLs get a graceful fallback (initials avatar) instead of ever reaching &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt;. Everything else gets the full optimization pipeline as normal.&lt;/p&gt;

&lt;p&gt;Does it actually work at scale?&lt;/p&gt;

&lt;p&gt;The first real production run checked several thousand unique image URLs against the live network in a few seconds thanks to the concurrency limit, and it did exactly what it was supposed to: flagged a handful of genuinely dead URLs, left everything else alone, and the build stopped depending on the uptime of every single image host on the internet.&lt;/p&gt;

&lt;p&gt;One small habit that paid off while shipping this: when I reverted the emergency &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt; fallback back to &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt; once the validator was in place, I diffed against git history (git show HEAD:path/to/file) instead of trusting my memory of what the original props looked like. Cheap insurance — worth doing any time you're undoing an emergency fix under time pressure.&lt;/p&gt;

&lt;p&gt;The takeaway&lt;/p&gt;

&lt;p&gt;If you're using Astro's &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt; or getImage() with remote sources you don't fully control — user uploads, a CMS, cloud storage — don't assume a bad URL fails gracefully. It doesn't, by design, because the fetch happens in a part of the pipeline you can't hook into. Validate before the build starts, not during it.&lt;/p&gt;

&lt;p&gt;We run this on bricoleapp.com, a bilingual home-services marketplace for Morocco and Egypt — happy to go deeper on any part of this if it's useful.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>seo</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
