<?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: casanovalabs</title>
    <description>The latest articles on DEV Community by casanovalabs (@casanovalabs).</description>
    <link>https://dev.to/casanovalabs</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%2F4057988%2F76fbdaa8-f637-448e-87ec-f031ff65476d.png</url>
      <title>DEV Community: casanovalabs</title>
      <link>https://dev.to/casanovalabs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/casanovalabs"/>
    <language>en</language>
    <item>
      <title>Serving AI-Generated Images at Scale on Next.js: Formats, Cache and the App Router Traps</title>
      <dc:creator>casanovalabs</dc:creator>
      <pubDate>Sat, 01 Aug 2026 14:58:59 +0000</pubDate>
      <link>https://dev.to/casanovalabs/serving-ai-generated-images-at-scale-on-nextjs-formats-cache-and-the-app-router-traps-19c6</link>
      <guid>https://dev.to/casanovalabs/serving-ai-generated-images-at-scale-on-nextjs-formats-cache-and-the-app-router-traps-19c6</guid>
      <description>&lt;p&gt;&lt;code&gt;next/image&lt;/code&gt; is built for a mostly-static image set: a handful of hero shots, a product catalogue, a blog's cover images. Point it at a pipeline that generates a fresh image per user action — an edited listing photo, a new render — and a few defaults that are invisible in the common case start to matter.&lt;/p&gt;

&lt;p&gt;We build &lt;a href="https://casanovalabs.com" rel="noopener noreferrer"&gt;CasaNova Labs&lt;/a&gt;, an AI studio for real estate photo and video editing, and every output the product generates gets served back through exactly this kind of pipeline. Here is what Next.js's own reference docs say about the parts that actually bite at that volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Format negotiation runs off the Accept header, not a version check
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;formats&lt;/code&gt; config controls which encodings the optimizer is allowed to produce. The default is &lt;code&gt;['image/webp']&lt;/code&gt;; Next.js reads the request's &lt;code&gt;Accept&lt;/code&gt; header to pick the best match, and if more than one configured format matches, "the array order matters" — the first hit in the array wins.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&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;formats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/avif&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/webp&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The docs are specific about the cost of adding AVIF: it "generally takes 50% longer to encode but it compresses 20% smaller compared to WebP," and when multiple formats are configured, "Next.js will cache each format separately" — meaning every source image now produces multiple cached variants, not one. Worth deciding on purpose, not by copying a starter config.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cache TTL is a floor, not a fact about the file
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;minimumCacheTTL&lt;/code&gt; sets the minimum seconds an optimized image is cached for; the default is &lt;code&gt;14400&lt;/code&gt; (4 hours). The actual expiration is "defined by either the &lt;code&gt;minimumCacheTTL&lt;/code&gt; or the upstream image &lt;code&gt;Cache-Control&lt;/code&gt; header, whichever is larger" — so a slow-changing config value can still be overridden upward by whatever header the origin image sent.&lt;/p&gt;

&lt;p&gt;The sharper edge is what happens when a source image changes: "there is no mechanism to invalidate the cache at this time." The documented fix is to change the &lt;code&gt;src&lt;/code&gt; prop (a new URL is a new cache entry) or delete the cached file under &lt;code&gt;&amp;lt;distDir&amp;gt;/cache/images&lt;/code&gt;. For anything that regenerates the same nominal image — a re-edited photo served from the same path — that's the detail that decides whether users see the old version for up to &lt;code&gt;minimumCacheTTL&lt;/code&gt; seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static imports skip the whole caching question
&lt;/h2&gt;

&lt;p&gt;For images that ship with the build rather than being generated at request time, the docs point at a different mechanism entirely: a Static Image Import. Importing a file directly, &lt;code&gt;import hero from './hero.jpg'&lt;/code&gt;, makes Next.js "automatically hash the file contents and cache the image forever with a &lt;code&gt;Cache-Control&lt;/code&gt; header of &lt;code&gt;immutable&lt;/code&gt;" — no TTL to tune, no invalidation problem, because a content hash in the URL means a changed file is a new URL. That only applies to build-time assets; anything generated after a request comes in still goes through the &lt;code&gt;remotePatterns&lt;/code&gt; and &lt;code&gt;minimumCacheTTL&lt;/code&gt; path above. Worth knowing which of the two a given image actually is before reaching for cache config.&lt;/p&gt;

&lt;p&gt;There's a related edge worth checking before wiring up authenticated image sources: "the Image Optimization API using the default loader will &lt;em&gt;not&lt;/em&gt; forward headers when fetching the &lt;code&gt;src&lt;/code&gt; image." A signed or authenticated URL for a freshly generated image can fail silently through the optimizer for exactly this reason — the documented way around it is the &lt;code&gt;unoptimized&lt;/code&gt; prop, which serves the source as-is instead of routing it through &lt;code&gt;/_next/image&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every external source has to be allow-listed
&lt;/h2&gt;

&lt;p&gt;If the images being served don't live under &lt;code&gt;/public&lt;/code&gt;, &lt;code&gt;remotePatterns&lt;/code&gt; is mandatory — Next.js will 400 anything that doesn't match:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&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="s1"&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="s1"&gt;assets.example.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;pathname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/renders/**&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;domains&lt;/code&gt;, the older config, is deprecated since Next.js 14 specifically because it "does not support wildcard pattern matching and it cannot restrict protocol, port, or pathname" — for a storage bucket or CDN host serving generated output, that's a meaningfully wider allow surface than &lt;code&gt;remotePatterns&lt;/code&gt; needs to grant.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;sizes&lt;/code&gt; decides how much of the srcset gets built
&lt;/h2&gt;

&lt;p&gt;Without a &lt;code&gt;sizes&lt;/code&gt; prop, "the browser assumes the image will be as wide as the viewport (100vw)," which the docs call out directly as a cause of unnecessarily large downloads. &lt;code&gt;sizes&lt;/code&gt; also changes what Next.js generates on the backend: without it, a limited srcset (roughly 1x/2x); with it, a full breakpoint-based srcset. For a grid of AI-generated thumbnails at a fixed card width, an unset &lt;code&gt;sizes&lt;/code&gt; prop is a real bandwidth cost, not a cosmetic detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quality allowlist is enforced server-side
&lt;/h2&gt;

&lt;p&gt;As of Next.js 16, &lt;code&gt;qualities&lt;/code&gt; is a required config — an explicit allowlist of quality values the optimizer will produce, defaulting to &lt;code&gt;[75]&lt;/code&gt;. A &lt;code&gt;quality&lt;/code&gt; prop that doesn't match an allowed value snaps to the closest one; hitting the optimization endpoint directly with a disallowed value returns &lt;code&gt;400&lt;/code&gt;. It exists, per the docs, because "unrestricted access could allow malicious actors to optimize more qualities than intended" — worth setting deliberately once output is coming from more than one static list of assets.&lt;/p&gt;

&lt;p&gt;None of these are bugs in Next.js — they're defaults tuned for a mostly-static image set, documented plainly, and worth revisiting the moment a pipeline starts generating images instead of just storing them. That's exactly the case &lt;a href="https://casanovalabs.com" rel="noopener noreferrer"&gt;CasaNova Labs&lt;/a&gt; builds for on every listing photo it renders back to a browser.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>performance</category>
      <category>javascript</category>
    </item>
    <item>
      <title>What a Portal's Resize Pipeline Actually Does to a Real Estate Listing Photo</title>
      <dc:creator>casanovalabs</dc:creator>
      <pubDate>Sat, 01 Aug 2026 14:58:58 +0000</pubDate>
      <link>https://dev.to/casanovalabs/what-a-portals-resize-pipeline-actually-does-to-a-real-estate-listing-photo-17l3</link>
      <guid>https://dev.to/casanovalabs/what-a-portals-resize-pipeline-actually-does-to-a-real-estate-listing-photo-17l3</guid>
      <description>&lt;p&gt;A listing photo leaves the camera looking right. By the time a portal has resized it for a thumbnail grid and recompressed it for delivery, it can look flatter, softer, or faintly wrong in a way nobody can quite name. None of that requires a bug on anyone's part — every step is documented, default behaviour in the image libraries doing the work.&lt;/p&gt;

&lt;p&gt;We build &lt;a href="https://casanovalabs.com" rel="noopener noreferrer"&gt;CasaNova Labs&lt;/a&gt;, an AI studio for real estate photo and video editing, and the same three operations — resize, recompress, convert format — run on every image the product touches. Here is what the defaults in &lt;code&gt;sharp&lt;/code&gt; (the Node.js binding for &lt;code&gt;libvips&lt;/code&gt;, the library most JS image pipelines end up calling) actually do at each step, read from sharp's own API docs rather than assumed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The kernel decides what a downscale looks like
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;resize()&lt;/code&gt; takes a &lt;code&gt;kernel&lt;/code&gt; option — the algorithm used to compute new pixel values when shrinking an image, with the inferred interpolator reused for upsampling. The default is &lt;code&gt;lanczos3&lt;/code&gt;, not the linear or nearest-neighbour interpolation a naive implementation might reach for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kernel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kernel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nearest&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;output.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line is in sharp's own docs as an example of what nearest-neighbour resizing looks like — a hard "before" case for comparison, not a recommendation. The docs also flag a second, easy-to-miss knob: &lt;code&gt;fastShrinkOnLoad&lt;/code&gt;, on by default, which takes advantage of JPEG and WebP's built-in shrink-on-load feature for speed. The tradeoff is explicit in the reference: it "can lead to a slight moiré pattern or round-down of an auto-scaled dimension." Fine for a thumbnail grid, worth turning off for a hero image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chroma subsampling isn't the same default across formats
&lt;/h2&gt;

&lt;p&gt;Chroma subsampling throws away colour resolution the eye is less sensitive to while keeping full luminance detail. Sharp's &lt;code&gt;jpeg()&lt;/code&gt; output defaults to &lt;code&gt;'4:2:0'&lt;/code&gt; — three-quarters of the colour data discarded — unless you set it explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jpeg&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;chromaSubsampling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4:4:4&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="nf"&gt;toBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part worth checking rather than assuming: &lt;code&gt;avif()&lt;/code&gt; and &lt;code&gt;heif()&lt;/code&gt; default the other way, to &lt;code&gt;'4:4:4'&lt;/code&gt; (full chroma), and only drop to &lt;code&gt;'4:2:0'&lt;/code&gt; if you ask for it. &lt;code&gt;webp()&lt;/code&gt; sits in between, with a &lt;code&gt;smartSubsample&lt;/code&gt; flag for higher-quality chroma subsampling and an &lt;code&gt;effort&lt;/code&gt; scale from &lt;code&gt;0&lt;/code&gt; (fastest) to &lt;code&gt;6&lt;/code&gt; (slowest), default &lt;code&gt;4&lt;/code&gt;. Three formats, three different defaults for the same concept — a pipeline that hard-codes one assumption across all three will be right for one of them and wrong for the other two.&lt;/p&gt;

&lt;h2&gt;
  
  
  The colour profile gets stripped unless you say otherwise
&lt;/h2&gt;

&lt;p&gt;This is the default sharp documents most plainly, and the one most likely to surprise: "the default behaviour, when &lt;code&gt;keepMetadata&lt;/code&gt; is not used, is to convert to the device-independent sRGB colour space and strip all metadata, including the removal of any ICC profile." A photo shot in Adobe RGB or with a camera-embedded profile gets flattened to sRGB on the way out, silently, every time.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;withMetadata()&lt;/code&gt; is the documented opt-out — it keeps EXIF, XMP and IPTC, and "will also convert to and add a web-friendly sRGB ICC profile if appropriate." For a specific target profile rather than the automatic one, &lt;code&gt;withIccProfile()&lt;/code&gt; takes either a filesystem path or one of the built-in names: &lt;code&gt;srgb&lt;/code&gt;, &lt;code&gt;p3&lt;/code&gt;, &lt;code&gt;cmyk&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;withIccProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;p3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cropping for a grid uses the same defaults, silently
&lt;/h2&gt;

&lt;p&gt;A portal's thumbnail grid almost always crops rather than letterboxes, and that crop has its own default worth knowing. Sharp's &lt;code&gt;resize()&lt;/code&gt; takes a &lt;code&gt;fit&lt;/code&gt; option — &lt;code&gt;cover&lt;/code&gt; (default), &lt;code&gt;contain&lt;/code&gt;, &lt;code&gt;fill&lt;/code&gt;, &lt;code&gt;inside&lt;/code&gt; or &lt;code&gt;outside&lt;/code&gt; — modelled on the CSS &lt;code&gt;object-fit&lt;/code&gt; property. With &lt;code&gt;fit: 'cover'&lt;/code&gt;, the crop is centred by default (&lt;code&gt;position: 'centre'&lt;/code&gt;), but it doesn't have to guess blindly: sharp also documents &lt;code&gt;gravity&lt;/code&gt; values (&lt;code&gt;north&lt;/code&gt;, &lt;code&gt;northeast&lt;/code&gt;, … &lt;code&gt;centre&lt;/code&gt;) and two content-aware strategies, &lt;code&gt;entropy&lt;/code&gt; (crops toward the region with the highest Shannon entropy) and &lt;code&gt;attention&lt;/code&gt; (crops toward the region of highest luminance frequency). A pipeline that leaves &lt;code&gt;fit&lt;/code&gt; and &lt;code&gt;position&lt;/code&gt; at their defaults gets a centred crop every time — fine for a portrait already centred on the subject, wrong for a wide shot where the interesting part of the room sits off to one side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attention&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;thumbnail.jpg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  None of this throws an error
&lt;/h2&gt;

&lt;p&gt;That is the actual problem with all three defaults: they are not bugs, so nothing fails and nothing logs. The image resizes, the format converts, the file ships — at three-quarters colour resolution, flattened to sRGB, through a kernel nobody chose on purpose. A pipeline that cares about the output has to set &lt;code&gt;kernel&lt;/code&gt;, &lt;code&gt;chromaSubsampling&lt;/code&gt; and the metadata/ICC handling deliberately, per format, rather than accept whatever the library shipped as a sane general-purpose default.&lt;/p&gt;

&lt;p&gt;That is the layer we tune for every image &lt;a href="https://casanovalabs.com" rel="noopener noreferrer"&gt;CasaNova Labs&lt;/a&gt; outputs, so a listing photo that comes out of the pipeline keeps the colour fidelity it went in with, rather than whatever three unrelated defaults happen to agree on.&lt;/p&gt;

</description>
      <category>node</category>
      <category>webdev</category>
      <category>sharp</category>
      <category>performance</category>
    </item>
    <item>
      <title>EU AI Act Article 50 for Image and Video Pipelines: What Actually Has to Be in the File</title>
      <dc:creator>casanovalabs</dc:creator>
      <pubDate>Sat, 01 Aug 2026 14:45:58 +0000</pubDate>
      <link>https://dev.to/casanovalabs/eu-ai-act-article-50-for-image-and-video-pipelines-what-actually-has-to-be-in-the-file-12f1</link>
      <guid>https://dev.to/casanovalabs/eu-ai-act-article-50-for-image-and-video-pipelines-what-actually-has-to-be-in-the-file-12f1</guid>
      <description>&lt;p&gt;Article 50 of the EU AI Act became applicable on 2 August 2026. It is one of the few provisions in the regulation that turns into an engineering ticket rather than a policy document: a system that generates images, audio or video must produce outputs that an automated tool can detect as artificially generated.&lt;/p&gt;

&lt;p&gt;We build &lt;a href="https://casanovalabs.com" rel="noopener noreferrer"&gt;CasaNova Labs&lt;/a&gt;, an AI studio for real estate photo and video editing, and we shipped this ahead of the date across every image and every video the product outputs. What follows isn't a legal explainer — it's what the requirement means at the file level, and the three traps that will make a pipeline ship unmarked files while every test stays green.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two obligations, two different actors
&lt;/h2&gt;

&lt;p&gt;The single most useful thing to get right before writing any code: Article 50 bundles requirements that fall on &lt;strong&gt;different parties&lt;/strong&gt;, and they are satisfied by different mechanisms.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;§2&lt;/strong&gt; binds the &lt;strong&gt;provider&lt;/strong&gt; of the generative system. Every synthetic output must carry marking in a &lt;strong&gt;machine-readable&lt;/strong&gt; format. Nothing in it is about human visibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;§4&lt;/strong&gt; binds the &lt;strong&gt;deployer&lt;/strong&gt; — whoever publishes the content. Where the content qualifies as a deepfake under Art. 3(60), manipulated content resembling existing people, objects or &lt;strong&gt;places&lt;/strong&gt; in a way that could pass as authentic, it must be disclosed "in a clear and recognisable manner."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distinction has a direct architectural consequence: §2 is metadata a detector parses, §4 is a label a human reads. They are two separate pipelines with two separate failure modes, and conflating them is the most common design mistake we've seen discussed.&lt;/p&gt;

&lt;p&gt;For our part, the claim we make and the only one: &lt;strong&gt;our outputs carry machine-readable marking within the meaning of Article 50(2)&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "machine-readable" resolves to
&lt;/h2&gt;

&lt;p&gt;There is no bespoke format to invent. Detectors converge on the IPTC &lt;code&gt;DigitalSourceType&lt;/code&gt; property, and the controlled-vocabulary value that matters for generated content is &lt;code&gt;trainedAlgorithmicMedia&lt;/code&gt;. Written into an XMP packet, it is what an automated checker actually looks for — a string a parser can match, rather than a free-text note in a description field that no tool will ever read.&lt;/p&gt;

&lt;p&gt;For still images that packet sits alongside standard EXIF fields. For MP4, it goes into a top-level &lt;code&gt;uuid&lt;/code&gt; box carrying the UUID reserved for XMP by the XMP specification (Part 3, &lt;em&gt;Storage in Files&lt;/em&gt;) — no re-encode required, since an MP4 is a flat sequence of &lt;code&gt;[size][type][payload]&lt;/code&gt; boxes.&lt;/p&gt;

&lt;p&gt;So far, so mechanical. The interesting part is everything that quietly removes it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 1 — your own resize eats the metadata
&lt;/h2&gt;

&lt;p&gt;Most image processing libraries drop metadata on transform by default; it's the safe behaviour for them and the wrong one for you. A thumbnail route that resizes on the fly will serve a file with no marking whatsoever, no matter what the stored original carries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// strips EXIF/XMP — the thumbnail ships unmarked&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;320&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;webp&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// preserves it&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;320&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;withMetadata&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;webp&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing throws. The image looks perfect. &lt;strong&gt;Audit every path that re-encodes an image between storage and the client&lt;/strong&gt;, not just the one that writes it — delivery-time transforms are where this hides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 2 — in MP4, &lt;em&gt;where&lt;/em&gt; you write it decides whether the file still plays
&lt;/h2&gt;

&lt;p&gt;Appending a box to an MP4 is only safe at the end. The &lt;code&gt;moov&lt;/code&gt; atom's &lt;code&gt;stco&lt;/code&gt; table holds &lt;strong&gt;absolute byte offsets&lt;/strong&gt; into &lt;code&gt;mdat&lt;/code&gt;: insert anything before &lt;code&gt;mdat&lt;/code&gt; and every one of those offsets is now wrong. This is not theoretical — a box inserted right after &lt;code&gt;ftyp&lt;/code&gt; gives a file that decodes zero frames with &lt;code&gt;Invalid NAL unit size&lt;/code&gt;. Appended after the last box, nothing shifts and playback is untouched.&lt;/p&gt;

&lt;p&gt;If you take one thing from this article: append, never insert.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 3 — the visible label destroys the machine marking
&lt;/h2&gt;

&lt;p&gt;This one only appears once both obligations are implemented, which is why it tends to be found late.&lt;/p&gt;

&lt;p&gt;A §4 visible label on video means overlaying a rendered label on every frame, which means a full re-encode. And a re-encode &lt;strong&gt;rebuilds the container from scratch&lt;/strong&gt; — wiping out any &lt;code&gt;uuid&lt;/code&gt; box you appended beforehand. Run the two steps in the intuitive order (machine marking first, since it's the cheap one) and the §2 marking silently disappears from every single video you ship.&lt;/p&gt;

&lt;p&gt;The correct order is the counter-intuitive one: &lt;strong&gt;visible mark first, machine marking last.&lt;/strong&gt; Whatever writes to the container has to be the final operation in the chain.&lt;/p&gt;

&lt;p&gt;There's a broader version of this rule worth internalising: the marking step belongs at the single choke point every output passes through on its way to storage, and it belongs &lt;em&gt;after&lt;/em&gt; every transform. Anything that runs later gets to undo it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Article 50(2) is a small, mechanical requirement once you separate it cleanly from §4: write a controlled-vocabulary metadata field, in a format that survives your own transforms, at the one point every output funnels through. The whole difficulty is in ordering — and none of the three traps above produce an error, a log line or a failing test. They produce a file that looks correct and isn't marked.&lt;/p&gt;

&lt;p&gt;We shipped this across every image and video &lt;a href="https://casanovalabs.com" rel="noopener noreferrer"&gt;CasaNova Labs&lt;/a&gt; produces, ahead of the 2 August 2026 date. If you're building on the generation side, the three ordering rules above are what the requirement actually costs you: an afternoon, and knowing where to look.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>node</category>
      <category>webdev</category>
      <category>compliance</category>
    </item>
  </channel>
</rss>
