<?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: uninterrupted</title>
    <description>The latest articles on DEV Community by uninterrupted (@engineering).</description>
    <link>https://dev.to/engineering</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%2F3290827%2F5ccc7612-9770-41ed-a766-77b0557e8c3a.jpg</url>
      <title>DEV Community: uninterrupted</title>
      <link>https://dev.to/engineering</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/engineering"/>
    <language>en</language>
    <item>
      <title>Faster Geospatial Animations with PMTiles: Multi-Layer MVT Time Series in OpenLayers</title>
      <dc:creator>uninterrupted</dc:creator>
      <pubDate>Wed, 08 Jul 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/u11d/faster-geospatial-animations-with-pmtiles-multi-layer-mvt-time-series-in-openlayers-30d6</link>
      <guid>https://dev.to/u11d/faster-geospatial-animations-with-pmtiles-multi-layer-mvt-time-series-in-openlayers-30d6</guid>
      <description>&lt;p&gt;Most people try to animate time series by stacking separate tile sources or flipping CSS styles mid-frame. That causes flicker and a surprising amount of wasted CPU/GPU work. The core takeaway is simple: bake your timesteps into a single vector-tile archive (PMTiles) with per-timestep named layers, load it once, and drive animation by re-styling already-decoded tiles.&lt;/p&gt;

&lt;p&gt;I tested this with OpenLayers and noticed the difference right away: playback became smoother and far less CPU-bound once tiles were pre-baked and cached. The hard part is server-side — how you generate, package, and serve tiles matters more than which client library you pick.&lt;/p&gt;

&lt;p&gt;When you animate a map by switching between separate tile sources, each frame causes one of two expensive operations: a network fetch of new tiles, or client-side re-decoding and re-styling. Baking timesteps into one archive avoids that by turning each frame into a cheap style re-evaluation on already-decoded features.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architecture: single archive, dynamic layers
&lt;/h2&gt;

&lt;p&gt;MVT (Mapbox Vector Tile) is a compact protobuf-based vector tile format that encodes geometry and attributes per z/x/y tile. PMTiles packages MVT tiles into a single file for efficient distribution and partial HTTP range retrieval.&lt;/p&gt;

&lt;p&gt;The key workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Server side&lt;/strong&gt; — each timestep is a separate GeoJSON file. &lt;code&gt;tippecanoe&lt;/code&gt; joins them into one PMTiles archive, giving each timestep its own named MVT layer (&lt;code&gt;timestep_00&lt;/code&gt;, &lt;code&gt;timestep_01&lt;/code&gt;, …).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client side&lt;/strong&gt; — one &lt;code&gt;VectorTileSource&lt;/code&gt; loads the archive once. One &lt;code&gt;VectorTileLayer&lt;/code&gt; uses a style function that reads a mutable &lt;code&gt;currentLayerName&lt;/code&gt; variable. Advancing a frame means: update the variable, call &lt;code&gt;layer.changed()&lt;/code&gt;. OpenLayers re-runs the style function on the already-decoded tile features and repaints — no network request, no re-decode.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Complete working example
&lt;/h2&gt;

&lt;p&gt;!animation.gif&lt;/p&gt;

&lt;p&gt;This section contains everything needed to reproduce the demo from scratch: 11 per-timestep GeoJSON files, a tile generation script, and the client HTML.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Per-timestep GeoJSON files
&lt;/h3&gt;

&lt;p&gt;Create 11 files, one per timestep. Each contains a single large polygon with a &lt;code&gt;value&lt;/code&gt; property. The temporal identity comes from the file name (and the MVT layer name tippecanoe will assign), not from a property on the feature.&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="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;0 10&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;pad&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"%02d"&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;i &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
  &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"square_t&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pad&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.geojson"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
{
  "type": "FeatureCollection",
  "features": [{
    "type": "Feature",
    "geometry": {
      "type": "Polygon",
      "coordinates": [[[-60,-50],[60,-50],[60,50],[-60,50],[-60,-50]]]
    },
    "properties": { "value": &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt; }
  }]
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces &lt;code&gt;square_t00.geojson&lt;/code&gt; (value=0) through &lt;code&gt;square_t10.geojson&lt;/code&gt; (value=100).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Generate PMTiles with named layers (&lt;code&gt;generate-tiles.sh&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;SCRIPT_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_SOURCE&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

tippecanoe &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--named-layer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;timestep_00:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SCRIPT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/square_t00.geojson"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--named-layer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;timestep_01:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SCRIPT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/square_t01.geojson"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--named-layer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;timestep_02:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SCRIPT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/square_t02.geojson"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--named-layer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;timestep_03:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SCRIPT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/square_t03.geojson"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--named-layer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;timestep_04:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SCRIPT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/square_t04.geojson"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--named-layer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;timestep_05:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SCRIPT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/square_t05.geojson"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--named-layer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;timestep_06:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SCRIPT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/square_t06.geojson"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--named-layer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;timestep_07:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SCRIPT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/square_t07.geojson"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--named-layer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;timestep_08:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SCRIPT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/square_t08.geojson"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--named-layer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;timestep_09:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SCRIPT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/square_t09.geojson"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--named-layer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;timestep_10:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SCRIPT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/square_t10.geojson"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SCRIPT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/square.pmtiles"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-z&lt;/span&gt; 8 &lt;span class="nt"&gt;-Z&lt;/span&gt; 0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-simplification&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-tile-size-limit&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-feature-limit&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run with: &lt;code&gt;bash generate-tiles.sh&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — Start the server
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4 — Client animation (&lt;code&gt;index.html&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The full self-contained HTML file. No build step, no npm install — all dependencies come from &lt;code&gt;esm.sh&lt;/code&gt;, which rewrites bare module specifiers (like &lt;code&gt;pbf&lt;/code&gt; imported inside &lt;code&gt;ol/format/MVT&lt;/code&gt;) to absolute CDN URLs that the browser can resolve.&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="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;MVT Animation&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;https://cdn.jsdelivr.net/npm/ol@10.9.0/ol.css&amp;gt;"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;box-sizing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;border-box&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;monospace&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#111&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#eee&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
           &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100vh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;#map&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;#hud&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt; &lt;span class="m"&gt;20px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1a1a1a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#333&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;24px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;#hud&lt;/span&gt; &lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;13px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#aaa&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;#hud&lt;/span&gt; &lt;span class="nt"&gt;span&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;#timestep-val&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#7af&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;#value-val&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fa7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;#color-swatch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;28px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;28px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                    &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#555&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;#progress-bar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#333&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;#progress-fill&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;linear-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="nb"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#00f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#f00&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;#controls&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#333&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#eee&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#555&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
             &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5px&lt;/span&gt; &lt;span class="m"&gt;14px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;13px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#444&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;#loading&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;inset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
               &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
               &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;pointer-events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;999&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;#loading&lt;/span&gt;&lt;span class="nc"&gt;.hidden&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"map"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"loading"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Loading tiles…&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"hud"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&amp;lt;label&amp;gt;&lt;/span&gt;Timestep &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"timestep-val"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&amp;lt;label&amp;gt;&lt;/span&gt;Value &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"value-val"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:flex;align-items:center;gap:8px;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Color &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"color-swatch"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"progress-bar"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"progress-fill"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"width:0%"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"controls"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"btn-playpause"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Pause&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"btn-reset"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Reset&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;              &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;https://esm.sh/ol@10.9.0/Map&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;View&lt;/span&gt;             &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;https://esm.sh/ol@10.9.0/View&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;TileLayer&lt;/span&gt;        &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;https://esm.sh/ol@10.9.0/layer/Tile&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;OSM&lt;/span&gt;              &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;https://esm.sh/ol@10.9.0/source/OSM&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;VectorTileLayer&lt;/span&gt;  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;https://esm.sh/ol@10.9.0/layer/VectorTile&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;VectorTileSource&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;https://esm.sh/ol@10.9.0/source/VectorTile&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;MVT&lt;/span&gt;              &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;https://esm.sh/ol@10.9.0/format/MVT&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Style&lt;/span&gt;            &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;https://esm.sh/ol@10.9.0/style/Style&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Fill&lt;/span&gt;             &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;https://esm.sh/ol@10.9.0/style/Fill&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Stroke&lt;/span&gt;           &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;https://esm.sh/ol@10.9.0/style/Stroke&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fromLonLat&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="s1"&gt;&amp;lt;https://esm.sh/ol@10.9.0/proj&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PMTiles&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="s1"&gt;&amp;lt;https://esm.sh/pmtiles@4.4.1&amp;gt;&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;LAYER_NAMES&lt;/span&gt; &lt;span class="o"&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;timestep_00&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;timestep_01&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;timestep_02&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;timestep_03&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;timestep_04&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;timestep_05&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;timestep_06&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;timestep_07&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;timestep_08&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;timestep_09&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;timestep_10&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;VALUES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;LAYER_NAMES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&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;STYLES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;LAYER_NAMES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Style&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;fill&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;Fill&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`rgb(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;,0,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="na"&gt;stroke&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;Stroke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#fff&lt;/span&gt;&lt;span class="dl"&gt;'&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;2&lt;/span&gt; &lt;span class="p"&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;archive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PMTiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;square.pmtiles&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;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VectorTileSource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;format&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;MVT&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;square.pmtiles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;maxZoom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;currentLayerIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;squareLayer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VectorTileLayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;layer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;LAYER_NAMES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;currentLayerIndex&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;STYLES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;currentLayerIndex&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;layers&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;TileLayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;source&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;OSM&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="nx"&gt;squareLayer&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;view&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;View&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;center&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fromLonLat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="na"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&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;FRAME_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onFrame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;intervalId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;currentLayerIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentLayerIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;squareLayer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;onFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;FRAME_MS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intervalId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;intervalId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;currentLayerIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentLayerIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="nx"&gt;squareLayer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
          &lt;span class="nf"&gt;onFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
          &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;FRAME_MS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestepEl&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;timestep-val&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;valueEl&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;value-val&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;swatchEl&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;color-swatch&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;progressFill&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;progress-fill&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;layerName&lt;/span&gt;&lt;span class="p"&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;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;LAYER_NAMES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;layerName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;timestepEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;layerName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;valueEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;VALUES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="nx"&gt;swatchEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;background&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;STYLES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;getFill&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getColor&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;progressFill&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LAYER_NAMES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;onFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LAYER_NAMES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&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;loadingEl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startWhenReady&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;started&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;loadingEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;player&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LAYER_NAMES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onFrame&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tileloadend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;startWhenReady&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rendercomplete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;startWhenReady&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startWhenReady&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;playing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-playpause&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;playing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;playing&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;playing&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pause&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;Play&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;playing&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-reset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;playing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;playing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-playpause&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pause&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&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="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Running the full setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Install tippecanoe (macOS)&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;tippecanoe

&lt;span class="c"&gt;# 2. Create the 11 per-timestep GeoJSON files&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;0 10&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;pad&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"%02d"&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;i &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-60,-50],[60,-50],[60,50],[-60,50],[-60,-50]]]},"properties":{"value":'&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="s1"&gt;'}}]}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; square_t&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pad&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.geojson
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# 3. Generate the PMTiles archive&lt;/span&gt;
bash generate-tiles.sh

&lt;span class="c"&gt;# 4. Start the server&lt;/span&gt;
npx serve

&lt;span class="c"&gt;# 5. Open the demo&lt;/span&gt;
open &amp;lt;http://localhost:3000/index.html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Conclusion: paint time, not flip it
&lt;/h2&gt;

&lt;p&gt;Don't stack separate tile sources for time-series animation. Bake timesteps into a single PMTiles archive as named MVT layers, load it once, and drive playback by updating a closure variable and calling &lt;code&gt;layer.changed()&lt;/code&gt;. Serve the archive with &lt;code&gt;npx serve&lt;/code&gt;. Start animation only after the first tile arrives.&lt;/p&gt;

&lt;p&gt;The result: smooth playback from first load, no flicker on hard refresh, no blank map with cache disabled.&lt;/p&gt;

</description>
      <category>gis</category>
      <category>openlayers</category>
      <category>datavisualization</category>
      <category>geospatial</category>
    </item>
    <item>
      <title>Next.js 16 Caching for E-Commerce: Cache Components, use cache, revalidateTag, and Fresh Product Data</title>
      <dc:creator>uninterrupted</dc:creator>
      <pubDate>Tue, 02 Jun 2026 22:00:00 +0000</pubDate>
      <link>https://dev.to/u11d/nextjs-16-caching-for-e-commerce-cache-components-use-cache-revalidatetag-and-fresh-product-4ik1</link>
      <guid>https://dev.to/u11d/nextjs-16-caching-for-e-commerce-cache-components-use-cache-revalidatetag-and-fresh-product-4ik1</guid>
      <description>&lt;p&gt;Caching in e-commerce is never just about speed. A fast storefront is useful only if it still shows the right price, the correct stock level, and the right experience for the current customer.&lt;/p&gt;

&lt;p&gt;That is why caching in a Next.js storefront can be deceptively hard. Some data should be shared broadly and kept warm for SEO and performance. Some data should be refreshed often. Some should never be shared between users at all.&lt;/p&gt;

&lt;p&gt;Next.js 16 gives teams a much clearer toolbox for solving this problem with Cache Components, &lt;code&gt;use cache&lt;/code&gt;, tag-based invalidation, and explicit cache lifetime controls. Used properly, these features let you keep pages fast without drifting into stale commerce data.&lt;/p&gt;

&lt;p&gt;In this guide, I will walk through a practical way to think about caching in a modern storefront and show how to combine &lt;code&gt;use cache&lt;/code&gt;, &lt;code&gt;cacheLife&lt;/code&gt;, and &lt;code&gt;revalidateTag&lt;/code&gt; for real e-commerce use cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Caching Is Harder in E-Commerce Than in a Typical Content Site
&lt;/h2&gt;

&lt;p&gt;On a standard marketing site, most content changes infrequently. If a page is cached for a few minutes or even a few hours, the business impact is usually negligible.&lt;/p&gt;

&lt;p&gt;Commerce systems work differently.&lt;/p&gt;

&lt;p&gt;The same product page may contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stable product descriptions and category copy&lt;/li&gt;
&lt;li&gt;semi-dynamic data such as price, availability, shipping estimates, or promotion labels&lt;/li&gt;
&lt;li&gt;private data such as cart state, recently viewed items, or customer-specific pricing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treating all of that data the same way leads to one of two bad outcomes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You cache too aggressively and serve stale prices or availability.&lt;/li&gt;
&lt;li&gt;You disable caching everywhere and lose the performance benefits that help SEO and conversion.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The better approach is to split your data by volatility and audience.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Cache Boundaries That Matter Most
&lt;/h2&gt;

&lt;p&gt;For most commerce projects, the cleanest mental model is to divide data into three groups.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Stable catalog content
&lt;/h3&gt;

&lt;p&gt;This is the part of the page that usually changes only when content editors or merchandisers update the catalog.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;product title and description&lt;/li&gt;
&lt;li&gt;brand information&lt;/li&gt;
&lt;li&gt;CMS blocks, FAQs, and long-form copy&lt;/li&gt;
&lt;li&gt;category landing page copy&lt;/li&gt;
&lt;li&gt;SEO metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the best candidate for shared caching because it improves performance for everyone and is usually safe to invalidate on demand when content changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Shared but fast-changing commerce data
&lt;/h3&gt;

&lt;p&gt;This data is still shared between many users, but it changes more often and has stronger operational impact.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;current price for a product and region&lt;/li&gt;
&lt;li&gt;inventory status&lt;/li&gt;
&lt;li&gt;campaign badges&lt;/li&gt;
&lt;li&gt;delivery promises&lt;/li&gt;
&lt;li&gt;low-stock signals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This layer often benefits from a shorter cache lifetime and explicit invalidation.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. User-specific or session-specific data
&lt;/h3&gt;

&lt;p&gt;This is the data that should not be globally cached.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cart contents&lt;/li&gt;
&lt;li&gt;logged-in customer state&lt;/li&gt;
&lt;li&gt;B2B contract pricing&lt;/li&gt;
&lt;li&gt;personalized recommendations&lt;/li&gt;
&lt;li&gt;account-specific discounts&lt;/li&gt;
&lt;li&gt;checkout calculations tied to the active session&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where teams most often make expensive mistakes. If the output depends on the current user, you need to keep that boundary private.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Next.js 16 Changes in Practice
&lt;/h2&gt;

&lt;p&gt;The major advantage of Next.js 16 is not just that it caches. It is that caching becomes more explicit.&lt;/p&gt;

&lt;p&gt;Instead of treating an entire route as fully static or fully dynamic, you can choose more precise boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cache a component or function with &lt;code&gt;use cache&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;assign cache tags to data and invalidate them later&lt;/li&gt;
&lt;li&gt;control lifetime with &lt;code&gt;cacheLife&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;keep per-user data private where needed&lt;/li&gt;
&lt;li&gt;stream dynamic parts separately instead of forcing the entire page into request-time rendering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For e-commerce storefronts, that means you can serve a fast shell and still keep critical parts fresh.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Storefront Strategy
&lt;/h2&gt;

&lt;p&gt;Let us look at a product page. In a typical headless stack, product content might come from Medusa or another commerce backend, while editorial content may come from a CMS.&lt;/p&gt;

&lt;p&gt;The important point is not the backend itself. The important point is how you cache each layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cache the stable product content
&lt;/h3&gt;

&lt;p&gt;If product descriptions and media do not change every minute, cache them explicitly and tag them so they can be invalidated when merchandisers update the catalog.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cacheTag&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="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;use cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

  &lt;span class="nf"&gt;cacheTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`product:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COMMERCE_API_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/products/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to load product&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;This is a good fit for data that should be broadly reusable and revalidated only when a product actually changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use a shorter cache boundary for price and stock
&lt;/h3&gt;

&lt;p&gt;Price and stock need more care. In some stores, they can still be shared safely for a short period. In others, especially B2B or highly promotional environments, they may need user-level or request-level handling.&lt;/p&gt;

&lt;p&gt;For shared price or inventory data, a short-lived cache can be a sensible compromise.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cacheLife&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cacheTag&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="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getOffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;regionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;use cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

  &lt;span class="nf"&gt;cacheTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`offer:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;regionId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;cacheLife&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;expire&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COMMERCE_API_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/offers/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?region=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;regionId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to load offer&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;Here the offer can be shared, but only briefly. That is often enough to reduce backend pressure while still keeping the storefront operationally safe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep customer-specific data out of the shared cache
&lt;/h3&gt;

&lt;p&gt;If pricing, recommendations, or entitlements depend on the active customer, do not treat them as generic shared data.&lt;/p&gt;

&lt;p&gt;This applies especially to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;B2B price lists&lt;/li&gt;
&lt;li&gt;customer group discounts&lt;/li&gt;
&lt;li&gt;partner-only catalog visibility&lt;/li&gt;
&lt;li&gt;personalized recommendation widgets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, a private boundary or request-time fetching becomes more appropriate than a shared cache.&lt;/p&gt;

&lt;p&gt;The principle is simple: if another customer must never see the same output, that data should not live in a shared cache.&lt;/p&gt;

&lt;p&gt;If you do want to cache user-specific data briefly to avoid repeating the same work during a session, keep that cache private instead of shared.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cacheLife&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="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getRecommendations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;use cache: private&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

  &lt;span class="nf"&gt;cacheLife&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;expire&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RECOMMENDATION_API_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/products/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/recommendations?customer=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to load recommendations&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;That keeps the result bounded to the current user context instead of letting it leak into global storefront output. It is also worth noting that&amp;nbsp;&lt;code&gt;'use cache: private'&lt;/code&gt;&amp;nbsp;does not store its cache entries on the server. The function still runs during server rendering, but the cached result is kept only in the browser’s memory and does not survive a full page reload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters for Medusa and Other Headless Commerce Backends
&lt;/h2&gt;

&lt;p&gt;This pattern becomes especially valuable in headless commerce because the storefront is often responsible for combining multiple systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;commerce backend&lt;/li&gt;
&lt;li&gt;CMS&lt;/li&gt;
&lt;li&gt;search service&lt;/li&gt;
&lt;li&gt;recommendation engine&lt;/li&gt;
&lt;li&gt;analytics and experiment layers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If all of those systems are fetched at request time for every page view, the frontend becomes slower and more fragile than it needs to be.&lt;/p&gt;

&lt;p&gt;If all of them are aggressively cached together, the storefront becomes fast but operationally unsafe.&lt;/p&gt;

&lt;p&gt;The right answer is to cache by business meaning, not by convenience.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CMS content can usually be cached until editors publish changes.&lt;/li&gt;
&lt;li&gt;catalog copy can usually be cached and invalidated via tags.&lt;/li&gt;
&lt;li&gt;regional shared pricing can often be cached briefly.&lt;/li&gt;
&lt;li&gt;logged-in B2B pricing should stay private.&lt;/li&gt;
&lt;li&gt;cart and checkout state should stay request-scoped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the difference between a caching strategy that helps the business and one that only looks good in benchmarks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using &lt;code&gt;revalidateTag&lt;/code&gt; to Keep Data Fresh
&lt;/h2&gt;

&lt;p&gt;One of the most useful patterns in Next.js 16 is tag-based invalidation.&lt;/p&gt;

&lt;p&gt;Instead of revalidating whole pages blindly, you can invalidate the exact data domain that changed. That is particularly useful when updates come from webhooks triggered by your CMS, PIM, or commerce backend.&lt;/p&gt;

&lt;p&gt;For example, if a product is updated in Medusa or an editorial team changes CMS content linked to that product, you can invalidate the relevant tag immediately.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;revalidateTag&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="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&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;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`product:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;max&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`offer:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;regionId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;max&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;In practice, this lets you keep product content cached most of the time while still reacting quickly to actual changes.&lt;/p&gt;

&lt;p&gt;That is a much better fit for commerce than short global revalidation windows everywhere.&lt;/p&gt;

&lt;p&gt;One operational note matters here: never expose a public revalidation endpoint without request validation. If invalidation is triggered by a CMS, Medusa, or ERP webhook, verify the source with a shared secret or signature check before calling &lt;code&gt;revalidateTag&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Teams Usually Get It Wrong
&lt;/h2&gt;

&lt;p&gt;There are a few caching mistakes that repeatedly appear in storefront projects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 1: Caching personalized pricing globally
&lt;/h3&gt;

&lt;p&gt;This is the most dangerous one. If the result depends on customer identity, customer group, contract terms, or session data, do not let it leak into a shared cache boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 2: Making the whole page dynamic because one widget is dynamic
&lt;/h3&gt;

&lt;p&gt;A product page does not need to become fully request-time just because the cart badge or recommendation box is personalized.&lt;/p&gt;

&lt;p&gt;Keep the stable content cached and isolate the dynamic parts instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 3: Using &lt;code&gt;no-store&lt;/code&gt; too broadly
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;no-store&lt;/code&gt; is sometimes necessary, but if it becomes the default reaction to uncertainty, you throw away much of the value of the App Router architecture.&lt;/p&gt;

&lt;p&gt;Use it for truly request-bound data, not as a shortcut for unclear cache design.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 4: Forgetting business dimensions in cache keys
&lt;/h3&gt;

&lt;p&gt;Prices are rarely just “product price.” They are often tied to region, currency, channel, campaign, or customer group.&lt;/p&gt;

&lt;p&gt;If those dimensions are not reflected in the data boundary, the cache becomes unsafe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 5: Mixing editorial freshness with operational freshness
&lt;/h3&gt;

&lt;p&gt;A product description may be safe to cache until a content change occurs. Inventory and promotions may not be. These two layers should not be forced into the same lifetime.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Good Default Model for Product Pages
&lt;/h2&gt;

&lt;p&gt;If you need a practical starting point, this model works well for many storefronts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data type&lt;/th&gt;
&lt;th&gt;Suggested approach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Product description, SEO copy, editorial blocks&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;use cache&lt;/code&gt; with tags and on-demand invalidation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared regional price and stock&lt;/td&gt;
&lt;td&gt;short cache lifetime plus tags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recommendations&lt;/td&gt;
&lt;td&gt;private or isolated dynamic boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cart state&lt;/td&gt;
&lt;td&gt;request-time or session-specific logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout totals and payment readiness&lt;/td&gt;
&lt;td&gt;request-time only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B2B contract pricing&lt;/td&gt;
&lt;td&gt;private boundary, never generic shared output&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is not a framework rule. It is a business-safe baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Works Well With Streaming and PPR Thinking
&lt;/h2&gt;

&lt;p&gt;If you have already explored Partial Prerendering in Next.js 16, this caching model should feel familiar.&lt;/p&gt;

&lt;p&gt;The idea is the same:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep the stable shell fast&lt;/li&gt;
&lt;li&gt;isolate dynamic or high-volatility parts&lt;/li&gt;
&lt;li&gt;avoid blocking the entire page for data that does not deserve that privilege&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For commerce, that means a product page can still load quickly for users and search engines while the truly dynamic fragments remain fresh.&lt;/p&gt;

&lt;p&gt;That is a much more scalable model than forcing every route into either “fully static” or “fully dynamic.”&lt;/p&gt;

&lt;h2&gt;
  
  
  A Note on Webhooks and Operational Ownership
&lt;/h2&gt;

&lt;p&gt;Caching only works well if invalidation is treated as part of the architecture, not as an afterthought.&lt;/p&gt;

&lt;p&gt;If your team updates product data in Medusa, content in a CMS, or prices in an ERP, your storefront should know how those changes trigger cache invalidation.&lt;/p&gt;

&lt;p&gt;Typical patterns include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Medusa webhooks invalidating product and offer tags&lt;/li&gt;
&lt;li&gt;CMS publish events invalidating page or content tags&lt;/li&gt;
&lt;li&gt;ERP sync jobs invalidating pricing-related tags&lt;/li&gt;
&lt;li&gt;stock update events invalidating inventory-related tags only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where many storefronts move from “fast in development” to “reliable in production.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The real value of caching in Next.js 16 is not that it makes everything static. It is that it helps you decide what should be shared, what should be refreshed, and what should remain private.&lt;/p&gt;

&lt;p&gt;For e-commerce teams, that distinction matters more than raw speed numbers. The fastest storefront is not the one that caches the most. It is the one that caches the right things.&lt;/p&gt;

&lt;p&gt;If you separate stable catalog content, shared operational data, and user-specific state into different cache boundaries, you can keep your storefront fast without risking stale or incorrect business data.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Asset-Based Data Orchestration: Lessons from Building a Multi-State Social Data Platform</title>
      <dc:creator>uninterrupted</dc:creator>
      <pubDate>Thu, 26 Mar 2026 07:21:25 +0000</pubDate>
      <link>https://dev.to/u11d/asset-based-data-orchestration-lessons-from-building-a-multi-state-social-data-platform-9l</link>
      <guid>https://dev.to/u11d/asset-based-data-orchestration-lessons-from-building-a-multi-state-social-data-platform-9l</guid>
      <description>&lt;p&gt;Building reliable data platforms rarely fails because of scale alone.&lt;br&gt;
More often, reliability collapses under &lt;strong&gt;heterogeneity&lt;/strong&gt;: multiple data providers, inconsistent schemas, partial updates, and unclear ownership.&lt;/p&gt;

&lt;p&gt;While building a multi-state social data platform ingesting resource data from dozens of organizations, we discovered that reliability is not a property of pipelines. It is a property of &lt;strong&gt;data artifacts and their relationships&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Why Reliability Becomes a Systems Problem at Scale
&lt;/h2&gt;

&lt;p&gt;The accompanying essay frames trust as something earned through consistent system behavior under real-world pressure. What that framing leaves implicit, but what became unavoidable in practice, is that reliability stops being a property of individual components very early on. Once multiple organizations, jurisdictions, and publishing surfaces are involved, reliability becomes an emergent property of the &lt;em&gt;entire system&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;For us, this meant that no single pipeline, connector, or database could be made "reliable enough" in isolation. Failures were rarely total. They were partial, localized, and often silent. The engineering challenge was not preventing all failure, but designing the system so that failures were isolated, detectable, and explainable.&lt;/p&gt;

&lt;p&gt;That requirement drove nearly every architectural decision that followed.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. System Overview
&lt;/h2&gt;

&lt;p&gt;Technically, the platform ingests social service resource data from independent organizations operating across multiple U.S. states. Each organization exposes data via a different source system (for example iCarol, WellSky, VisionLink, RTM), with varying schemas, update cadences, and quality guarantees.&lt;/p&gt;

&lt;p&gt;At a high level, the system consists of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Writers&lt;/strong&gt; - per-tenant ingestion and transformation projects that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetch raw data from source systems via connector adapters&lt;/li&gt;
&lt;li&gt;Persist raw data into Snowflake source schemas&lt;/li&gt;
&lt;li&gt;Normalize and standardize data via DBT&lt;/li&gt;
&lt;li&gt;Apply enhancements such as geocoding or translations&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Readers&lt;/strong&gt; - publisher processes that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React to completed writer runs, either bulk or incremental&lt;/li&gt;
&lt;li&gt;Publish curated artifacts to OpenSearch, MongoDB, and optionally Postgres&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Snowflake acts as the system of record for intermediate and normalized datasets. Dagster coordinates the execution and materialization of data assets. DBT is used explicitly for set-based transformations, not orchestration.&lt;/p&gt;

&lt;p&gt;Scale is not extreme in raw volume, but complexity is high: dozens of tenants, hundreds of tables per tenant, and frequent partial updates.&lt;/p&gt;
&lt;h3&gt;
  
  
  High-Level Architecture
&lt;/h3&gt;

&lt;p&gt;The following diagram illustrates the writer → Snowflake → DBT → asset orchestration → readers pattern.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgmvl4kvl30b7i0uowsfj.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgmvl4kvl30b7i0uowsfj.webp" alt="Asset based data orchestration high level architecture" width="800" height="216"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The platform is designed around &lt;strong&gt;asset lineage and normalization contracts&lt;/strong&gt;, not pipelines.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Source Heterogeneity as the Dominant Constraint
&lt;/h2&gt;

&lt;p&gt;The hardest constraint was not throughput or storage. It was heterogeneity.&lt;/p&gt;

&lt;p&gt;Each data provider differed along several axes simultaneously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schema shape&lt;/strong&gt;: even when nominally "the same" entities existed, fields varied&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantics&lt;/strong&gt;: identical fields often meant subtly different things&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update cadence&lt;/strong&gt;: some sources updated continuously, others weekly or ad hoc&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality guarantees&lt;/strong&gt;: missing fields, stale records, or partial exports were common&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Early on, we underestimated how strongly these differences would dominate system design. Attempting to treat ingestion as a uniform, pipeline-shaped process led to brittle assumptions and cross-tenant coupling.&lt;/p&gt;

&lt;p&gt;The system only became manageable once heterogeneity was treated as &lt;em&gt;fundamental&lt;/em&gt;, not incidental.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Normalization (HSDS, SDOH or Equivalent) as an Architectural Contract
&lt;/h2&gt;

&lt;p&gt;Normalization into an HSDS-like model was not implemented as a downstream convenience. It became an architectural contract.&lt;/p&gt;

&lt;p&gt;All downstream consumers, internal and external, implicitly rely on the guarantees of the normalized model: stable fields, predictable relationships, and documented semantics. That meant normalization could not be "best effort" or delayed until the end of a pipeline.&lt;/p&gt;

&lt;p&gt;In practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Raw source data is written verbatim into Snowflake source schemas&lt;/li&gt;
&lt;li&gt;DBT ELT projects transform this raw data into standardized intermediate models&lt;/li&gt;
&lt;li&gt;DBT STAGE projects apply tenant-specific adaptations while preserving the contract&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation made it explicit where interpretation happens. If a field is wrong in the normalized model, the question becomes &lt;em&gt;which contract was violated&lt;/em&gt;, not "what broke in the pipeline".&lt;/p&gt;
&lt;h2&gt;
  
  
  5. What We Got Wrong Initially
&lt;/h2&gt;

&lt;p&gt;Several early assumptions did not survive contact with reality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pipeline-first thinking&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
We initially modeled work as long-running jobs. This obscured which intermediate datasets were durable, reusable, or safe to depend on. Debugging often meant rerunning more than necessary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manual validation&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Data quality checks lived outside the orchestration layer. Engineers and analysts manually inspected outputs, which worked at small scale but failed under concurrency and time pressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shared failure domains&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Multiple tenants often shared execution paths. A failure in one tenant’s ingestion could block or delay others, even when their data was unrelated.&lt;/p&gt;

&lt;p&gt;None of these issues were catastrophic individually. Together, they made reliability depend on human attention.&lt;/p&gt;
&lt;h2&gt;
  
  
  6. Transitioning to Asset-Based Data Orchestration with Dagster
&lt;/h2&gt;

&lt;p&gt;The shift to asset-based orchestration was driven less by tooling preference and more by a change in mental model.&lt;/p&gt;

&lt;p&gt;Instead of asking "what jobs should run?", we started asking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What data artifacts must exist?&lt;/li&gt;
&lt;li&gt;What do they depend on?&lt;/li&gt;
&lt;li&gt;How fresh do they need to be?&lt;/li&gt;
&lt;li&gt;What constitutes success or failure for &lt;em&gt;this&lt;/em&gt; artifact?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dagster assets provided a way to encode those questions directly.&lt;/p&gt;

&lt;p&gt;A simplified example from a writer project shows how DBT models are treated as assets rather than opaque steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# writer-xyz/assets.py (excerpt)
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dagster_dbt&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_assets_from_dbt_project&lt;/span&gt;

&lt;span class="n"&gt;dbt_elt_assets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_assets_from_dbt_project&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;project_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt_elt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;profiles_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt_elt&lt;/span&gt;&lt;span class="sh"&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;This does not explain how DBT runs. It declares that the resulting tables are first-class assets with lineage and state.&lt;/p&gt;

&lt;p&gt;Once assets replaced jobs as the primary abstraction, freshness, lineage, and partial recomputation became explicit rather than implicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Partitioning for Failure Isolation
&lt;/h2&gt;

&lt;p&gt;Partitioning was critical for isolating failures.&lt;/p&gt;

&lt;p&gt;We partitioned primarily along tenant and state boundaries, not time. This reflected operational reality: data issues almost always affected a single organization or region.&lt;/p&gt;

&lt;p&gt;In Dagster terms, this meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Separate writer projects per tenant&lt;/li&gt;
&lt;li&gt;Independent schedules and sensors&lt;/li&gt;
&lt;li&gt;Asset materializations scoped to a tenant’s data domain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A failure in one writer no longer blocked publishing for others. More importantly, remediation could be targeted and auditable.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Data Quality Embedded in the Asset Graph
&lt;/h2&gt;

&lt;p&gt;Data validation moved into the asset graph itself.&lt;/p&gt;

&lt;p&gt;Instead of post-hoc checks, validations became explicit dependencies. If a validation asset failed, downstream assets simply did not materialize.&lt;/p&gt;

&lt;p&gt;An example pattern used across writers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@asset&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_staging_tables&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;staging_tables&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;staging_tables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count_missing_ids&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is intentionally simple. The key point is not the check itself, but that failure is structural. The system records that an expected artifact does not exist, rather than silently publishing bad data.&lt;/p&gt;

&lt;p&gt;This shifted failure detection earlier and reduced the blast radius of errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Operational Outcomes
&lt;/h2&gt;

&lt;p&gt;Day-to-day operations changed in several concrete ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On-call work shifted from rerunning pipelines to inspecting asset lineage&lt;/li&gt;
&lt;li&gt;Partial backfills became routine rather than exceptional&lt;/li&gt;
&lt;li&gt;Publishing delays were easier to attribute to specific upstream causes&lt;/li&gt;
&lt;li&gt;New tenants could be added without increasing shared operational risk&lt;/li&gt;
&lt;li&gt;None of this eliminated operational effort. It made that effort more focused and less reactive.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  10. Open Trade-offs and Unresolved Questions
&lt;/h2&gt;

&lt;p&gt;Some challenges remain unresolved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cross-tenant schema evolution still requires coordination and discipline&lt;/li&gt;
&lt;li&gt;Observability across Snowflake, DBT, Dagster, and downstream stores is fragmented&lt;/li&gt;
&lt;li&gt;Cost attribution at the asset level is still coarse-grained&lt;/li&gt;
&lt;li&gt;Human review remains necessary for certain semantic validations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With more time, we would invest earlier in unified observability and more formal schema versioning.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Why These Lessons Matter Beyond This Platform
&lt;/h2&gt;

&lt;p&gt;These lessons are not unique to this system.&lt;/p&gt;

&lt;p&gt;Any platform operating in civic tech, govtech, or environmental data shares similar constraints: multiple data producers, uneven quality, and real-world consequences for failure.&lt;/p&gt;

&lt;p&gt;The core takeaway is not "use asset-based orchestration", but treat data artifacts as obligations. Once that shift happens, many architectural decisions become clearer.&lt;/p&gt;

&lt;p&gt;Reliability stops being something you hope for and becomes something you can reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The biggest lesson from this platform was not about any particular tool. It was about how we model the system itself.&lt;/p&gt;

&lt;p&gt;Once data artifacts became the core abstraction, many reliability problems became easier to reason about. Failures became visible, dependencies became explicit, and operational work shifted from firefighting pipelines to managing data contracts.&lt;/p&gt;

&lt;p&gt;For data platforms operating across heterogeneous sources, this shift can be the difference between a system that merely runs - and one that can be trusted.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  What is asset-based data orchestration?
&lt;/h3&gt;

&lt;p&gt;Asset-based data orchestration treats data artifacts (tables, datasets, models) as the primary units of orchestration instead of pipelines or jobs. Systems like Dagster allow teams to define dependencies between assets, enabling better lineage tracking, partial recomputation, and failure isolation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is asset-based orchestration useful for complex data platforms?
&lt;/h3&gt;

&lt;p&gt;In large systems with many data producers and consumers, failures are rarely binary. Asset-based orchestration makes dependencies explicit, allowing teams to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;isolate failures&lt;/li&gt;
&lt;li&gt;recompute only affected datasets&lt;/li&gt;
&lt;li&gt;track lineage across transformations&lt;/li&gt;
&lt;li&gt;enforce data quality checks before publishing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How does Dagster differ from traditional pipeline orchestrators?
&lt;/h3&gt;

&lt;p&gt;Traditional orchestrators schedule jobs or workflows. Dagster emphasizes data assets and lineage. This enables better visibility into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what datasets exist&lt;/li&gt;
&lt;li&gt;what produced them&lt;/li&gt;
&lt;li&gt;what depends on them&lt;/li&gt;
&lt;li&gt;whether they meet freshness or quality requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When should teams adopt asset-based orchestration?
&lt;/h3&gt;

&lt;p&gt;Asset-based orchestration becomes particularly valuable when systems have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;many data sources&lt;/li&gt;
&lt;li&gt;heterogeneous schemas&lt;/li&gt;
&lt;li&gt;multiple downstream consumers&lt;/li&gt;
&lt;li&gt;partial or incremental updates&lt;/li&gt;
&lt;li&gt;strict reliability requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These conditions are common in &lt;strong&gt;multi-tenant data platforms and civic tech systems&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does asset-based orchestration replace tools like DBT?
&lt;/h3&gt;

&lt;p&gt;No. Asset-based orchestration &lt;strong&gt;complements transformation tools&lt;/strong&gt; like DBT. In many architectures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DBT performs &lt;strong&gt;set-based transformations&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Dagster manages &lt;strong&gt;asset lineage, dependencies, scheduling, and validation&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation keeps orchestration and transformation responsibilities clear.&lt;/p&gt;

</description>
      <category>dagster</category>
      <category>dataorchestration</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Next.js 16 Partial Prerendering (PPR): The Best of Static and Dynamic Rendering</title>
      <dc:creator>uninterrupted</dc:creator>
      <pubDate>Thu, 05 Mar 2026 08:21:46 +0000</pubDate>
      <link>https://dev.to/u11d/nextjs-16-partial-prerendering-ppr-the-best-of-static-and-dynamic-rendering-2fgg</link>
      <guid>https://dev.to/u11d/nextjs-16-partial-prerendering-ppr-the-best-of-static-and-dynamic-rendering-2fgg</guid>
      <description>&lt;p&gt;Next.js has long been a leader in giving developers flexible, high-performance rendering strategies - &lt;a href="https://u11d.com/blog/ssg-isr-ssr-csr-which-strategy-should-i-use-in-my-next-js-e-commerce-platform/" rel="noopener noreferrer"&gt;Static Site Generation (SSG), Server-Side Rendering (SSR), Incremental Static Regeneration (ISR), and Client-Side Rendering (CSR)&lt;/a&gt; all play roles depending on your use case. With Next.js 14, Partial Prerendering (PPR) was introduced as an experimental feature, starting from Next.js 16, PPR has become a stable and becomes a foundational part of this landscape by letting you&amp;nbsp;&lt;em&gt;blend static pre-rendering and dynamic behavior in the same route&lt;/em&gt;, improving perceived performance and SEO without sacrificing real-time data needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What Is Partial Prerendering (PPR)?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Partial Prerendering&lt;/strong&gt;&amp;nbsp;is a rendering strategy that lets Next.js deliver a&amp;nbsp;&lt;strong&gt;static HTML “shell” immediately&lt;/strong&gt;, then&amp;nbsp;&lt;em&gt;stream in dynamic content&lt;/em&gt;&amp;nbsp;as it becomes available - &lt;em&gt;all in the same server response&lt;/em&gt;. Instead of choosing static&amp;nbsp;&lt;em&gt;or&lt;/em&gt;&amp;nbsp;dynamic at the page level, PPR combines both on a per-component basis.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The&amp;nbsp;&lt;strong&gt;static shell&lt;/strong&gt;&amp;nbsp;(layout, logo, product titles, navigation) is pre-generated at build time or cached ahead of requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic parts&lt;/strong&gt;&amp;nbsp;(cart state, recommendations, session data) load later and are&amp;nbsp;&lt;em&gt;streamed&lt;/em&gt;&amp;nbsp;to the client using React’s&amp;nbsp;&lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;&amp;nbsp;boundaries.&lt;/li&gt;
&lt;li&gt;Users see meaningful UI immediately, with dynamic content filling in seamlessly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This leads to&amp;nbsp;&lt;em&gt;faster Time to First Byte (TTFB)&lt;/em&gt;&amp;nbsp;and a smoother perceived experience, while still allowing data personalization and real-time updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How Partial Prerendering Works in Next.js 16&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Cache Components: The Heart of PPR&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;In Next.js 16, PPR isn’t just an experimental flag - it’s integrated into the&amp;nbsp;&lt;a href="https://nextjs.org/docs/app/getting-started/cache-components" rel="noopener noreferrer"&gt;&lt;strong&gt;Cache Components&lt;/strong&gt;&lt;/a&gt;&amp;nbsp;system. Cache Components lets you&amp;nbsp;&lt;em&gt;opt-in to cache certain components or data instead of rendering them on every request&lt;/em&gt;, making pre-rendering more predictable and performant.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cacheComponents: true&lt;/code&gt;&lt;/strong&gt;&amp;nbsp;in&amp;nbsp;&lt;code&gt;next.config.ts&lt;/code&gt;&amp;nbsp;enables PPR and component-level caching.&lt;/li&gt;
&lt;li&gt;The new&amp;nbsp;&lt;a href="https://nextjs.org/docs/app/getting-started/cache-components#during-prerendering" rel="noopener noreferrer"&gt;&lt;code&gt;"use cache"&lt;/code&gt;&lt;/a&gt;&amp;nbsp;directive lets you mark data-fetching functions or components as cacheable.&lt;/li&gt;
&lt;li&gt;Anything not cacheable or depending on request-specific data can be wrapped in a&amp;nbsp;&lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;&amp;nbsp;fallback.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach gives you fine-grained control: static UI is reused across requests, while dynamic parts rehydrate or stream in only when needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Streaming and Suspense: The Engine Under the Hood&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;React’s&amp;nbsp;&lt;strong&gt;&lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;&lt;/strong&gt;&amp;nbsp;plays a key role in PPR:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wrap dynamic components in&amp;nbsp;&lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;&amp;nbsp;with a fallback UI (like a skeleton loader).&lt;/li&gt;
&lt;li&gt;During rendering, Suspense tells Next.js where to&amp;nbsp;&lt;em&gt;halt pre-rendering&lt;/em&gt;&amp;nbsp;and stream dynamic content later.&lt;/li&gt;
&lt;li&gt;The server sends the pre-rendered shell immediately and then streams dynamic sections in parallel as they’re ready.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This strategy avoids blocks in page delivery and reduces the “white screen” effect often seen in traditional SSR or CSR approaches.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why PPR Matters for E-commerce&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;For e-commerce platforms, performance directly impacts sales and SEO - factors also emphasized in the U11D articles about rendering strategies. A slow page can hurt conversions and search rankings. Partial Prerendering improves this by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Faster initial loads:&lt;/strong&gt;&amp;nbsp;Users see the page skeleton instantly, improving engagement and performance metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO advantages:&lt;/strong&gt;&amp;nbsp;Static content is available for crawler indexing immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic personalization:&lt;/strong&gt;&amp;nbsp;Cart contents, recommendations, user-specific prices or availability can appear without blocking the initial render.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More flexible than SSG/SSR alone:&lt;/strong&gt;&amp;nbsp;You’re not limited to fully static or completely dynamic pages - the best of both lives together.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In e-commerce apps where product detail pages might be static for most users but show personalized recommendations or inventory status, PPR is a compelling hybrid.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How to Enable and Use PPR in Next.js 16&lt;/strong&gt;
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Enable Cache Components:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In your&amp;nbsp;&lt;code&gt;next.config.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight tsx"&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="s1"&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;cacheComponents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// enables Partial Prerendering&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;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Wrap dynamic UI:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use React’s&amp;nbsp;&lt;code&gt;Suspense&lt;/code&gt;&amp;nbsp;for dynamic content:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Suspense&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="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Header&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;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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;Loading product...&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;span class="si"&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="nc"&gt;ProductDetails&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;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;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;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use&amp;nbsp;&lt;code&gt;use cache&lt;/code&gt;&amp;nbsp;for predictable dynamic data:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Inside dynamic data functions, prefix with&amp;nbsp;&lt;code&gt;"use cache"&lt;/code&gt;&amp;nbsp;to mark them cacheable:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use cache&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;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;This tells Next.js that caching is safe for that data.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How PPR Fits with Other Rendering Strategies&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Partial Prerendering doesn’t replace SSR, SSG, ISR, or CSR - but&amp;nbsp;&lt;em&gt;coordinates with them&lt;/em&gt;. While U11D’s articles give a great overview of traditional strategies like SSG, SSR, and ISR, PPR adds a hybrid strategy that sits between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SSG/ISR:&lt;/strong&gt;&amp;nbsp;Good for full static pages or pages that regenerate occasionally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSR:&lt;/strong&gt;&amp;nbsp;Ideal for real-time personalized content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSR:&lt;/strong&gt;&amp;nbsp;Great for highly interactive client-heavy UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PPR (Next.js 16):&lt;/strong&gt;&amp;nbsp;Combines static cached shells with streaming dynamic content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of PPR as a&amp;nbsp;&lt;em&gt;component-level SSG + dynamic hybrid -&lt;/em&gt; fast to load like SSG, flexible like SSR.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Wrapping Up&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Next.js 16 elevates Partial Prerendering from an experimental concept to a practical, integrated performance strategy through&amp;nbsp;&lt;strong&gt;Cache Components&lt;/strong&gt;&amp;nbsp;and React Suspense. It’s especially powerful for complex, dynamic sites like e-commerce stores, where you want the benefits of static pre-rendering&amp;nbsp;&lt;em&gt;and&lt;/em&gt;&amp;nbsp;dynamic personalization without sacrificing UX or SEO.&lt;/p&gt;

&lt;p&gt;By serving instantly usable HTML and streaming dynamic parts in parallel, PPR bridges the traditional divide between static and dynamic rendering - helping your app&amp;nbsp;&lt;em&gt;feel&lt;/em&gt;&amp;nbsp;faster while staying robust and scalable.&lt;/p&gt;

&lt;p&gt;If you’re building with Next.js 16, definitely explore PPR alongside other strategies like SSR, ISR, and CSR to find the most performance-optimized combination for your routes.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>ppr</category>
    </item>
    <item>
      <title>Using Proxy (before Middleware) in Next.js: a modern layer</title>
      <dc:creator>uninterrupted</dc:creator>
      <pubDate>Wed, 26 Nov 2025 07:00:00 +0000</pubDate>
      <link>https://dev.to/u11d/using-proxy-before-middleware-in-nextjs-a-modern-layer-1iik</link>
      <guid>https://dev.to/u11d/using-proxy-before-middleware-in-nextjs-a-modern-layer-1iik</guid>
      <description>&lt;p&gt;&lt;a href="https://nextjs.org/blog/next-16" rel="noopener noreferrer"&gt;Next.js 16&lt;/a&gt; introduces a new file-convention:&amp;nbsp;&lt;code&gt;proxy.js&lt;/code&gt;&amp;nbsp;/&amp;nbsp;&lt;code&gt;proxy.ts&lt;/code&gt;&amp;nbsp;(often “Proxy”) which supersedes the older&amp;nbsp;&lt;code&gt;middleware.js&lt;/code&gt;. This feature allows you to intercept HTTP requests early (before routing/rendering) and run custom logic—redirects, rewrites, header manipulation, or forwarding (proxying) to another service.&lt;/p&gt;

&lt;p&gt;In e-commerce (or any frontend/back-end scenario) this gives you a powerful tool: you can make your Next.js app act as a&amp;nbsp;&lt;strong&gt;Backend-for-Frontend (BFF)&lt;/strong&gt;&amp;nbsp;layer, hiding upstream APIs, consolidating micro-services, enforcing auth, caching, rewriting URLs, etc. This aligns well with many of the architectures discussed in your referenced articles about Next.js for commerce (SSG/ISR/SSR strategies etc).&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Proxy and when to use it
&lt;/h2&gt;

&lt;p&gt;The Proxy file (&lt;code&gt;proxy.js&lt;/code&gt;/&lt;code&gt;.ts&lt;/code&gt;) in Next.js is a special server-side file (located in the project root or under&amp;nbsp;&lt;code&gt;src&lt;/code&gt;) that runs&amp;nbsp;&lt;em&gt;before&lt;/em&gt;&amp;nbsp;any route handling or page rendering occurs. In fact, Proxy (and Middleware) executes before&amp;nbsp;&lt;strong&gt;every&lt;/strong&gt;&amp;nbsp;incoming request — not just those for pages or APIs — including requests for assets like images, icons, or JavaScript files. Because of this, Next.js provides the&amp;nbsp;&lt;code&gt;matcher&lt;/code&gt;&amp;nbsp;configuration, allowing you to specify which routes the Proxy should apply to and avoid intercepting unnecessary requests.&lt;/p&gt;

&lt;p&gt;It allows you to inspect the incoming&amp;nbsp;&lt;code&gt;request&lt;/code&gt;, then respond, redirect, rewrite, or modify headers based on your application’s needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Relation to Middleware &amp;amp; Rewrites&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In previous versions,&amp;nbsp;&lt;code&gt;middleware.js&lt;/code&gt;&amp;nbsp;(App Router) was used for this pattern; Proxy replaces it.&lt;/li&gt;
&lt;li&gt;While&amp;nbsp;&lt;code&gt;rewrites()&lt;/code&gt;&amp;nbsp;in&amp;nbsp;&lt;code&gt;next.config.js&lt;/code&gt;&amp;nbsp;can act like a proxy by mapping URL paths to &lt;a href="https://nextjs.org/docs/app/api-reference/config/next-config-js/rewrites" rel="noopener noreferrer"&gt;different destinations&lt;/a&gt;, the Proxy feature goes further — it gives you direct access to the&amp;nbsp;&lt;strong&gt;request object itself&lt;/strong&gt;, including headers, cookies, and other request data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When to use Proxy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Proxy works best in scenarios that require lightweight, real-time adjustments to incoming requests — such as performing quick redirects based on request details, dynamically rewriting routes for A/B testing or experimental features, or modifying request and response headers across specific routes or the entire application.&lt;/p&gt;

&lt;p&gt;However, it’s not well-suited for tasks involving heavy data fetching or complex session management, as those are better handled by dedicated API routes or backend services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Relation to Backend-for-Frontend (BFF) pattern&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the BFF pattern, the frontend has a dedicated backend layer (the “for the frontend”) that aggregates, transforms, or proxies requests to multiple microservices. Next.js supports this pattern via Route Handlers,&amp;nbsp;Proxy and server actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up Proxy: conventions &amp;amp; example
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;File convention&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a&amp;nbsp;&lt;code&gt;proxy.ts&lt;/code&gt;&amp;nbsp;(or&amp;nbsp;&lt;code&gt;proxy.js&lt;/code&gt;) file in the&amp;nbsp;&lt;strong&gt;project root&lt;/strong&gt;&amp;nbsp;or inside&amp;nbsp;&lt;code&gt;src/&lt;/code&gt;&amp;nbsp;alongside your top‐level&amp;nbsp;&lt;code&gt;app/&lt;/code&gt;&amp;nbsp;or&amp;nbsp;&lt;code&gt;pages/&lt;/code&gt;folder.&lt;/li&gt;
&lt;li&gt;Only one proxy file per project is supported—though you can modularize logic within it by importing helpers.&lt;/li&gt;
&lt;li&gt;Export a function named&amp;nbsp;&lt;code&gt;proxy&lt;/code&gt;&amp;nbsp;(or default) that receives a&amp;nbsp;&lt;code&gt;NextRequest&lt;/code&gt;&amp;nbsp;and returns a&amp;nbsp;&lt;code&gt;NextResponse&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Optionally export a&amp;nbsp;&lt;code&gt;config&lt;/code&gt;&amp;nbsp;object with a&amp;nbsp;&lt;code&gt;matcher&lt;/code&gt;&amp;nbsp;property to specify which routes this proxy applies to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Basic example&lt;/strong&gt;&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="c1"&gt;// proxy.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&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="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&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;NextRequest&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="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// simple redirect&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&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="s1"&gt;/home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/about/:path*&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;This intercepts&amp;nbsp;&lt;code&gt;/about/*&lt;/code&gt;&amp;nbsp;and sends users to&amp;nbsp;&lt;code&gt;/home&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proxying to different pathname&lt;/strong&gt;&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="c1"&gt;// proxy.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&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="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/v1/docs&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="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/v2/docs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&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;This way&amp;nbsp;&lt;code&gt;/v1/docs&lt;/code&gt;&amp;nbsp;in your Next.js app becomes a proxy to your &lt;code&gt;v2&lt;/code&gt; version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important flags &amp;amp; matching&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;matcher&lt;/code&gt;&amp;nbsp;can be a string, array of strings, or objects with&amp;nbsp;&lt;code&gt;source&lt;/code&gt;,&amp;nbsp;&lt;code&gt;has&lt;/code&gt;,&amp;nbsp;&lt;code&gt;missing&lt;/code&gt;,&amp;nbsp;&lt;code&gt;regexp&lt;/code&gt;&amp;nbsp;for fine control.&lt;/li&gt;
&lt;li&gt;You should exclude internal Next.js paths such as&amp;nbsp;&lt;code&gt;/_next/&lt;/code&gt;, static assets etc, to avoid intercepting everything accidentally.&lt;/li&gt;
&lt;li&gt;Be aware of runtime: The Proxy is executed by default in the Edge Runtime environment; performance matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Use cases &amp;amp; how it fits e-commerce / frontend/back-end scenarios
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use case 1: Authentication / session gating before forwarding&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before forwarding to upstream API, you can inspect cookies/headers in Proxy, validate session, add or strip auth headers, or reject unauthorized requests early. This centralises “frontend backend” logic.&lt;/p&gt;

&lt;p&gt;For example:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sessionToken&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&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;&lt;strong&gt;Use case 2: A/B testing, feature flags, routing to different backends&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;E-commerce often runs experiments: you might want to route certain requests to a “new checkout backend” or “legacy order API” based on a feature flag. Proxy allows dynamic routing:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;featureFlag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;newCheckout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/checkout&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rewrite&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="s1"&gt;/new-checkout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&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;This is exactly where Proxy shines, beyond simple rewrites. The docs mention this scenario.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use case 3: Multi-zone / micro-frontend routing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you break your site into zones (e.g.,&amp;nbsp;&lt;code&gt;/shop&lt;/code&gt;,&amp;nbsp;&lt;code&gt;/blog&lt;/code&gt;,&amp;nbsp;&lt;code&gt;/dashboard&lt;/code&gt;) and each is a separate Next.js app or separate backend service, you can use Proxy to route to the correct service based on path or condition. The docs mention this under Multi-Zones: “Proxy can also be used when there is a need for a dynamic decision when routing.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How this complements SSG/ISR/SSR strategies&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In your e-commerce context (as your referenced articles explore SSG/ISR/SSR), Proxy supports architecture by giving you control over how client-requests hit your backend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you statically generate product pages (SSG/ISR) but still need dynamic APIs behind the scenes (inventory, pricing), Proxy sits in front of those APIs.&lt;/li&gt;
&lt;li&gt;If you use SSR for checkout or account pages, you might fetch via your BFF rather than directly from client; Proxy gives you that layer.&lt;/li&gt;
&lt;li&gt;This allows you to&amp;nbsp;&lt;strong&gt;decouple&lt;/strong&gt;&amp;nbsp;the frontend build (Next.js) from the backend services, while still providing unified domain, consistent cookies/sessions, and reduction of client-side complexity.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The Proxy file convention in Next.js is a modern, flexible tool for intercepting HTTP requests early in your application lifecycle. It’s especially valuable in setups where your frontend is part of a larger ecosystem (microservices, e-commerce, feature-flagged experiments) and you need a lightweight backend-for-frontend layer without building a full custom server.&lt;/p&gt;

&lt;p&gt;For e-commerce platforms built with Next.js—where you may have static product pages, dynamic inventory/pricing APIs, customer sessions, checkout flows—the Proxy gives you an elegant place to manage domain-consolidation, routing, auth, A/B experimentation and API facade logic.&lt;/p&gt;

&lt;p&gt;However, you should use it wisely: keep logic lean, use it for the right cases (redirects, rewrites, header manipulation, request forwarding) rather than heavy data aggregation. Combined with Next.js’s rendering strategies (SSG, ISR, SSR) and BFF pattern, Proxy becomes an important architectural building block.&lt;/p&gt;

</description>
      <category>nextjs</category>
    </item>
    <item>
      <title>Automated Tax Compliance for Global E-Commerce Growth</title>
      <dc:creator>uninterrupted</dc:creator>
      <pubDate>Mon, 17 Nov 2025 08:06:29 +0000</pubDate>
      <link>https://dev.to/u11d/automated-tax-compliance-for-global-e-commerce-growth-5g8b</link>
      <guid>https://dev.to/u11d/automated-tax-compliance-for-global-e-commerce-growth-5g8b</guid>
      <description>&lt;p&gt;Choosing an e-commerce platform today means choosing your capabilities for the next 5-10 years. Most businesses focus on features and design, overlooking a critical question: can this platform handle tax compliance as we grow, regulations change, and new sales channels emerge?&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Compliance Challenges That Break Legacy Platforms
&lt;/h2&gt;

&lt;p&gt;Real-time performance matters more than you think. Every millisecond at checkout affects conversion. Legacy tax calculation systems add 300-500ms latency per request — enough to impact sales. Worse, they often fail silently during high-traffic periods, forcing you to choose between lost sales or compliance risk.&lt;/p&gt;

&lt;p&gt;Headless architecture becomes critical as your business evolves. Selling through mobile apps, marketplaces, social commerce, and IoT devices means your tax system needs API-first design. Shopify's monolithic architecture forces you into their frontend. WooCommerce wasn't built for headless operations. Magento's complexity makes omnichannel integration expensive.&lt;/p&gt;

&lt;p&gt;Audit readiness isn't optional. Tax authorities increasingly audit e-commerce businesses, and "we did our best" isn't a defense. You need complete transaction history, documentation of every tax decision, and proof that calculations were accurate at transaction time. Most platforms treat this as an afterthought, storing minimal data that won't satisfy auditors.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Performance-Compliance Tradeoff (That Shouldn't Exist)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.avalara.com/us/en/index.html" rel="noopener noreferrer"&gt;Avalara's&lt;/a&gt; real-time API calculates taxes in under 100ms, handling peak traffic without degradation. This isn't just about speed — it's about reliability. During Black Friday traffic spikes, tax calculations don't become a bottleneck.&lt;/p&gt;

&lt;p&gt;Combined with Medusa's modern architecture, you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sub-second checkout experiences with real-time tax accuracy&lt;/li&gt;
&lt;li&gt;Async tax calculation options for ultra-high-volume scenarios&lt;/li&gt;
&lt;li&gt;Automatic failover and retry logic for maximum reliability&lt;/li&gt;
&lt;li&gt;Complete transaction logging for audit trails&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Technical Foundation Matters
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://medusajs.com/" rel="noopener noreferrer"&gt;Medusa's&lt;/a&gt; API-first, headless design means tax compliance works consistently across every channel. Selling through your website, mobile app, and Amazon? Same tax logic, same accuracy, same audit trail — without custom integration for each channel.&lt;/p&gt;

&lt;p&gt;This architecture also enables sophisticated business rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customer-specific tax exemptions applied automatically across all channels&lt;/li&gt;
&lt;li&gt;Dynamic tax calculation based on real-time inventory location&lt;/li&gt;
&lt;li&gt;Split fulfillment from multiple warehouses with correct origin-based taxation&lt;/li&gt;
&lt;li&gt;International expansion without rebuilding your tax infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Building Audit-Ready From Day One
&lt;/h2&gt;

&lt;p&gt;When tax authorities audit your business, they want answers to specific questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How did you determine tax rates for each transaction?&lt;/li&gt;
&lt;li&gt;Where are exemption certificates for tax-exempt sales?&lt;/li&gt;
&lt;li&gt;How do you handle returns and refunds?&lt;/li&gt;
&lt;li&gt;What's your documentation for nexus determination?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://www.npmjs.com/package/@u11d/medusa-avalara" rel="noopener noreferrer"&gt;Medusa-Avalara integration&lt;/a&gt; maintains complete records automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every tax calculation with jurisdiction breakdown&lt;/li&gt;
&lt;li&gt;Customer exemption certificates with validation status&lt;/li&gt;
&lt;li&gt;Transaction lifecycle (calculated → committed → voided/refunded)&lt;/li&gt;
&lt;li&gt;Address validation records proving due diligence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You're not preparing for audits — you're always ready. Export complete documentation on demand, proving compliance for every transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Platform Investment Decision
&lt;/h2&gt;

&lt;p&gt;Legacy platforms force tradeoffs: Shopify is easy but inflexible. Magento is powerful but expensive and complex. WooCommerce is customizable but scales poorly.&lt;/p&gt;

&lt;p&gt;Medusa eliminates these tradeoffs: open-source flexibility, modern architecture, enterprise capabilities without enterprise costs. When paired with Avalara's tax automation, you get infrastructure that grows with your business.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scale Without Technical Debt
&lt;/h2&gt;

&lt;p&gt;Most e-commerce businesses outgrow their first platform. They've built workarounds for limitations, accumulated technical debt, and face expensive migration projects. This is avoidable.&lt;/p&gt;

&lt;p&gt;Starting with proper infrastructure—headless architecture, real-time tax automation, complete compliance documentation—means your platform grows with you instead of holding you back.&lt;/p&gt;

&lt;p&gt;Tax compliance isn't exciting, but it's foundational. Get it right from the start, and it becomes invisible infrastructure. Get it wrong, and it becomes an expensive barrier to growth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Need help with Avalara integration?
&lt;/h2&gt;

&lt;p&gt;Looking to implement automated tax compliance with Avalara in your Medusa store? u11d specializes in e-commerce integrations and tax automation solutions. We can help you set up, customize, and optimize your Avalara integration for maximum compliance and efficiency. Our team has extensive experience with both Medusa e-commerce platforms and Avalara tax systems, ensuring seamless integration that meets your specific business requirements.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://u11d.com/contact/" rel="noopener noreferrer"&gt;Contact Us&lt;/a&gt;&lt;/p&gt;

</description>
      <category>avalara</category>
      <category>medusa</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>A Practical Guide to Scaling Medusa with Kubernetes Autoscalers</title>
      <dc:creator>uninterrupted</dc:creator>
      <pubDate>Wed, 29 Oct 2025 09:56:36 +0000</pubDate>
      <link>https://dev.to/u11d/a-practical-guide-to-scaling-medusa-with-kubernetes-autoscalers-4hmj</link>
      <guid>https://dev.to/u11d/a-practical-guide-to-scaling-medusa-with-kubernetes-autoscalers-4hmj</guid>
      <description>&lt;p&gt;As your &lt;strong&gt;Medusa.js e-commerce platform&lt;/strong&gt; grows, performance and reliability depend on how well it scales under load. Kubernetes provides native tools like the &lt;strong&gt;Horizontal Pod Autoscaler (HPA)&lt;/strong&gt; and &lt;strong&gt;KEDA&lt;/strong&gt; to automatically adjust resources based on real-time demand.&lt;br&gt;
In this guide, you’ll learn how to configure &lt;strong&gt;Medusa for horizontal scaling in Kubernetes&lt;/strong&gt;, using &lt;strong&gt;Prometheus&lt;/strong&gt;, &lt;strong&gt;cAdvisor&lt;/strong&gt;, and &lt;strong&gt;HPA&lt;/strong&gt; - ensuring your store remains responsive even during peak traffic periods.&lt;/p&gt;
&lt;h2&gt;
  
  
  Prerequisites for Medusa Autoscaling on Kubernetes
&lt;/h2&gt;

&lt;p&gt;Before implementing autoscaling, ensure that your monitoring and metric systems are in place.&lt;/p&gt;
&lt;h3&gt;
  
  
  Required Tools: cAdvisor, Prometheus, and KEDA
&lt;/h3&gt;

&lt;p&gt;To make HPA and KEDA work efficiently, you’ll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;cAdvisor&lt;/strong&gt; – collects container-level CPU and memory metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prometheus&lt;/strong&gt; – scrapes, stores, and visualizes time-series metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prometheus Adapter or KEDA&lt;/strong&gt; – exposes those metrics to the HPA.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This setup is essential to achieve a reliable, responsive scaling mechanism that monitors pod and container utilization with fine-grained metric resolution (ideally 1 second or less).&lt;/p&gt;
&lt;h2&gt;
  
  
  Setting Up Metrics for HPA
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Configuring cAdvisor for container-level metrics
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;cAdvisor (Container Advisor)&lt;/strong&gt; is a running daemon that provides per-container resource usage data. It collects and exports information about all containers running on a host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key configuration tips:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoid collecting all metrics to reduce resource consumption.&lt;/li&gt;
&lt;li&gt;Adjust collection intervals according to your scaling sensitivity.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--allow_dynamic_housekeeping=false&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--housekeeping_interval=1s&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--max_housekeeping_interval=2s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;These flags ensure you get up-to-date metrics while keeping overhead low.&lt;/p&gt;
&lt;h3&gt;
  
  
  Prometheus Configuration for Scraping cAdvisor Data
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Prometheus&lt;/strong&gt; collects metrics from cAdvisor and stores them for HPA or KEDA to consume. Below is a sample configuration to scrape and relabel container metrics efficiently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;scrape_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cadvisor&lt;/span&gt;
    &lt;span class="na"&gt;scrape_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1s&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cadvisor.cadvisor.svc.cluster.local:8080"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;metric_relabel_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source_labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;container_label_io_kubernetes_pod_namespace&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;target_label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;namespace&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source_labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;container_label_io_kubernetes_pod_name&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;target_label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pod&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source_labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;container_label_io_kubernetes_container_name&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;target_label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;container&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source_labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;container_label_io_kubernetes_pod_node_name&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;target_label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;regex&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;container_label_.*&lt;/span&gt;
        &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;labeldrop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Use a 1-second scrape interval for real-time scaling accuracy, but monitor your Prometheus load — frequent scrapes can impact cluster performance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using Prometheus Adapter to Expose Custom Metrics
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Prometheus Adapter&lt;/strong&gt; acts as a bridge between Prometheus and the Kubernetes HPA. It translates Prometheus metrics into Kubernetes-readable custom metrics that HPA can act upon.&lt;/p&gt;

&lt;p&gt;Example configuration for CPU and memory metrics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;containerQuery&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;sum by (&amp;lt;&amp;lt;.GroupBy&amp;gt;&amp;gt;) (&lt;/span&gt;
        &lt;span class="s"&gt;rate(container_cpu_usage_seconds_total{container!="",&amp;lt;&amp;lt;.LabelMatchers&amp;gt;&amp;gt;}[5s])&lt;/span&gt;
      &lt;span class="s"&gt;)&lt;/span&gt;
    &lt;span class="na"&gt;nodeQuery&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;sum  by (&amp;lt;&amp;lt;.GroupBy&amp;gt;&amp;gt;) (&lt;/span&gt;
        &lt;span class="s"&gt;rate(node_cpu_seconds_total{mode!="idle",mode!="iowait",mode!="steal",&amp;lt;&amp;lt;.LabelMatchers&amp;gt;&amp;gt;}[3m])&lt;/span&gt;
      &lt;span class="s"&gt;)&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;overrides&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node&lt;/span&gt;
        &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;namespace&lt;/span&gt;
        &lt;span class="na"&gt;pod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pod&lt;/span&gt;
    &lt;span class="na"&gt;containerLabel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;container&lt;/span&gt;
  &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;containerQuery&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;sum by (&amp;lt;&amp;lt;.GroupBy&amp;gt;&amp;gt;) (&lt;/span&gt;
        &lt;span class="s"&gt;avg_over_time(container_memory_working_set_bytes{container!="",&amp;lt;&amp;lt;.LabelMatchers&amp;gt;&amp;gt;}[5s])&lt;/span&gt;
      &lt;span class="s"&gt;)&lt;/span&gt;
    &lt;span class="na"&gt;nodeQuery&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;sum by (&amp;lt;&amp;lt;.GroupBy&amp;gt;&amp;gt;) (&lt;/span&gt;
        &lt;span class="s"&gt;avg_over_time(node_memory_MemTotal_bytes{&amp;lt;&amp;lt;.LabelMatchers&amp;gt;&amp;gt;}[3m])&lt;/span&gt;
        &lt;span class="s"&gt;-&lt;/span&gt;
        &lt;span class="s"&gt;avg_over_time(node_memory_MemAvailable_bytes{&amp;lt;&amp;lt;.LabelMatchers&amp;gt;&amp;gt;}[3m])&lt;/span&gt;
      &lt;span class="s"&gt;)&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;overrides&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node&lt;/span&gt;
        &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;namespace&lt;/span&gt;
        &lt;span class="na"&gt;pod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pod&lt;/span&gt;
    &lt;span class="na"&gt;containerLabel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;container&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration exposes per-pod resource metrics that HPA can use to make scaling decisions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scaling Medusa with KEDA
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;How KEDA Integrates with Kubernetes HPA&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KEDA (Kubernetes Event-Driven Autoscaler)&lt;/strong&gt; enhances Kubernetes autoscaling by allowing scaling based on external events - for example, message queue depth, API requests, or Prometheus metrics.&lt;/p&gt;

&lt;p&gt;KEDA works alongside HPA to provide &lt;strong&gt;fine-grained, event-driven scaling&lt;/strong&gt; for your Medusa backend.&lt;/p&gt;

&lt;p&gt;Below is a sample configuration for scaling Medusa using KEDA with Prometheus as the metric source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keda.sh/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ScaledObject&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;medusa-backend&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scaleTargetRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;medusa-backend&lt;/span&gt;
  &lt;span class="na"&gt;pollingInterval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;cooldownPeriod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
  &lt;span class="na"&gt;minReplicaCount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;maxReplicaCount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;triggers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prometheus&lt;/span&gt;
      &lt;span class="na"&gt;metricType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AverageValue&lt;/span&gt;
      &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;serverAddress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;http://prometheus-server.default.svc.cluster.local:80&amp;gt;&lt;/span&gt;
        &lt;span class="na"&gt;metricName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;avg_cpu_usage&lt;/span&gt;
        &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.5"&lt;/span&gt;
        &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;sum by (pod) (rate(container_cpu_usage_seconds_total{pod=~"medusa-backend-.*", container!=""}[5s]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This YAML defines a &lt;strong&gt;ScaledObject&lt;/strong&gt; that dynamically adjusts Medusa’s replica count based on CPU usage metrics scraped by Prometheus.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fine-Tuning Kubernetes HPA
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;kube-controller-manager Parameters for Faster Scaling&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;kube-controller-manager&lt;/strong&gt; is a core Kubernetes component that controls how quickly HPA reacts to metric changes.&lt;/p&gt;

&lt;p&gt;By tuning its parameters, you can make scaling nearly instantaneous - for example, achieving a &lt;strong&gt;5-second response time&lt;/strong&gt; to CPU utilization spikes.&lt;/p&gt;

&lt;p&gt;Ensure you’ve optimized:&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="nt"&gt;--horizontal-pod-autoscaler-sync-period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lowering this interval makes the autoscaler evaluate conditions more frequently, but note that some managed Kubernetes distributions may restrict access to these flags.&lt;/p&gt;

&lt;h3&gt;
  
  
  Visualizing Scaling with Grafana
&lt;/h3&gt;

&lt;p&gt;Grafana dashboards can help you track CPU utilization, pod counts, and scaling behavior in real time.&lt;br&gt;
Below is an example visualization showing how HPA scales pods in and out based on CPU load.&lt;/p&gt;

&lt;p&gt;This visualization also highlights the importance of minimizing &lt;strong&gt;container startup and readiness probe times&lt;/strong&gt;, which directly affect how quickly new replicas become active.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo4f56aejpifi28446isw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo4f56aejpifi28446isw.webp" alt="Grafana dashboards to track CPU utilization, pod counts, and scaling behavior in real time." width="800" height="239"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices and Common Pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Optimize Startup Times and Readiness Probes&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ensure Medusa starts and becomes ready as quickly as possible.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;readiness probes&lt;/strong&gt; to signal when pods can receive traffic.&lt;/li&gt;
&lt;li&gt;Long startup times can delay scaling and degrade performance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Selecting the Right Scaling Metrics&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU utilization is common, but not always optimal.&lt;/li&gt;
&lt;li&gt;Consider custom business metrics (e.g., requests per second, queue depth).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Dealing with HPA Sync Delays&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The default HPA sync period is &lt;strong&gt;15 seconds&lt;/strong&gt;, which might be too slow for bursty workloads.&lt;/li&gt;
&lt;li&gt;Decreasing it improves responsiveness but can increase API traffic and controller load.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Implementing &lt;strong&gt;HPA and KEDA&lt;/strong&gt; for your &lt;strong&gt;Medusa.js e-commerce platform&lt;/strong&gt; ensures efficient scaling, better performance under load, and optimal resource utilization.&lt;/p&gt;

&lt;p&gt;Key takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always configure readiness probes and optimize startup times.&lt;/li&gt;
&lt;li&gt;Choose scaling metrics carefully - CPU isn’t always the best indicator.&lt;/li&gt;
&lt;li&gt;Monitor your autoscaler’s responsiveness using Grafana and Prometheus.&lt;/li&gt;
&lt;li&gt;Test your setup under realistic load scenarios to validate scaling behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the right configuration, Kubernetes can make your Medusa deployment both &lt;strong&gt;resilient and self-scaling&lt;/strong&gt;, ensuring you’re always ready for traffic spikes.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ: Scaling Medusa in Kubernetes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q1: What is the best way to scale Medusa on Kubernetes?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A1: Use Kubernetes HPA with metrics from Prometheus and cAdvisor, or event-driven scaling through KEDA for more flexibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q2: Does Medusa support autoscaling natively?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A2: Medusa itself doesn’t manage scaling, but it runs well in Kubernetes environments that use HPA or KEDA.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q3: Which metrics should I track for autoscaling?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A3: Start with CPU and memory usage; consider adding metrics like request rate, queue size, or API latency for advanced control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q4: How can I monitor Medusa’s scaling performance?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A4: Integrate Prometheus with Grafana dashboards to visualize pod utilization and replica changes in real time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q5: What’s the difference between HPA and KEDA?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A5: HPA relies on internal Kubernetes metrics, while KEDA extends it to external sources like Prometheus queries or event systems.&lt;/p&gt;

</description>
      <category>medusa</category>
      <category>kubernetes</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>Missing files in your Packer built image? You might be skipping graceful shutdowns</title>
      <dc:creator>uninterrupted</dc:creator>
      <pubDate>Thu, 23 Oct 2025 11:04:49 +0000</pubDate>
      <link>https://dev.to/u11d/missing-files-in-your-packer-built-image-you-might-be-skipping-graceful-shutdowns-4ccj</link>
      <guid>https://dev.to/u11d/missing-files-in-your-packer-built-image-you-might-be-skipping-graceful-shutdowns-4ccj</guid>
      <description>&lt;p&gt;Are your files and folders missing after you run the newly created image? This is a common issue when building images with &lt;a href="https://developer.hashicorp.com/packer" rel="noopener noreferrer"&gt;Packer&lt;/a&gt;. It is a widely used tool for creating machine images in a repeatable and automated way. When using the &lt;a href="https://developer.hashicorp.com/packer/integrations/hashicorp/qemu/latest/components/builder/qemu" rel="noopener noreferrer"&gt;&lt;code&gt;QEMU builder&lt;/code&gt;&lt;/a&gt; to generate local or CI-friendly images, it's common to upload files with the&amp;nbsp;&lt;a href="https://developer.hashicorp.com/packer/docs/provisioners/file" rel="noopener noreferrer"&gt;&lt;code&gt;file provisioner&lt;/code&gt;&lt;/a&gt; and configure systems using&amp;nbsp;&lt;a href="https://developer.hashicorp.com/packer/docs/provisioners/shell" rel="noopener noreferrer"&gt;&lt;code&gt;shell provisioner&lt;/code&gt;&lt;/a&gt;. One often overlooked but critical part of this workflow is how the virtual machine shuts down after provisioning. Without a proper shutdown, the resulting image can be unstable, incomplete, or even unbootable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Let’s see why &lt;strong&gt;graceful shutdowns&lt;/strong&gt; matter.
&lt;/h3&gt;

&lt;p&gt;After provisioning completes, &lt;a href="https://developer.hashicorp.com/packer" rel="noopener noreferrer"&gt;Packer&lt;/a&gt; snapshots the virtual disk to produce the final image. If the guest operating system is not properly shut down before this snapshot, you risk file system corruption, lost configuration changes, services failing to start, or persistent files missing entirely - especially those written during the final steps of provisioning. These issues often go unnoticed until the image is deployed in staging or production environments, where the consequences are far more disruptive.&lt;/p&gt;

&lt;p&gt;By default, the&amp;nbsp;&lt;code&gt;shutdown_command&lt;/code&gt;&amp;nbsp;is set to an empty string (&lt;code&gt;""&lt;/code&gt;). This means &lt;a href="https://developer.hashicorp.com/packer" rel="noopener noreferrer"&gt;Packer&lt;/a&gt; does not attempt a clean shutdown unless you explicitly configure one. Instead, the virtual machine is forcefully terminated, which is equivalent to abruptly cutting power on a physical machine - a common cause of data loss and image inconsistency.&lt;/p&gt;

&lt;p&gt;To prevent this, you must define a&amp;nbsp;&lt;code&gt;shutdown_command&lt;/code&gt;&amp;nbsp;that instructs the OS to shut down safely after provisioning. This ensures that services have time to stop, file operations complete, and the system reaches a stable state before the image is captured.&lt;/p&gt;

&lt;h3&gt;
  
  
  Correct usage of&amp;nbsp;&lt;code&gt;shutdown_command&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;A clean shutdown requires a properly formed command with the necessary privileges, and the exact command you use depends on the operating system inside the virtual machine. Most Linux distributions support the&amp;nbsp;&lt;code&gt;shutdown&lt;/code&gt;&amp;nbsp;command, but the syntax, required privileges, and available tools can vary slightly.&lt;/p&gt;

&lt;p&gt;Here’s a reliable example in HCL2 for a Debian/Ubuntu based image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="err"&gt;...&lt;/span&gt;
&lt;span class="nx"&gt;shutdown_command&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"echo 'packer' | sudo --stdin shutdown --poweroff now"&lt;/span&gt;
&lt;span class="err"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command assumes the use of a user with password-based&amp;nbsp;&lt;code&gt;sudo&lt;/code&gt;&amp;nbsp;access. It triggers a privileged system shutdown, allowing services to stop cleanly and file buffers to flush before power-off. The&amp;nbsp;&lt;code&gt;--poweroff&lt;/code&gt;&amp;nbsp;flag ensures the machine actually powers off, which is essential for clean termination when using virtual hardware emulation. Adjust the password to match your base image setup.&lt;/p&gt;

&lt;p&gt;If you're using a different base image - such as CentOS, RHEL, or Alpine - the shutdown binary might be located elsewhere, require different flags, or use an alternative like&amp;nbsp;&lt;code&gt;poweroff&lt;/code&gt;&amp;nbsp;or&amp;nbsp;&lt;code&gt;halt&lt;/code&gt;. You may also need to configure&amp;nbsp;&lt;code&gt;sudoers&lt;/code&gt;&amp;nbsp;to avoid prompts or password issues during provisioning.&lt;/p&gt;

&lt;p&gt;Always test the shutdown command interactively inside a VM before inserting it into your template. It should work silently and reliably in the automated provisioning context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;To sum up, graceful shutdowns are a necessary safeguard when building machine images with the &lt;a href="https://developer.hashicorp.com/packer/integrations/hashicorp/qemu/latest/components/builder/qemu" rel="noopener noreferrer"&gt;&lt;code&gt;QEMU builder&lt;/code&gt;&lt;/a&gt; in &lt;a href="https://developer.hashicorp.com/packer" rel="noopener noreferrer"&gt;Packer&lt;/a&gt;. Skipping this step or relying on Packer’s default behavior (which can forcibly terminate the VM) introduces the risk of unstable and corrupted images. By using a proper&amp;nbsp;&lt;code&gt;shutdown_command&lt;/code&gt;, you ensure the system shuts down in a controlled way, preserving the integrity of the file system and producing a clean, bootable image.&lt;/p&gt;

&lt;p&gt;This practice is simple to implement and greatly improves reliability in both development and production environments. If you're managing CI pipelines or building images for downstream automation, it's a foundational step that prevents subtle and costly failures later in the lifecycle.&lt;/p&gt;

</description>
      <category>packer</category>
      <category>qemu</category>
    </item>
    <item>
      <title>Headless CMS in E-Commerce: How to Integrate Medusa for Scalable, Content-Rich Online Stores</title>
      <dc:creator>uninterrupted</dc:creator>
      <pubDate>Wed, 01 Oct 2025 08:03:00 +0000</pubDate>
      <link>https://dev.to/u11d/headless-cms-in-e-commerce-how-to-integrate-medusa-for-scalable-content-rich-online-stores-4kkd</link>
      <guid>https://dev.to/u11d/headless-cms-in-e-commerce-how-to-integrate-medusa-for-scalable-content-rich-online-stores-4kkd</guid>
      <description>&lt;p&gt;Modern e-commerce isn’t just about selling products; it’s about delivering dynamic, content-driven experiences across multiple channels. To achieve this, businesses lean on &lt;strong&gt;Content Management Systems (CMS)&lt;/strong&gt; for structured, scalable content workflows. With the emergence of headless commerce engines like Medusa.js and modern framework like Next.js, integrating a CMS has become more streamlined and customizable than ever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining CMS in the E-Commerce Stack
&lt;/h2&gt;

&lt;p&gt;A&amp;nbsp;Content Management System (CMS)&amp;nbsp;is a tool that lets you create, edit, and organize website content without touching code.&lt;/p&gt;

&lt;p&gt;In e-commerce terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It separates&amp;nbsp;content&amp;nbsp;(product descriptions, blog posts, banners) from your&amp;nbsp;storefront&amp;nbsp;(the part customers see and shop in).&lt;/li&gt;
&lt;li&gt;Editors and marketers can add or update pages in minutes without breaking anything.&lt;/li&gt;
&lt;li&gt;Developers don’t have to be “content gatekeepers” — the CMS is the safe workspace for all non-technical updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of it as&amp;nbsp;Google Docs for your website content, but integrated into your online store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Medusa.js: Architecture and Integration
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://medusajs.com" rel="noopener noreferrer"&gt;Medusa.js&lt;/a&gt; is an open-source, Node.js-based headless commerce engine. Unlike traditional monolithic platforms (e.g. Magento, Shopify), Medusa.js separates the commerce backend from the frontend, exposing commerce logic through APIs. This architecture supports integration with any frontend framework (e.g. Next.js) or CMS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Pair Medusa.js with a CMS?
&lt;/h3&gt;

&lt;p&gt;Medusa.js powers the&amp;nbsp;transactional&amp;nbsp;side of your storefront. A CMS adds that storytelling layer, and&amp;nbsp;storefront (e.g. Next.js)&amp;nbsp;is the perfect glue that pulls both content and commerce into a seamless experience.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Blog posts&lt;/strong&gt;&amp;nbsp;— CMS stores articles, Next.js fetches them alongside Medusa product data for a fully integrated shopping-and-reading experience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Landing pages&lt;/strong&gt;&amp;nbsp;— Launch seasonal campaigns in the CMS; Next.js dynamically renders them with real-time Medusa product data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rich product descriptions&lt;/strong&gt;&amp;nbsp;— Pull long-form text, videos, and extra images from the CMS while keeping inventory and pricing from Medusa.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO improvements&lt;/strong&gt;&amp;nbsp;— Use Next.js server-side rendering (SSR) or static site generation (SSG) to ensure Google sees your content instantly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Decision Factors When Choosing a CMS
&lt;/h2&gt;

&lt;p&gt;When selecting a CMS to pair with Medusa, consider the following trade-offs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cloud (SaaS) vs. Self-Hosted&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloud (SaaS)&lt;/strong&gt;: Ideal when your scale is small to medium or when you lack in-house DevOps expertise. Fast to set up, minimal maintenance, and predictable subscription costs. However, you trade off some control, and expenses can grow significantly as traffic, content volume, or user count increases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-Hosted&lt;/strong&gt;: Full control and deep customization, with the potential for lower raw hosting costs in the long term. But infrastructure setup, deployments, scaling, security, and ongoing maintenance require time and expertise — which translates directly into operational cost.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer Friendliness vs. Ease of Use for Editors&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Some CMSs (like &lt;strong&gt;Payload&lt;/strong&gt;) are highly flexible and API-driven, offering developers full control but requiring training for non-technical editors.&lt;/li&gt;
&lt;li&gt;Others (like &lt;strong&gt;Strapi&lt;/strong&gt;) strike a balance, with strong developer features and a user-friendly admin panel.&lt;/li&gt;
&lt;li&gt;Platforms like &lt;strong&gt;Sanity&lt;/strong&gt; and &lt;strong&gt;Contentful&lt;/strong&gt; lean heavily toward editor experience, prioritizing usability and real-time collaboration.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community and Plugins&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;A strong community speeds up problem-solving and provides a library of ready-to-use plugins.&lt;/li&gt;
&lt;li&gt;For example, Strapi’s plugin ecosystem includes tools for image optimization, role-based permissions, analytics, and more.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;CMS Selection Matrix for Medusa.js Integration&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CMS&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Key Features&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Strapi&lt;/td&gt;
&lt;td&gt;Open-source (self-hosted or cloud)&lt;/td&gt;
&lt;td&gt;Customizable, large OSS community, plugin ecosystem&lt;/td&gt;
&lt;td&gt;Teams wanting balance between dev control and editor usability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payload&lt;/td&gt;
&lt;td&gt;Open-source (self-hosted or cloud)&lt;/td&gt;
&lt;td&gt;Developer-centric, clean editor, powerful APIs&lt;/td&gt;
&lt;td&gt;Dev-heavy teams building tailored workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sanity&lt;/td&gt;
&lt;td&gt;Headless SaaS&lt;/td&gt;
&lt;td&gt;Real-time collaboration, content portability&lt;/td&gt;
&lt;td&gt;Content-heavy teams with remote collaboration needs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contentful&lt;/td&gt;
&lt;td&gt;Headless SaaS&lt;/td&gt;
&lt;td&gt;Enterprise-ready, robust APIs, scalable&lt;/td&gt;
&lt;td&gt;Large organizations needing stability and global delivery&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Example Implementation: Medusa + Strapi + Next.js
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Medusa.js&lt;/strong&gt;: Manages catalog, checkout, orders, and authentication workflows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strapi&lt;/strong&gt;: Controls homepage banners, blog content, landing pages, and SEO metadata.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js&lt;/strong&gt;: Consumes APIs from both Medusa and Strapi to render the frontend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This decoupled structure allows content managers and developers to work in parallel, ensuring rapid deployment and updates across both commerce and content layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Combining a headless CMS with Medusa.js and modern storefront framework enables highly customizable, scalable, and performant e-commerce platforms. This architecture decouples content and commerce, allowing independent optimization and rapid iteration. As the industry moves toward open, API-driven systems, Medusa.js and compatible CMS platforms provide a robust foundation for next-generation digital commerce.&lt;/p&gt;

</description>
      <category>medusa</category>
      <category>strapi</category>
      <category>nextjs</category>
      <category>payload</category>
    </item>
    <item>
      <title>Kubernetes logs unavailable behind a proxy: Diagnosing API server communication issues</title>
      <dc:creator>uninterrupted</dc:creator>
      <pubDate>Wed, 06 Aug 2025 13:20:14 +0000</pubDate>
      <link>https://dev.to/u11d/kubernetes-logs-unavailable-behind-a-proxy-diagnosing-api-server-communication-issues-2af5</link>
      <guid>https://dev.to/u11d/kubernetes-logs-unavailable-behind-a-proxy-diagnosing-api-server-communication-issues-2af5</guid>
      <description>&lt;p&gt;In modern infrastructure setups, especially on-premises environments, the presence of outbound proxies is increasingly common. These proxies often enforce organizational access policies and provide observability into network traffic. While container orchestration platforms like Kubernetes can generally operate well under such constraints, some subtle and easily overlooked configuration issues can lead to unexpected behavior.&lt;/p&gt;

&lt;p&gt;In this article, I’ll walk through a real-world issue we encountered where Kubernetes pod logs were not retrievable via the API, despite the cluster appearing healthy otherwise. We'll also dive deeper into how proxy behavior interacts with Kubernetes components.&lt;/p&gt;

&lt;h3&gt;
  
  
  The symptom: cluster looks healthy, but&amp;nbsp;&lt;code&gt;kubectl logs&lt;/code&gt;&amp;nbsp;fails
&lt;/h3&gt;

&lt;p&gt;We were running a Kubernetes cluster on bare metal within a datacenter. The environment was placed behind a corporate HTTP proxy.&lt;/p&gt;

&lt;p&gt;On the surface, everything appeared operational:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;kubectl get pods&lt;/code&gt;,&amp;nbsp;&lt;code&gt;kubectl get svc&lt;/code&gt;, and other API queries worked fine,&lt;/li&gt;
&lt;li&gt;etcd connectivity was intact,&lt;/li&gt;
&lt;li&gt;Node and pod statuses reported healthy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, when attempting to retrieve logs from running pods using&amp;nbsp;&lt;code&gt;kubectl logs &amp;lt;pod&amp;gt;&lt;/code&gt;, the command failed with a timeout or a generic connection error. This was observed across all pods and namespaces.&lt;/p&gt;

&lt;h3&gt;
  
  
  Investigating the root cause
&lt;/h3&gt;

&lt;p&gt;Kubernetes retrieves pod logs through the kubelet on the node where the pod is running. The kube-apiserver acts as a reverse proxy for these requests and must establish a direct connection to the kubelet endpoint (typically via HTTPS on port 10250). This is where the problem manifested.&lt;/p&gt;

&lt;p&gt;We first validated that logs were, in fact, being generated. By SSH-ing into the node where the pod was scheduled and using&amp;nbsp;&lt;code&gt;crictl logs &amp;lt;container_id&amp;gt;&lt;/code&gt;, we could view logs without issue. This pointed to a problem not with the container runtime or logging configuration, but with how the kube-apiserver was trying to reach kubelet.&lt;/p&gt;

&lt;p&gt;Further inspection revealed that the kube-apiserver container was not bypassing the proxy for internal node-to-node communication. This wasn't a failure of the proxy settings - it was because the&amp;nbsp;&lt;code&gt;NO_PROXY&lt;/code&gt;&amp;nbsp;environment variable had never been set in the first place. In environments operating behind a proxy, this omission can critically impair the cluster's internal operations. Without an explicit&amp;nbsp;&lt;code&gt;NO_PROXY&lt;/code&gt;&amp;nbsp;configuration, even local traffic destined for internal services like the kubelet may be incorrectly routed through the proxy, leading to timeouts and unpredictable failures.&lt;/p&gt;

&lt;h3&gt;
  
  
  How HTTP proxies work in Kubernetes environments
&lt;/h3&gt;

&lt;p&gt;To understand the root of the problem, it's important to grasp how HTTP proxies interact with Kubernetes networking.&lt;/p&gt;

&lt;p&gt;When you set the environment variables&amp;nbsp;&lt;code&gt;HTTP_PROXY&lt;/code&gt;&amp;nbsp;or&amp;nbsp;&lt;code&gt;HTTPS_PROXY&lt;/code&gt;, applications on the host — including kube-apiserver, container runtime clients, and even kubelet — will route outbound traffic through the proxy server, unless explicitly told not to via&amp;nbsp;&lt;code&gt;NO_PROXY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In a Kubernetes cluster:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Most control plane components communicate over private IPs or node-local addresses,&lt;/li&gt;
&lt;li&gt;The kube-apiserver connects to the kubelet to fetch logs, execute commands, or forward ports.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the API server tries to connect to the kubelet via a private IP (e.g., 10.0.0.12) and this IP is not included in the&amp;nbsp;&lt;code&gt;NO_PROXY&lt;/code&gt;list, the request is sent to the proxy. Since proxies typically can't route to internal IPs or ports like 10250 (kubelet), the request fails silently or times out.&lt;/p&gt;

&lt;p&gt;This is further complicated on bare metal because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloud provider integrations (e.g., in EKS, GKE) often auto-configure these exemptions,&lt;/li&gt;
&lt;li&gt;Static pods don’t inherit host environment variables unless explicitly defined in their manifests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The fix: update static pod environment variables
&lt;/h3&gt;

&lt;p&gt;Because kube-apiserver was running as a static pod, its environment variables had to be defined explicitly in the corresponding manifest file, typically located at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/etc/kubernetes/manifests/kube-apiserver.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We modified this file to include proper proxy settings, specifically ensuring the&amp;nbsp;&lt;code&gt;NO_PROXY&lt;/code&gt;&amp;nbsp;variable covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All node IP addresses&lt;/li&gt;
&lt;li&gt;The cluster CIDR&lt;/li&gt;
&lt;li&gt;The service CIDR&lt;/li&gt;
&lt;li&gt;Loopback addresses&lt;/li&gt;
&lt;li&gt;Hostnames used by the control plane&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a simplified version of the environment variable section added to the static pod spec:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HTTP_PROXY&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://proxy.example.com:3128"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HTTPS_PROXY&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://proxy.example.com:3128"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NO_PROXY&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1,localhost,10.0.0.0/8,192.168.0.0/16,172.16.0.0/12,.cluster.local"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After saving the file, the kubelet automatically reloaded the manifest and restarted the kube-apiserver. Once the API server came back online,&amp;nbsp;&lt;code&gt;kubectl logs&lt;/code&gt;&amp;nbsp;began returning expected output from all pods.&lt;/p&gt;

&lt;h3&gt;
  
  
  Takeaways and best practices
&lt;/h3&gt;

&lt;p&gt;This experience reinforced the importance of configuring&amp;nbsp;&lt;code&gt;NO_PROXY&lt;/code&gt;&amp;nbsp;correctly when operating in a proxied environment, particularly on bare metal where defaults that “just work” in cloud environments may not apply.&lt;/p&gt;

&lt;p&gt;Key points to remember:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The API server needs direct access to kubelets to retrieve logs, exec into pods, and perform port-forwarding&lt;/li&gt;
&lt;li&gt;Failing to set&amp;nbsp;&lt;code&gt;NO_PROXY&lt;/code&gt;&amp;nbsp;for internal traffic can cause silent and hard-to-debug failures&lt;/li&gt;
&lt;li&gt;Static pods require environment variables to be set in the pod manifest, not in the host shell environment&lt;/li&gt;
&lt;li&gt;Use tools like&amp;nbsp;&lt;code&gt;crictl&lt;/code&gt;,&amp;nbsp;&lt;code&gt;curl&lt;/code&gt;, or&amp;nbsp;&lt;code&gt;tcpdump&lt;/code&gt;&amp;nbsp;on the control plane node to confirm where traffic is being routed&lt;/li&gt;
&lt;li&gt;Explicitly list IP ranges and DNS suffixes used within the cluster in&amp;nbsp;&lt;code&gt;NO_PROXY&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
    </item>
    <item>
      <title>Layouts in Next.js: How to Structure Your E-commerce Platform the Right Way</title>
      <dc:creator>uninterrupted</dc:creator>
      <pubDate>Mon, 04 Aug 2025 12:58:03 +0000</pubDate>
      <link>https://dev.to/u11d/layouts-in-nextjs-how-to-structure-your-e-commerce-platform-the-right-way-3g6n</link>
      <guid>https://dev.to/u11d/layouts-in-nextjs-how-to-structure-your-e-commerce-platform-the-right-way-3g6n</guid>
      <description>&lt;p&gt;The &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; 15 application router provides first-class support for layouts, making shared UI patterns seamless, efficient and composable. Here's a deeper dive into how &lt;a href="https://nextjs.org/docs/app/getting-started/layouts-and-pages" rel="noopener noreferrer"&gt;layouts&lt;/a&gt; work, their benefits and best practices.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are Layouts?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Layouts are React components that wrap pages or sub-layouts in &lt;code&gt;app/&lt;/code&gt;, rendering &lt;code&gt;children&lt;/code&gt; properties for &lt;a href="https://nextjs.org/docs/app/getting-started/layouts-and-pages#nesting-layouts" rel="noopener noreferrer"&gt;nested content&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The main layout (in &lt;code&gt;app/layout.tsx&lt;/code&gt;) is mandatory - it must contain &lt;code&gt;&amp;lt;html&amp;gt;...&amp;lt;body&amp;gt;&lt;/code&gt; tags and applies globally.&lt;/li&gt;
&lt;li&gt;Additional layouts in subdirectories (e.g., &lt;code&gt;app/categories/layout.tsx&lt;/code&gt;) only include routes in that segment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Folder-based hierarchy and nesting&lt;/p&gt;

&lt;p&gt;File system routing dictates layout behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;app/
  layout.tsx          ← root layout &lt;span class="o"&gt;(&lt;/span&gt;all routes&lt;span class="o"&gt;)&lt;/span&gt;
  page.tsx            ← homepage
  categories/
    layout.tsx        ← wraps categories routes
    page.tsx          ← /categories
    &lt;span class="o"&gt;[&lt;/span&gt;slug]/
      page.tsx        ← /categories/[slug]
  profile/
    layout.tsx        ← wraps client profile route
    page.tsx          ← /client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here root layout wraps every page, profile layout wraps only profile routes - nesting children accordingly. Wrapping a folder name in square brackets (e.g. &lt;code&gt;[slug]&lt;/code&gt;) creates a &lt;a href="https://nextjs.org/docs/app/api-reference/file-conventions/dynamic-routes" rel="noopener noreferrer"&gt;dynamic route segment&lt;/a&gt; which is used to generate multiple pages from data. (e.g. specific category pages, dedicated product pages, etc.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefits of using Layouts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Preservation of state&lt;/strong&gt;&lt;br&gt;
Since layouts persist during navigation, the state of shared components (e.g. &lt;code&gt;sidebars&lt;/code&gt;) is not lost, reducing reassembly and improving UX. This makes an ideal place to initialize global contexts. Especially useful when some of the values come from cookies or headers - helping to eliminate prop drilling and improving the maintainability of your codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Performance optimization&lt;/strong&gt;&lt;br&gt;
Layouts are server-side components by default and are cached. Client-side navigation keeps layouts live, minimizing re-rendering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Modular UI Structure&lt;/strong&gt;&lt;br&gt;
You can define different layouts for different sections of the application - e.g., &lt;code&gt;category area&lt;/code&gt; vs. &lt;code&gt;client profile&lt;/code&gt; - by nesting or using route groups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Clean code organization&lt;/strong&gt;&lt;br&gt;
Layouts enforce structure and collocated logic - styling, metadata, additional providers - all in route-specific files.&lt;/p&gt;
&lt;h2&gt;
  
  
  First steps: Basic concepts
&lt;/h2&gt;

&lt;p&gt;Defining a root layout:&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="c1"&gt;// app/layout.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Header&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/main&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Footer&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/body&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/html&lt;/span&gt;&lt;span class="err"&gt;&amp;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;This defines a globally shared UI and is required.&lt;/p&gt;

&lt;p&gt;Adding nested layouts&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="c1"&gt;// app/categories/layout.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CategoriesLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SideNav&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flex-grow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;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;Wraps &lt;code&gt;categories/&lt;/code&gt; routes, stacking inside the root layout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Main vs. nested: Only main layouts contain &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; tags. Subordinate layouts wrap inside them without duplicating these tags.&lt;/li&gt;
&lt;li&gt;Clean structure: Store &lt;code&gt;layout.tsx&lt;/code&gt;, &lt;code&gt;page.tsx&lt;/code&gt; and relevant UI components together by route segment.&lt;/li&gt;
&lt;li&gt;Selective nesting: Use nested layouts only where necessary to avoid unnecessary UI on pages.&lt;/li&gt;
&lt;li&gt;Use of server components: Default layouts are server-side - ideal for retrieving data via parameters or shared UI logic.&lt;/li&gt;
&lt;li&gt;Route groups: Create clear layout boundaries (e.g., &lt;code&gt;categories/&lt;/code&gt; vs. &lt;code&gt;profile/&lt;/code&gt;) to isolate sections - allowing for multiple layouts at the root level if needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Summary&lt;/h2&gt;

&lt;p&gt;Layouts empower you to:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Function&lt;/th&gt;
      &lt;th&gt;Description&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Shared UI&lt;/td&gt;
      &lt;td&gt;Headers, footers, navigations applied globally or segmented&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Nested UIs&lt;/td&gt;
      &lt;td&gt;Each route folder can define its own wrap layout&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Smooth state&lt;/td&gt;
      &lt;td&gt;Maintain React state in nav without full reloading&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Organized code&lt;/td&gt;
      &lt;td&gt;UI and logic co-located per route for ease of maintenance&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Define a global shell in &lt;code&gt;app/layout.tsx&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add segment-specific layouts if necessary.&lt;/li&gt;
&lt;li&gt;Organize code by route directory for modularity.&lt;/li&gt;
&lt;li&gt;Use route groups if you separate contexts with different user interfaces.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These patterns make your Next.js application more maintainable, efficient and user-friendly.&lt;/p&gt;

&lt;p&gt;Happy building!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Manage user cookie consent with Google Tag Manager: Adapting to CookieConsent v3</title>
      <dc:creator>uninterrupted</dc:creator>
      <pubDate>Mon, 14 Jul 2025 07:00:00 +0000</pubDate>
      <link>https://dev.to/u11d/manage-user-cookie-consent-with-google-tag-manager-adapting-to-cookieconsent-v3-2pl</link>
      <guid>https://dev.to/u11d/manage-user-cookie-consent-with-google-tag-manager-adapting-to-cookieconsent-v3-2pl</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;With the release of &lt;a href="https://cookieconsent.orestbida.com/" rel="noopener noreferrer"&gt;CookieConsent v3&lt;/a&gt;, we've decided to create this article to help you understand and adapt to the new version. This article builds on concepts and areas discussed in our previous post. For a deeper dive and to see the full adaptation process, please read our previous &lt;a href="https://u11d.com/blog/manage-user-cookie-consent-with-google-tag-manager-a-step-by-step-guide/" rel="noopener noreferrer"&gt;step-by-step guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding a Cookie Consent Banner
&lt;/h2&gt;

&lt;p&gt;First, let's look at the changes in the default configuration of Cookie Consent. Here's an example of the code you need to paste into &lt;strong&gt;Custom HTML&lt;/strong&gt; in &lt;strong&gt;Tag Configuration&lt;/strong&gt;:&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;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text/css"&lt;/span&gt;
  &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/gh/orestbida/cookieconsent@3.0.1/dist/cookieconsent.css"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/gh/orestbida/cookieconsent@3.0.1/dist/cookieconsent.umd.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;CookieConsent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="c1"&gt;// https://cookieconsent.orestbida.com/reference/configuration-reference.html#guioptions&lt;/span&gt;
    &lt;span class="na"&gt;guiOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;consentModal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cloud inline&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bottom left&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;equalWeightButtons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;flipButtons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;preferencesModal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;box&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;equalWeightButtons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;flipButtons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="na"&gt;onFirstConsent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// callback triggered only once on the first accept/reject action&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="na"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;necessary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// this category is enabled by default&lt;/span&gt;
        &lt;span class="na"&gt;readOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;  &lt;span class="c1"&gt;// this category cannot be disabled&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;autoClear&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;cookies&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/^_ga/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// regex: match all cookies starting with '_ga'&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_gid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// string: exact cookie name&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="c1"&gt;// https://cookieconsent.orestbida.com/reference/configuration-reference.html#category-services&lt;/span&gt;
        &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;ga&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Google Analytics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;cookies&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;_ga|_gid&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&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="na"&gt;targeting&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;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;translations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;consentModal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;We use cookies&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cookie modal description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;acceptAllBtn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accept all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;acceptNecessaryBtn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reject all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;showPreferencesBtn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Manage Individual preferences&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;preferencesModal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Manage cookie preferences&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;acceptAllBtn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accept all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;acceptNecessaryBtn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reject all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;savePreferencesBtn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accept current selection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;closeIconLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Close modal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;sections&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;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cookie usage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;We use cookies to ensure the basic functionalities of the website and to enhance your online experience ...&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="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Strictly necessary cookies&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;These cookies are essential for the proper functioning of my website. Without these cookies, the website would not work properly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;linkedCategory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;necessary&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="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Performance and Analytics cookies&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;These cookies allow the website to remember the choices you have made in the past&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;linkedCategory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;analytics&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;cookieTable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Service&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="na"&gt;expiration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Expiration&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="na"&gt;body&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_ga&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Google Analytics&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cookie set by &amp;lt;a href=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;#das&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;Google Analytics&amp;lt;/a&amp;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;expiration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Expires after 12 days&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_gid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Google Analytics&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cookie set by &amp;lt;a href=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;#das&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;Google Analytics&amp;lt;/a&amp;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;expiration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Session&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="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Targeting and Advertising&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;These cookies are used to make advertising messages more relevant to you and your interests. The intention is to display ads that are relevant and engaging for the individual user and thereby more valuable for publishers and third party advertisers.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;linkedCategory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;targeting&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="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;More information&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;For any queries in relation to my policy on cookies and your choices, please &amp;lt;a href="#contact-page"&amp;gt;contact us&amp;lt;/a&amp;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="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you preview your application through the GTM panel, you should see a banner in the bottom left corner of the page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fojtaqudux4gp497kzmhc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fojtaqudux4gp497kzmhc.png" alt="Cookie Consent Banner in the User's Browser" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring GTM Variables
&lt;/h2&gt;

&lt;p&gt;When reading a cookie, it's possible that you might receive a string instead of an object when trying to read it by name.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg9b8sck4obn5tc252rb4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg9b8sck4obn5tc252rb4.png" alt="CC Cookie string problem" width="800" height="197"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To prevent this, make sure to check the &lt;code&gt;URI-decode cookie&lt;/code&gt; box when adding a &lt;strong&gt;1st Party Cookie&lt;/strong&gt; in &lt;strong&gt;Variable Configuration&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7tj2i79yvqdjy615u4oj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7tj2i79yvqdjy615u4oj.png" alt="CC Cookie URI-decode cookie" width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Consent Variables
&lt;/h2&gt;

&lt;p&gt;The script we presented earlier for reading the selected field in the banner works correctly for selecting an entire category.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function() {
  var cookie = {{CC Cookie}}
  if (!cookie) { return 'denied'; }
  var json = JSON.parse(cookie)
  if (json["categories"].includes("analytics")) {
    return "granted";
  } else {
    return 'denied';
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this article, we'll stick with this configuration. However, Cookie Consent also allows for the configuration of acceptance for individual services if they have been added to the configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// https://cookieconsent.orestbida.com/reference/configuration-reference.html#category-services
services: {
  ga: {
    label: 'Google Analytics',
    cookies: [
      {
        name: /^(_ga|_gid)/
      }
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftwry8lcj1udjcv11vszt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftwry8lcj1udjcv11vszt.png" alt="Cookie Consent Banner services" width="800" height="697"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to create variables for individual services, replace the condition in the script. For example &lt;code&gt;json["categories"].includes("analytics")&lt;/code&gt; to &lt;code&gt;json["services"]["analytics"].includes("ga")&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Note that even if there are no services in a given category, the services object will contain an array for that category, which will be empty.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Updating GTM Consents
&lt;/h2&gt;

&lt;p&gt;Another update involves renaming fields in &lt;strong&gt;Consent Mode (Google tags)&lt;/strong&gt;. These fields now appear as follows:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkwt3r2w2pwopvm6mq25t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkwt3r2w2pwopvm6mq25t.png" alt="Consent Mode Tags" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; You can also utilize the functionality of services to assign the appropriate consent to individual fields.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Thank you for reading! For more detailed steps and additional information, make sure to check out our &lt;a href="https://u11d.com/blog/manage-user-cookie-consent-with-google-tag-manager-a-step-by-step-guide/" rel="noopener noreferrer"&gt;previous post&lt;/a&gt;. If you have any questions or need further assistance, feel free to reach out.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>privacy</category>
      <category>compliance</category>
    </item>
  </channel>
</rss>
