<?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: Ricardo Diaz Miralles</title>
    <description>The latest articles on DEV Community by Ricardo Diaz Miralles (@ricardo_diazmiralles).</description>
    <link>https://dev.to/ricardo_diazmiralles</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%2F3907989%2F8bd936ab-5447-44a8-8309-f96aaccceee7.jpg</url>
      <title>DEV Community: Ricardo Diaz Miralles</title>
      <link>https://dev.to/ricardo_diazmiralles</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ricardo_diazmiralles"/>
    <language>en</language>
    <item>
      <title>Next.js opengraph-image applies to one route, not one subtree</title>
      <dc:creator>Ricardo Diaz Miralles</dc:creator>
      <pubDate>Mon, 10 Aug 2026 17:06:11 +0000</pubDate>
      <link>https://dev.to/ricardo_diazmiralles/nextjs-opengraph-image-applies-to-one-route-not-one-subtree-b7d</link>
      <guid>https://dev.to/ricardo_diazmiralles/nextjs-opengraph-image-applies-to-one-route-not-one-subtree-b7d</guid>
      <description>&lt;p&gt;I shipped a fix for my broken link previews and made 65 of 66 pages worse. The&lt;br&gt;
build passed. Nothing warned me. The only way I found out was reading the&lt;br&gt;
compiled HTML.&lt;/p&gt;
&lt;h2&gt;
  
  
  The original bug
&lt;/h2&gt;

&lt;p&gt;Every page on the site declared this:&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;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:image"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"https://example.net/favicon.svg"&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;property=&lt;/span&gt;&lt;span class="s"&gt;"og:image:width"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"1200"&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;property=&lt;/span&gt;&lt;span class="s"&gt;"og:image:height"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"630"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An SVG. Facebook, X, LinkedIn and WhatsApp do not render SVG as &lt;code&gt;og:image&lt;/code&gt;, so&lt;br&gt;
every link anyone shared appeared with no thumbnail at all. The declared&lt;br&gt;
dimensions were fiction too — a favicon is a small square, not 1200×630.&lt;/p&gt;

&lt;p&gt;It came from a default parameter in my metadata helper that nobody had revisited:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createPageMetadata&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/favicon.svg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;lt;- here&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;I will not pretend this is a subtle one. It is a site whose entire pitch is&lt;br&gt;
"check your images before you publish them", failing at exactly that.&lt;/p&gt;
&lt;h2&gt;
  
  
  The obvious fix
&lt;/h2&gt;

&lt;p&gt;Next.js has a file convention for this. Drop &lt;code&gt;opengraph-image.tsx&lt;/code&gt; in &lt;code&gt;app/&lt;/code&gt;,&lt;br&gt;
export a function returning an &lt;code&gt;ImageResponse&lt;/code&gt;, and Next generates the PNG at&lt;br&gt;
build time and writes the tags for you:&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;// app/opengraph-image.tsx&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;ImageResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/og&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;const&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&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;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;630&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;contentType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/png&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;const&lt;/span&gt; &lt;span class="nx"&gt;alt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="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;Image&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ImageResponse&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="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* … */&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;gt;&lt;/span&gt;…&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;size&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;I added that, plus one per route for the five pages that deserved their own&lt;br&gt;
artwork. Removed the hardcoded &lt;code&gt;image&lt;/code&gt; default so nothing would override the&lt;br&gt;
generated file. &lt;code&gt;next build&lt;/code&gt; — clean. Route list showed the image routes being&lt;br&gt;
prerendered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├ ○ /opengraph-image
├ ○ /instagram-image-checker/opengraph-image
├ ○ /social-media-image-sizes/opengraph-image
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks finished. It was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually shipped
&lt;/h2&gt;

&lt;p&gt;Before deploying I grepped the compiled HTML, which is the only reason this&lt;br&gt;
story has a happy ending:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;index                          -&amp;gt; /opengraph-image?82cf03fe3b00233a
instagram-image-checker        -&amp;gt; /instagram-image-checker/opengraph-image?2e46…
social-media-image-sizes       -&amp;gt; /social-media-image-sizes/opengraph-image?d5aa…
guides/image-alt-text          -&amp;gt; NO og:image
image-seo-meta-checker         -&amp;gt; NO og:image
open-graph-image-checker       -&amp;gt; NO og:image
es/social-media-image-sizes    -&amp;gt; NO og:image
es/instagram-image-checker     -&amp;gt; NO og:image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six routes had an image. Every other page had &lt;strong&gt;no &lt;code&gt;og:image&lt;/code&gt; tag at all&lt;/strong&gt; —&lt;br&gt;
worse than the SVG, because at least the SVG was a URL some scrapers would try.&lt;/p&gt;
&lt;h2&gt;
  
  
  The controlled test
&lt;/h2&gt;

&lt;p&gt;I did not want to publish a claim I had only seen once in a messy state, so I&lt;br&gt;
reduced it. Branch, one file at the root, nothing else emitting images:&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;// app/opengraph-image.tsx  — the only image file in the project&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;Image&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ImageResponse&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="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;ROOT OG&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="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;size&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;Build, then check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/                       -&amp;gt;  /opengraph-image?29e6d2edd61b209f
/about                  -&amp;gt;  no og:image
/guides                 -&amp;gt;  no og:image
/guides/image-alt-text  -&amp;gt;  no og:image
/image-alt-text-checker -&amp;gt;  no og:image
/es/about               -&amp;gt;  no og:image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the question anyone will ask next — does a file in a segment cover that&lt;br&gt;
segment's children? Added &lt;code&gt;app/guides/opengraph-image.tsx&lt;/code&gt; and rebuilt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/guides                      -&amp;gt;  /guides/opengraph-image?321e476533b4f29e
/guides/image-alt-text       -&amp;gt;  no og:image
/guides/image-size-for-web   -&amp;gt;  no og:image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No. &lt;code&gt;app/guides/opengraph-image.tsx&lt;/code&gt; covers &lt;code&gt;/guides&lt;/code&gt; and nothing beneath it.&lt;br&gt;
The file applies to &lt;strong&gt;the one route the file sits in&lt;/strong&gt;. Next.js 16.2.4.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the mental model is wrong
&lt;/h2&gt;

&lt;p&gt;The docs are not wrong here, and it is worth being precise about that. The&lt;br&gt;
&lt;a href="https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image" rel="noopener noreferrer"&gt;API reference&lt;/a&gt;&lt;br&gt;
says the convention sets the image &lt;strong&gt;"for a route segment"&lt;/strong&gt;, and that you add&lt;br&gt;
the file to &lt;strong&gt;"any route segment"&lt;/strong&gt;. Exactly right, both times. I read straight&lt;br&gt;
past it, twice.&lt;/p&gt;

&lt;p&gt;The reason it is easy to misread is that the &lt;em&gt;other&lt;/em&gt; metadata system in App&lt;br&gt;
Router does cascade. A &lt;code&gt;metadata&lt;/code&gt; object exported from a layout merges down into&lt;br&gt;
every page beneath it — that is the whole point of putting &lt;code&gt;metadataBase&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;title.template&lt;/code&gt; and &lt;code&gt;openGraph.siteName&lt;/code&gt; in the root layout once. So you build a&lt;br&gt;
mental model where "metadata set high in the tree applies to everything below",&lt;br&gt;
and then the file convention, which lives in the same feature area and is&lt;br&gt;
described on adjacent doc pages, does not behave that way.&lt;/p&gt;

&lt;p&gt;And the failure mode is silent. A missing &lt;code&gt;og:image&lt;/code&gt; is not an error. There is no&lt;br&gt;
build warning, no type error, no runtime complaint. The page renders perfectly.&lt;br&gt;
You find out when someone shares a link and it comes out as a grey box, which&lt;br&gt;
might be weeks later.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix that scales
&lt;/h2&gt;

&lt;p&gt;The literal fix is one &lt;code&gt;opengraph-image.tsx&lt;/code&gt; per route segment. For this project&lt;br&gt;
that meant about 40 files, and doubling again for the &lt;code&gt;/es&lt;/code&gt; subtree — plus a&lt;br&gt;
matching &lt;code&gt;twitter-image.tsx&lt;/code&gt; in each, or accept that X falls back to &lt;code&gt;og:image&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So instead: one static route that serves every variant, and one default in the&lt;br&gt;
metadata helper.&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;// app/og/[slug]/route.tsx&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;createOgImage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/ogImage&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;DEFAULT_OG_SLUG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OG_VARIANTS&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/ogVariants&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;const&lt;/span&gt; &lt;span class="nx"&gt;dynamic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;force-static&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;const&lt;/span&gt; &lt;span class="nx"&gt;dynamicParams&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateStaticParams&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="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;OG_VARIANTS&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;slug&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;slug&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&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="nx"&gt;params&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="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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createOgImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;OG_VARIANTS&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="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;OG_VARIANTS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;DEFAULT_OG_SLUG&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;force-static&lt;/code&gt; plus &lt;code&gt;generateStaticParams&lt;/code&gt; means these are prerendered at build&lt;br&gt;
time exactly like the file convention would be — the route handler never runs on&lt;br&gt;
a request. Slugs carry the extension (&lt;code&gt;default.png&lt;/code&gt;, &lt;code&gt;instagram.png&lt;/code&gt;) so the URL&lt;br&gt;
looks like an image to anything that cares.&lt;/p&gt;

&lt;p&gt;Then the helper gives every page one by default:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ogImage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ogImageMeta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ogVariant&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// ogVariant defaults to "default.png"&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;openGraph&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* … */&lt;/span&gt; &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ogImage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;twitter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;summary_large_image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* … */&lt;/span&gt; &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ogImage&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;A page that wants its own passes one word:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createPageMetadata&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;Instagram Image Size Checker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/instagram-image-checker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ogVariant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;instagram.png&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;One more detail that is easy to get wrong: keep the variant catalogue in a module&lt;br&gt;
that does &lt;strong&gt;not&lt;/strong&gt; import &lt;code&gt;next/og&lt;/code&gt;. If your metadata helper imports the file that&lt;br&gt;
imports &lt;code&gt;ImageResponse&lt;/code&gt;, every page that reads metadata drags the renderer along&lt;br&gt;
with it. Catalogue and constants in one file, the &lt;code&gt;ImageResponse&lt;/code&gt; JSX in another.&lt;/p&gt;

&lt;p&gt;Result — 66 real pages, 66 with a PNG:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;og&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;image        https://publishpixel.net/og/instagram.png&lt;/span&gt;
&lt;span class="py"&gt;og&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;image:type   image/png&lt;/span&gt;
&lt;span class="py"&gt;og&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;image:width  1200&lt;/span&gt;
&lt;span class="py"&gt;og&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;image:height 630&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What this costs
&lt;/h2&gt;

&lt;p&gt;Two things you give up, and you should decide they are acceptable rather than&lt;br&gt;
discover them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You write the dimension tags yourself.&lt;/strong&gt; The file convention reads &lt;code&gt;size&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;contentType&lt;/code&gt; from your exports and emits &lt;code&gt;og:image:width&lt;/code&gt;, &lt;code&gt;og:image:height&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;og:image:type&lt;/code&gt; for free. Rolling your own route means declaring them in your&lt;br&gt;
metadata — and if you change the canvas size in one place and not the other, you&lt;br&gt;
are back to lying about dimensions, which is the bug I started with. Derive both&lt;br&gt;
from the same constant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You lose per-route &lt;code&gt;params&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;app/shop/[slug]/opengraph-image.tsx&lt;/code&gt; receives&lt;br&gt;
the route's &lt;code&gt;params&lt;/code&gt; and can generate an image from the actual product. A shared&lt;br&gt;
route cannot. If your images are genuinely per-URL — a blog with 400 posts&lt;br&gt;
rendering each title into the card — the file convention is the right tool and&lt;br&gt;
you should use it, one file in the dynamic segment, which covers every URL under&lt;br&gt;
it because they are all the same route.&lt;/p&gt;

&lt;p&gt;That is the real dividing line. &lt;strong&gt;Per-route images: file convention.&lt;/strong&gt; A handful&lt;br&gt;
of shared images across many static routes: a shared route beats 40 files.&lt;/p&gt;
&lt;h2&gt;
  
  
  How to check your own project
&lt;/h2&gt;

&lt;p&gt;After &lt;code&gt;next build&lt;/code&gt;, list the prerendered pages that have no &lt;code&gt;og:image&lt;/code&gt;:&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="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="s1"&gt;'property="og:image"'&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;find .next/server/app &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.html'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;grep -L&lt;/code&gt; prints the files without a match. Anything unexpected in that list is a&lt;br&gt;
page whose links will share as a grey box. Only covers prerendered routes, so&lt;br&gt;
check dynamic ones against a running server.&lt;/p&gt;

&lt;p&gt;Worth doing even if you never touch the file convention — the SVG default that&lt;br&gt;
started all this had been in production for months, and no tool I was using&lt;br&gt;
flagged it, because a page with a valid &lt;code&gt;og:image&lt;/code&gt; URL pointing at an unsupported&lt;br&gt;
format looks fine to a validator right up until a real scraper tries to render it.&lt;/p&gt;




&lt;p&gt;The project is &lt;a href="https://publishpixel.net" rel="noopener noreferrer"&gt;PublishPixel&lt;/a&gt;, a set of browser-based&lt;br&gt;
image checks that run locally without uploading anything — I build and maintain&lt;br&gt;
it, and the numbers above come from its production deployment. The page that&lt;br&gt;
sent me looking was the&lt;br&gt;
&lt;a href="https://publishpixel.net/open-graph-image-checker" rel="noopener noreferrer"&gt;Open Graph image checker&lt;/a&gt;,&lt;br&gt;
which teaches people to get link previews right and was, at the time, shipping&lt;br&gt;
its own preview as an SVG.&lt;/p&gt;

&lt;p&gt;If you have hit the opposite version of this — a file convention you expected to&lt;br&gt;
apply to one route and found applied to many — I would like to hear it. I now&lt;br&gt;
assume nothing in App Router cascades until I have checked the compiled HTML.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>seo</category>
      <category>react</category>
    </item>
    <item>
      <title>One line in your root layout can make every Next.js page render on demand</title>
      <dc:creator>Ricardo Diaz Miralles</dc:creator>
      <pubDate>Thu, 06 Aug 2026 17:53:44 +0000</pubDate>
      <link>https://dev.to/ricardo_diazmiralles/one-line-in-your-root-layout-can-make-every-nextjs-page-render-on-demand-24o0</link>
      <guid>https://dev.to/ricardo_diazmiralles/one-line-in-your-root-layout-can-make-every-nextjs-page-render-on-demand-24o0</guid>
      <description>&lt;p&gt;I spent a week looking at the wrong things.&lt;/p&gt;

&lt;p&gt;The site is a small Next.js 16 App Router project on Netlify. Static content,&lt;br&gt;
a few client-side tools, nothing exotic. But every response came back with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate
Cache-Status: "Netlify Durable"; fwd=bypass, "Netlify Edge"; fwd=miss
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;no-store&lt;/code&gt;. On a content site. Every request going to origin, every time.&lt;/p&gt;

&lt;p&gt;I assumed it was a Netlify configuration problem, because that is what the&lt;br&gt;
header looks like. It was not. The cause was four lines in my root layout, and&lt;br&gt;
the fix took two files.&lt;/p&gt;
&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;next build&lt;/code&gt; prints a legend at the end of the route list that is easy to skim&lt;br&gt;
past:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;○  (Static)   prerendered as static content
ƒ  (Dynamic)  server-rendered on demand
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mine looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├ ƒ /
├ ƒ /about
├ ƒ /guides
├ ƒ /guides/image-size-for-web
├ ƒ /compress-image
...
├ ○ /sitemap.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seventy-six &lt;code&gt;ƒ&lt;/code&gt;. One &lt;code&gt;○&lt;/code&gt;, and the one static route was the sitemap.&lt;/p&gt;

&lt;p&gt;Every page in the project was being server-rendered per request. Not because any&lt;br&gt;
page asked for it, but because something above them did.&lt;/p&gt;
&lt;h2&gt;
  
  
  The cause
&lt;/h2&gt;

&lt;p&gt;Here is the relevant part of my root layout:&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;headers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/headers&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="k"&gt;async&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headersList&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;headers&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;pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;headersList&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="s2"&gt;x-publishpixel-pathname&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getLocaleFromPathname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pathname&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;&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&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;html&lt;/span&gt;&lt;span class="p"&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;I needed the locale in the root layout to set &lt;code&gt;&amp;lt;html lang&amp;gt;&lt;/code&gt;, and the root layout&lt;br&gt;
is a Server Component with no access to the current path. So I had middleware&lt;br&gt;
set a header with the pathname and read it back with &lt;code&gt;headers()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It works. It is also the whole problem.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;headers()&lt;/code&gt; is a Dynamic API. Calling it opts the route into dynamic rendering,&lt;br&gt;
because the output now depends on the request. And because I called it in the&lt;br&gt;
&lt;strong&gt;root layout&lt;/strong&gt;, every route under it inherited that. One call, seventy-six&lt;br&gt;
dynamic routes.&lt;/p&gt;

&lt;p&gt;Netlify then does the correct thing for a dynamically rendered response: it&lt;br&gt;
refuses to cache it. &lt;code&gt;no-store&lt;/code&gt; was not a misconfiguration. It was Netlify&lt;br&gt;
honouring what my app told it.&lt;/p&gt;

&lt;p&gt;This is the part I want to flag, because it is what cost me the week: &lt;strong&gt;the&lt;br&gt;
header made it look like an infrastructure problem.&lt;/strong&gt; I read Netlify docs, I&lt;br&gt;
looked at &lt;code&gt;netlify.toml&lt;/code&gt;, I checked the adapter. The answer was in my own layout.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;The locale genuinely does depend on the URL. But I already had a client&lt;br&gt;
component deriving it, and I had not noticed:&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&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;usePathname&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/navigation&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;LanguageProvider&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="nx"&gt;initialLanguage&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="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;pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;usePathname&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;routeLanguage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getLocaleFromPathname&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="s2"&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLanguage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initialLanguage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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="nf"&gt;setLanguage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;routeLanguage&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="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;routeLanguage&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="nx"&gt;routeLanguage&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;usePathname&lt;/code&gt; resolves during server rendering too. In a statically generated&lt;br&gt;
route it resolves at build time, per route, because each route is prerendered&lt;br&gt;
separately. The provider was already receiving the right answer from the router.&lt;br&gt;
It was just throwing it away on the first render in favour of the&lt;br&gt;
&lt;code&gt;initialLanguage&lt;/code&gt; prop that &lt;code&gt;headers()&lt;/code&gt; was feeding it.&lt;/p&gt;

&lt;p&gt;So the state seeds from the route instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-export function LanguageProvider({ children, initialLanguage = "en" }) {
&lt;/span&gt;&lt;span class="gi"&gt;+export function LanguageProvider({ children }) {
&lt;/span&gt;   const pathname = usePathname();
   const routeLanguage = getLocaleFromPathname(pathname || "/");
&lt;span class="gd"&gt;-  const [language, setLanguage] = useState(initialLanguage);
&lt;/span&gt;&lt;span class="gi"&gt;+  const [language, setLanguage] = useState(routeLanguage);
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the root layout stops reading headers entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-export default async function RootLayout({ children }) {
-  const headersList = await headers();
-  const pathname = headersList.get("x-publishpixel-pathname") || "/";
-  const locale = getLocaleFromPathname(pathname);
-
&lt;/span&gt;&lt;span class="gi"&gt;+export default function RootLayout({ children }) {
&lt;/span&gt;   return (
&lt;span class="gd"&gt;-    &amp;lt;html lang={locale}&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+    &amp;lt;html lang="en"&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two files. The middleware also stopped propagating a header nobody read any more.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before:  1 static route,  76 dynamic
After:  77 static routes,  0 dynamic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cache-Control&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;private, no-cache, no-store&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public, max-age=0, must-revalidate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cache-Status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Netlify Durable; fwd=bypass&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Netlify Durable; hit&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFB&lt;/td&gt;
&lt;td&gt;0.34 – 0.44 s&lt;/td&gt;
&lt;td&gt;0.16 – 0.20 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Roughly half the time to first byte, on content that had not changed at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tradeoff I accepted
&lt;/h2&gt;

&lt;p&gt;The root layout now serves &lt;code&gt;lang="en"&lt;/code&gt; in the static HTML for every route,&lt;br&gt;
including the Spanish ones, and the provider corrects it on hydration.&lt;/p&gt;

&lt;p&gt;I looked at doing this properly with route groups and two root layouts&lt;br&gt;
(&lt;code&gt;app/(en)/layout.tsx&lt;/code&gt; and &lt;code&gt;app/(es)/layout.tsx&lt;/code&gt;), which is the canonical&lt;br&gt;
solution and keeps &lt;code&gt;lang&lt;/code&gt; correct in the raw HTML. I did not, because it meant&lt;br&gt;
relocating around sixty route directories for an attribute that Google has&lt;br&gt;
repeatedly said it ignores for language detection — the signals that actually&lt;br&gt;
matter are &lt;code&gt;hreflang&lt;/code&gt; and the visible content language, and both were already&lt;br&gt;
correct.&lt;/p&gt;

&lt;p&gt;Accessibility was the real concern, so the Spanish subtree gets a small layout&lt;br&gt;
that fixes the attribute before hydration:&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;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;SpanishLayout&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="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="nt"&gt;script&lt;/span&gt; &lt;span class="na"&gt;dangerouslySetInnerHTML&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;__html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;document.documentElement.lang="es"&lt;/span&gt;&lt;span class="dl"&gt;'&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;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&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;/div&gt;



&lt;p&gt;Worth being explicit that this is a compromise, not a best practice. If you are&lt;br&gt;
starting a multilingual App Router project from scratch, use route groups with&lt;br&gt;
separate root layouts and skip this entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check your own project
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;next build&lt;/code&gt; and count the &lt;code&gt;ƒ&lt;/code&gt; markers. If routes you expect to be static&lt;br&gt;
are dynamic, something is calling a Dynamic API above them. The usual suspects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;headers()&lt;/code&gt; and &lt;code&gt;cookies()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;searchParams&lt;/code&gt; in a page or layout&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;noStore()&lt;/code&gt; or &lt;code&gt;connection()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;export const dynamic = "force-dynamic"&lt;/code&gt; left over from debugging&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;fetch&lt;/code&gt; with &lt;code&gt;cache: "no-store"&lt;/code&gt; in a layout&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The higher up the tree the call is, the more it costs. In a leaf page it makes&lt;br&gt;
one route dynamic. In the root layout it makes all of them dynamic.&lt;/p&gt;

&lt;p&gt;One thing that helped me: check the &lt;strong&gt;legend&lt;/strong&gt;, not just the list. It is easy to&lt;br&gt;
read seventy route names and never notice they all carry the same marker.&lt;/p&gt;




&lt;p&gt;The project is &lt;a href="https://publishpixel.net" rel="noopener noreferrer"&gt;PublishPixel&lt;/a&gt;, a set of&lt;br&gt;
browser-based image checks that run locally without uploading anything. The&lt;br&gt;
measurements above come from its production deployment; the&lt;br&gt;
&lt;a href="https://publishpixel.net/social-media-image-sizes" rel="noopener noreferrer"&gt;social media image sizes reference&lt;/a&gt;&lt;br&gt;
is the page I was profiling when I noticed the header.&lt;/p&gt;

&lt;p&gt;If you have hit the same thing with a different Dynamic API, I would like to&lt;br&gt;
hear which one — I suspect &lt;code&gt;searchParams&lt;/code&gt; in a layout catches more people than&lt;br&gt;
&lt;code&gt;headers()&lt;/code&gt; does.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>performance</category>
      <category>react</category>
    </item>
    <item>
      <title>The pre-publish image checklist: 7 things I verify before any image goes live</title>
      <dc:creator>Ricardo Diaz Miralles</dc:creator>
      <pubDate>Fri, 03 Jul 2026 22:44:18 +0000</pubDate>
      <link>https://dev.to/ricardo_diazmiralles/the-pre-publish-image-checklist-7-things-i-verify-before-any-image-goes-live-4h1h</link>
      <guid>https://dev.to/ricardo_diazmiralles/the-pre-publish-image-checklist-7-things-i-verify-before-any-image-goes-live-4h1h</guid>
      <description>&lt;p&gt;Every developer has shipped an image that was too heavy, wrongly sized, or worse: one that still had GPS coordinates buried in its EXIF data. I did it enough times that I ended up building a checklist, and eventually a set of free browser tools around it. Here is the checklist, with both manual ways to run each check and the shortcut.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Dimensions: is it the size it will actually display at?
&lt;/h2&gt;

&lt;p&gt;Serving a 4000px-wide photo in an 800px container wastes bandwidth and hurts LCP. Check the rendered size in DevTools (hover the img in the Elements panel) and compare it with the intrinsic size. As a rule of thumb, export at 1x and 2x of the display size and let srcset do the rest.&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;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"hero-800.webp"&lt;/span&gt;
     &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"hero-800.webp 1x, hero-1600.webp 2x"&lt;/span&gt;
     &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"800"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"450"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. File size: does it fit your budget?
&lt;/h2&gt;

&lt;p&gt;For most content images, aim for under ~150 KB; hero images can justify more. If you are on Node, sharp handles resizing and compression in one pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;sharp&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sharp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input.jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1600&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;webp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;78&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;toFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hero.webp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Format: WebP/AVIF unless you have a reason not to
&lt;/h2&gt;

&lt;p&gt;JPEG/PNG still make sense for compatibility corner cases, but WebP typically cuts 25-35% at equal quality, and AVIF more. Photographs: AVIF/WebP. Logos and flat art: SVG. Screenshots: WebP or PNG.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Metadata: strip what you don't want public
&lt;/h2&gt;

&lt;p&gt;This is the one people forget. Photos from phones carry EXIF: device model, timestamps, and often GPS location. Check what's inside with exiftool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;exiftool photo.jpg | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"gps|serial|owner"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-exporting through most build pipelines (sharp, imagemin) strips EXIF, but images uploaded straight to a CMS often keep it.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Filename and alt text: the SEO part
&lt;/h2&gt;

&lt;p&gt;IMG_4032.jpg tells search engines nothing. blue-running-shoes-side-view.webp does. Same for alt text: describe the image for someone who can't see it. That serves accessibility first and image SEO as a side effect.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Social readiness: will the crop survive each platform?
&lt;/h2&gt;

&lt;p&gt;The same source image gets cropped differently by Open Graph previews (1.91:1), Instagram (4:5 or 1:1), YouTube thumbnails (16:9 with TV-safe margins)... If a campaign matters, prepare dedicated crops. I keep a &lt;a href="https://publishpixel.net/social-media-image-sizes" rel="noopener noreferrer"&gt;cheat sheet of every platform's exact pixel sizes&lt;/a&gt; because they change every year.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Open Graph: does the preview actually render?
&lt;/h2&gt;

&lt;p&gt;Before sharing, verify og:image resolves, is at least 1200x630, and isn't blocked by robots. The classic way is the Meta Sharing Debugger or curl + eyeballing the tags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;meta property="og:[^&amp;gt;]*&amp;gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The shortcut
&lt;/h2&gt;

&lt;p&gt;Running all seven checks by hand gets old fast. I built &lt;a href="https://publishpixel.net" rel="noopener noreferrer"&gt;PublishPixel&lt;/a&gt; to run them in one pass: you drop an image and it reports dimensions, weight, format fit, filename, alt-text plan, metadata (including GPS), and readiness for web/SEO/social/YouTube. Everything runs client-side in your browser: no upload, no account, files never leave your machine. It's free; it exists because of this checklist.&lt;/p&gt;

&lt;p&gt;Disclosure: PublishPixel is my project. The manual methods above work fine without it. The point is to run the checklist somehow before hitting publish.&lt;/p&gt;

&lt;p&gt;What would you add as check #8?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>performance</category>
      <category>beginners</category>
    </item>
    <item>
      <title>A warehouse supervisor's guide to shipping a SaaS in 3 weeks (with a lot of AI help)</title>
      <dc:creator>Ricardo Diaz Miralles</dc:creator>
      <pubDate>Fri, 01 May 2026 18:41:25 +0000</pubDate>
      <link>https://dev.to/ricardo_diazmiralles/a-warehouse-supervisors-guide-to-shipping-a-saas-in-3-weeks-with-a-lot-of-ai-help-4goi</link>
      <guid>https://dev.to/ricardo_diazmiralles/a-warehouse-supervisors-guide-to-shipping-a-saas-in-3-weeks-with-a-lot-of-ai-help-4goi</guid>
      <description>&lt;p&gt;Hey DEV community 👋&lt;/p&gt;

&lt;p&gt;I'm Ricardo. By day I run a warehouse in Boston, Lincolnshire (UK). By night, for the last few weeks, I've been building a SaaS with the help of AI tools — and as of this week, it's live in production with real Stripe payments.&lt;/p&gt;

&lt;p&gt;This post is partly an introduction, partly a "non-developer founder ships product in 2026" snapshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The product
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://fairtime.net" rel="noopener noreferrer"&gt;FairTime&lt;/a&gt; is a meeting scheduler that finds times across time zones and ranks options by &lt;strong&gt;attendance&lt;/strong&gt;, &lt;strong&gt;comfort&lt;/strong&gt;, and &lt;strong&gt;fairness&lt;/strong&gt; — so the same person doesn't always end up taking the 11pm or the 6am call.&lt;/p&gt;

&lt;p&gt;The reason it exists is personal. My family is scattered across Venezuela, the UK, and Spain. Every video call, someone is eating dinner late or waking up early. Almost always my mum. I wanted to build something that rotates the inconvenience instead of always defaulting to whoever complains less.&lt;/p&gt;

&lt;p&gt;Then I realised distributed teams have the same problem dressed up in calendar invites.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16&lt;/strong&gt; (App Router, Turbopack) — frontend + API routes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stripe&lt;/strong&gt; — subscriptions, with two-layer anti-trial-abuse (email + card fingerprint)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turso (libSQL)&lt;/strong&gt; — edge database for fairness history and trial tracking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind + shadcn/ui&lt;/strong&gt; — UI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Netlify&lt;/strong&gt; — hosting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bilingual EN/ES from day one — useful for hispano teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI fits in
&lt;/h2&gt;

&lt;p&gt;I am not a developer. I read code, I can edit it, I can spot when something looks wrong — but I can't write 800 lines of TypeScript from scratch. AI tools (Claude in particular) have done most of the heavy lifting on the code I couldn't write myself.&lt;/p&gt;

&lt;p&gt;What I bring: product judgement, the actual problem, attention to detail, and the willingness to push back when the AI suggests something wrong or over-engineered.&lt;/p&gt;

&lt;p&gt;What I learned: AI doesn't replace understanding. The bugs I spent the most time on were the ones where I trusted the AI's first answer instead of pushing it to explain why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I am right now
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Started early April 2026&lt;/li&gt;
&lt;li&gt;Solo founder, no investors&lt;/li&gt;
&lt;li&gt;No paying users yet (the honest number)&lt;/li&gt;
&lt;li&gt;Stripe live, anti-trial-abuse layered, OAuth verification with Google in progress&lt;/li&gt;
&lt;li&gt;Just submitted to Capterra, working on getting indexed properly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd love from you
&lt;/h2&gt;

&lt;p&gt;Honest, technical feedback on the landing page. Specifically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does the value prop land in the first 5 seconds, or do you bounce?&lt;/li&gt;
&lt;li&gt;Is the pricing page clear, or does anything feel sketchy?&lt;/li&gt;
&lt;li&gt;What's the dumbest thing you'd remove?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The free tier needs no signup, so testing the actual planner costs nothing.&lt;/p&gt;

&lt;p&gt;Also genuinely curious — anyone else here building without being a "real" developer? How do you handle the fear of imposter syndrome at 11pm when something breaks in production?&lt;/p&gt;

&lt;p&gt;Cheers from a small town in Lincolnshire 🙏&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://fairtime.net" rel="noopener noreferrer"&gt;https://fairtime.net&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
