<?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: Olivia</title>
    <description>The latest articles on DEV Community by Olivia (@olivia_342fsfsdgrere).</description>
    <link>https://dev.to/olivia_342fsfsdgrere</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%2F3390717%2Ff349b5f5-7ec6-42bd-b73b-8089c6bb0af4.jpeg</url>
      <title>DEV Community: Olivia</title>
      <link>https://dev.to/olivia_342fsfsdgrere</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/olivia_342fsfsdgrere"/>
    <language>en</language>
    <item>
      <title>Running a 4-Language Next.js 16 Site for Humans and AI Crawlers: hreflang, llms.txt, JSON-LD and IndexNow</title>
      <dc:creator>Olivia</dc:creator>
      <pubDate>Sat, 10 Oct 2026 17:17:52 +0000</pubDate>
      <link>https://dev.to/olivia_342fsfsdgrere/running-a-4-language-nextjs-16-site-for-humans-and-ai-crawlers-hreflang-llmstxt-json-ld-and-42j</link>
      <guid>https://dev.to/olivia_342fsfsdgrere/running-a-4-language-nextjs-16-site-for-humans-and-ai-crawlers-hreflang-llmstxt-json-ld-and-42j</guid>
      <description>&lt;p&gt;&lt;a href="https://www.modernwebseo.com/" rel="noopener noreferrer"&gt;&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Futbmhjkm748948fbrbmx.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Futbmhjkm748948fbrbmx.jpg" alt=" " width="800" height="447"&gt;&lt;/a&gt;  &lt;/p&gt;

&lt;p&gt;I run &lt;a href="https://www.modernwebseo.com/en" rel="noopener noreferrer"&gt;ModernWebSEO&lt;/a&gt;, a small web design and SEO studio in Istanbul. Our own site is in four languages: Turkish (the main market, at the root), English (&lt;code&gt;/en&lt;/code&gt;), Spanish (&lt;code&gt;/es&lt;/code&gt;) and Arabic (&lt;code&gt;/ar&lt;/code&gt;, right to left). The sitemap holds 700 URLs.&lt;/p&gt;

&lt;p&gt;An SEO studio's site gets judged on the details, by people and increasingly by machines: Googlebot, Bingbot, and the crawlers and fetchers behind ChatGPT, Claude, Perplexity and Gemini. This post walks through how the site handles each layer, with code from the repo. The stack is Next.js 16 (App Router), TypeScript, Tailwind CSS v4, deployed on Vercel.&lt;/p&gt;

&lt;p&gt;I'll also point out the places where the code is a compromise or simply wrong. Those are usually the useful parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Folder-per-locale routing, no i18n library
&lt;/h2&gt;

&lt;p&gt;There is no &lt;code&gt;next-intl&lt;/code&gt; and no &lt;code&gt;[locale]&lt;/code&gt; dynamic segment. Each locale is a plain folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
  page.tsx            -&amp;gt; /            (Turkish)
  hizmetler/          -&amp;gt; /hizmetler
  metodoloji/         -&amp;gt; /metodoloji
  en/
    layout.tsx
    services/         -&amp;gt; /en/services
    methodology/      -&amp;gt; /en/methodology
  es/
    servicios/        -&amp;gt; /es/servicios
    metodologia/      -&amp;gt; /es/metodologia
  ar/
    layout.tsx
    services/         -&amp;gt; /ar/services
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why folders? The URLs are translated, not just prefixed. &lt;code&gt;/hizmetler/e-ticaret&lt;/code&gt; is &lt;code&gt;/en/services/e-commerce&lt;/code&gt; in English and &lt;code&gt;/es/servicios/tienda-online&lt;/code&gt; in Spanish. Writing each locale's route tree by hand made that explicit, and each language's pages can have their own layout and copy instead of one template with swapped strings.&lt;/p&gt;

&lt;p&gt;The cost shows up in &lt;code&gt;&amp;lt;html lang&amp;gt;&lt;/code&gt;. The root layout renders &lt;code&gt;&amp;lt;html lang="tr" dir="ltr"&amp;gt;&lt;/code&gt; for every page. Reading the locale from &lt;code&gt;headers()&lt;/code&gt; in the root layout would make every route dynamic, and nothing could be served as static from the CDN. So the locale layouts set language and direction on a wrapper instead:&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/ar/layout.tsx (trimmed)&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;ArLayout&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="c1"&gt;// Root layout no longer calls headers() (static prerender).&lt;/span&gt;
  &lt;span class="c1"&gt;// &amp;lt;html&amp;gt; carries lang="tr" dir="ltr"; everything locale-specific lives here.&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;div&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"ar"&lt;/span&gt; &lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rtl"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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;cairoAr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variable&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; font-[family-name:var(--font-cairo)]`&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;/&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Arabic text renders right to left with its own font, and the page is still static. But the document-level &lt;code&gt;lang&lt;/code&gt; is wrong on non-Turkish pages, and hreflang has to carry the real language signal. If I started over, I'd use a &lt;code&gt;[lang]&lt;/code&gt; segment with &lt;code&gt;generateStaticParams&lt;/code&gt; so the root &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; could get the right attribute at build time. It's a trade-off I made knowingly, and I'd rather say so than pretend it's ideal.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. hreflang when the slugs are translated
&lt;/h2&gt;

&lt;p&gt;Translated slugs mean hreflang can't be generated by swapping a prefix. Every page needs a map. For services, the map sits next to the route:&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/en/services/[slug]/page.tsx (excerpt)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HREFLANG_BY_SLUG&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ServiceSlug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&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;e-commerce&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;tr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/hizmetler/e-ticaret&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/en/services/e-commerce&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;es&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/es/servicios/tienda-online&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/ar/services/e-commerce&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="c1"&gt;// seo, aeo, mobile-responsive ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Blog posts use a separate slug-map module (&lt;code&gt;blogSlugMapTrToEn&lt;/code&gt;, &lt;code&gt;blogSlugMapEnToTr&lt;/code&gt;, and so on), and &lt;code&gt;generateMetadata&lt;/code&gt; builds the alternates from it. &lt;code&gt;x-default&lt;/code&gt; points at the Turkish version, because that's the primary market:&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/en/blog/[slug]/page.tsx (excerpt)&lt;/span&gt;
&lt;span class="nx"&gt;alternates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/en/blog/&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="nx"&gt;languages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;blogSlugMapEnToTr&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;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="nx"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/en/blog/&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="nx"&gt;es&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/es/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;blogSlugMapEnToEs&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;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="nx"&gt;ar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/ar/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;blogSlugMapEnToAr&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;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;x-default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;blogSlugMapEnToTr&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;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="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;Each locale gets its own self-referencing canonical. The English page never canonicalizes to the Turkish one; that would tell Google to drop the translation.&lt;/p&gt;

&lt;p&gt;The same alternates also go into &lt;code&gt;app/sitemap.ts&lt;/code&gt;, using the &lt;code&gt;alternates.languages&lt;/code&gt; field that Next's &lt;code&gt;MetadataRoute.Sitemap&lt;/code&gt; supports:&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/sitemap.ts (excerpt)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;staticRouteMap&lt;/span&gt; &lt;span class="o"&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;tr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;metodoloji&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en/methodology&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;es&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;es/metodologia&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ar/methodology&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;tr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vaka-analizleri&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en/case-studies&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;es&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;es/casos-de-estudio&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ar/case-studies&lt;/span&gt;&lt;span class="dl"&gt;'&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alternates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;languages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tr&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="nf"&gt;buildUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;en&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;es&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;es&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;ar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ar&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Gotcha: two sources of hreflang can disagree.&lt;/strong&gt; While writing this post I compared page metadata against the sitemap. The Spanish methodology page lists all four languages. The English methodology page lists only &lt;code&gt;tr&lt;/code&gt; and &lt;code&gt;en&lt;/code&gt;, while the sitemap lists all four for it. hreflang has to be reciprocal, so this is the kind of mismatch Search Console reports as missing return links. If you declare alternates in two places, write a small check that diffs them, or generate both from one map.&lt;/p&gt;

&lt;p&gt;A related cleanup: posts that were once published under the wrong language prefix (an English post at &lt;code&gt;/blog/...&lt;/code&gt;) got permanent redirects in &lt;code&gt;next.config.ts&lt;/code&gt; to their correct locale path, so the old URLs don't sit in the index as duplicates.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. robots.txt as a route handler, with Content Signals
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;robots.txt&lt;/code&gt; is a route handler at &lt;code&gt;app/robots.txt/route.ts&lt;/code&gt;, not a static file, so the rules are built from arrays:&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/robots.txt/route.ts (excerpt)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CONTENT_SIGNAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search=yes, ai-input=yes, ai-train=yes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commonDisallow&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;/api/&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;/admin/&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;/private/&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;buildRules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userAgents&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;disallow&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="kr"&gt;string&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;userAgents&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ua&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="s2"&gt;`User-agent: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ua&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nContent-Signal: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;CONTENT_SIGNAL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nAllow: /\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;disallow&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;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`Disallow: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n\n&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;The named list covers the search crawlers and the AI ones: &lt;code&gt;GPTBot&lt;/code&gt;, &lt;code&gt;OAI-SearchBot&lt;/code&gt;, &lt;code&gt;ChatGPT-User&lt;/code&gt;, &lt;code&gt;ClaudeBot&lt;/code&gt;, &lt;code&gt;PerplexityBot&lt;/code&gt;, &lt;code&gt;Perplexity-User&lt;/code&gt;, &lt;code&gt;Google-Extended&lt;/code&gt;, &lt;code&gt;Applebot-Extended&lt;/code&gt;, &lt;code&gt;CCBot&lt;/code&gt;, &lt;code&gt;Amazonbot&lt;/code&gt;, &lt;code&gt;Meta-ExternalAgent&lt;/code&gt; and others. The &lt;a href="https://contentsignals.org" rel="noopener noreferrer"&gt;Content Signals&lt;/a&gt; line says what the content may be used for. The same value is also sent as an HTTP header (from &lt;code&gt;next.config.ts&lt;/code&gt; headers) and a &lt;code&gt;&amp;lt;meta name="content-signal"&amp;gt;&lt;/code&gt; tag, so a client that ignores robots.txt still sees it.&lt;/p&gt;

&lt;p&gt;Allowing training is a business decision. For a studio that sells AI search visibility, blocking the crawlers we tell clients to welcome would be odd.&lt;/p&gt;

&lt;p&gt;Honest note: a few entries in that list, like &lt;code&gt;Bard&lt;/code&gt;, &lt;code&gt;Gemini&lt;/code&gt; and &lt;code&gt;Copilot&lt;/code&gt;, are not user-agent tokens any crawler actually sends. They are harmless, but they are noise. The real control for Google's AI use is &lt;code&gt;Google-Extended&lt;/code&gt;. If you copy a list like this, check each name against the vendor's documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. llms.txt, and Markdown for agents that ask for it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;public/llms.txt&lt;/code&gt; is a hand-maintained Markdown file: a short blockquote saying what the business is, then sections for services, pricing, tools, the 91 profession guide pages and blog posts per language. A longer &lt;code&gt;llms-full.txt&lt;/code&gt; sits next to it.&lt;/p&gt;

&lt;p&gt;One habit I'd recommend for multilingual sites: say which language a linked page is in. Several service pages only exist in Turkish, so their lines end with "(Service page in Turkish.)". A model summarizing the site for an English speaker then knows not to promise an English page that isn't there.&lt;/p&gt;

&lt;p&gt;Discovery is handled in the root layout's &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;meta&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"content-signal"&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"search=yes, ai-input=yes, ai-train=yes"&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;link&lt;/span&gt; &lt;span class="na"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text/markdown"&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/llms.txt"&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"LLM Context"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and &lt;code&gt;next.config.ts&lt;/code&gt; redirects &lt;code&gt;/.well-known/llms.txt&lt;/code&gt; to &lt;code&gt;/llms.txt&lt;/code&gt; for clients that look there.&lt;/p&gt;

&lt;p&gt;Agents that send &lt;code&gt;Accept: text/markdown&lt;/code&gt; (or request a &lt;code&gt;.md&lt;/code&gt; path) get Markdown instead of HTML. The middleware rewrites those requests to an API route:&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;// middleware.ts (excerpt)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;accept&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;headers&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;accept&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isMarkdownRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/markdown&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;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.md&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="o"&gt;!&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;/api/&lt;/span&gt;&lt;span class="dl"&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;isMarkdownRequest&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;markdownUrl&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;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;/api/markdown&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="nx"&gt;markdownUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&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="nx"&gt;markdownUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&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="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Signal&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;search=yes, ai-input=yes, ai-train=yes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&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="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Vary&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;Accept&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;res&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;/api/markdown&lt;/code&gt; resolves the path: the root returns &lt;code&gt;llms.txt&lt;/code&gt;, a blog URL returns the post's Markdown body with a small front matter block (title, author, dates, canonical URL) and its FAQ appended, and profession and case study pages get the same treatment. Because the source content already lives in TypeScript data files, the Markdown comes from the same data as the HTML instead of a second copy.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Vary: Accept&lt;/code&gt; is set on both branches so a cache doesn't hand the Markdown version to a browser. Check the real headers with &lt;code&gt;curl -I&lt;/code&gt; after deploying; frameworks and CDNs sometimes rewrite &lt;code&gt;Vary&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. One JSON-LD graph, linked by &lt;a class="mentioned-user" href="https://dev.to/id"&gt;@id&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;The site-wide structured data is a single &lt;code&gt;&amp;lt;script type="application/ld+json"&amp;gt;&lt;/code&gt; with an &lt;code&gt;@graph&lt;/code&gt;. The nodes reference each other by &lt;code&gt;@id&lt;/code&gt; instead of repeating themselves:&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;// components/seo/structured-data.tsx (trimmed)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&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;@type&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;Organization&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;@id&lt;/span&gt;&lt;span class="dl"&gt;'&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;SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/#organization`&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="nx"&gt;SITE_CONFIG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SITE_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;foundingDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2020&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;founder&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;@id&lt;/span&gt;&lt;span class="dl"&gt;'&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;SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/#founder`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;contactPoint&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;@type&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;ContactPoint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;availableLanguage&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;Turkish&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;English&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;Spanish&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;Arabic&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;sameAs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SITE_CONFIG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sameAs&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@type&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;Person&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;@id&lt;/span&gt;&lt;span class="dl"&gt;'&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;SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/#founder`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;worksFor&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;@id&lt;/span&gt;&lt;span class="dl"&gt;'&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;SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/#organization`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;sameAs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SITE_CONFIG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;founder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sameAs&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@type&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;WebSite&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;@id&lt;/span&gt;&lt;span class="dl"&gt;'&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;SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/#website`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;publisher&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;@id&lt;/span&gt;&lt;span class="dl"&gt;'&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;SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/#organization`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;inLanguage&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;tr-TR&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;en-US&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;es-ES&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;ar-AR&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="nx"&gt;extraNodes&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;sameAs&lt;/code&gt; lives in one config file (&lt;code&gt;lib/config.ts&lt;/code&gt;) and includes the Wikidata entry, so every profile link is maintained once. That matters for entity recognition: Google and AI tools connect "ModernWebSEO" across platforms through those links.&lt;/p&gt;

&lt;p&gt;Page-level schema then sets its own language. The English GEO service page declares &lt;code&gt;inLanguage: 'en-US'&lt;/code&gt; on its Service node and adds an FAQPage in English. Schema in the page's language, on the page in that language, is the rule.&lt;/p&gt;

&lt;p&gt;Two things I'd fix here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ar-AR&lt;/code&gt; reads as "Arabic, Argentina", because &lt;code&gt;AR&lt;/code&gt; is Argentina's country code. Plain &lt;code&gt;ar&lt;/code&gt; (or a real target country, like &lt;code&gt;ar-SA&lt;/code&gt;) would be correct.&lt;/li&gt;
&lt;li&gt;The global WebPage and Article nodes use &lt;code&gt;dateModified: new Date().toISOString()&lt;/code&gt;, so the date changes on every build. That contradicts the rule I follow in the sitemap (next section). A modified date should change when the content changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. IndexNow after every build, without lying about lastmod
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.indexnow.org" rel="noopener noreferrer"&gt;IndexNow&lt;/a&gt; lets you tell Bing, Yandex and the other participating engines that a URL changed. Google doesn't use it. The site pings it from npm's &lt;code&gt;postbuild&lt;/code&gt; hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"next build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"postbuild"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsx scripts/submit-indexnow.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"indexnow:manual"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsx scripts/submit-indexnow.ts"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script fetches the sitemap (which covers all four locales), keeps only URLs whose &lt;code&gt;lastmod&lt;/code&gt; falls in the last 24 hours, and posts them in batches:&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;// lib/indexnow/client.ts (excerpt)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API_ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.indexnow.org/indexnow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BATCH_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BATCH_DELAY_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2000&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="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;keyLocation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;host&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;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.txt`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;urlList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// summarized from submitBatch():&lt;/span&gt;
&lt;span class="c1"&gt;// 200/202 = ok, 429 = wait 60s and retry once,&lt;/span&gt;
&lt;span class="c1"&gt;// 403 = key file not reachable, 422 = host mismatch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key file lives in &lt;code&gt;public/&lt;/code&gt;, named after the key. A failure logs and moves on; the script deliberately never calls &lt;code&gt;process.exit(1)&lt;/code&gt;, because a search engine ping should never break a deploy.&lt;/p&gt;

&lt;p&gt;The 24-hour filter only works if &lt;code&gt;lastmod&lt;/code&gt; is honest. That is the most important line in the sitemap file:&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/sitemap.ts (excerpt)&lt;/span&gt;
&lt;span class="cm"&gt;/**
 * Section content dates (comment translated from Turkish).
 * Build time (new Date()) is NOT used:
 * telling Google the whole site changed on every deploy breaks
 * its trust in lastmod, and pushes every URL into the postbuild
 * IndexNow submission (last-24-hours filter).
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CONTENT_DATES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;core&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-06-19&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;localizedServices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-07-31&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;hazirSite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-10-09&lt;/span&gt;&lt;span class="dl"&gt;'&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;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Blog posts use their own &lt;code&gt;updatedAt&lt;/code&gt; or &lt;code&gt;publishedAt&lt;/code&gt;. A listing page uses the date of its newest post. When I change a section's content, I change its date by hand. It's manual, and that's the point: the date means something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha: postbuild sees the old sitemap.&lt;/strong&gt; &lt;code&gt;postbuild&lt;/code&gt; runs while the new deployment is still building, and the script fetches the sitemap from the live domain. That is the previous deployment's sitemap. A page added in this deploy isn't in it yet. For changes that matter, I run &lt;code&gt;npm run indexnow:manual&lt;/code&gt; after the deploy is live.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist you can steal
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Translated slugs need an explicit locale map. Generate page alternates and sitemap alternates from the same map, or diff them in CI.&lt;/li&gt;
&lt;li&gt;Self-referencing canonical per locale. Never canonicalize a translation to the original.&lt;/li&gt;
&lt;li&gt;If you skip a &lt;code&gt;[lang]&lt;/code&gt; segment to stay static, put &lt;code&gt;lang&lt;/code&gt; and &lt;code&gt;dir&lt;/code&gt; on a wrapper and know what you gave up.&lt;/li&gt;
&lt;li&gt;robots.txt: name the AI crawlers you want, and verify every name exists.&lt;/li&gt;
&lt;li&gt;llms.txt: say what the site is in the first lines, and mark pages that only exist in one language.&lt;/li&gt;
&lt;li&gt;Markdown negotiation: &lt;code&gt;Vary: Accept&lt;/code&gt;, then check the real headers.&lt;/li&gt;
&lt;li&gt;JSON-LD: one graph, &lt;code&gt;@id&lt;/code&gt; references, &lt;code&gt;inLanguage&lt;/code&gt; per page, real modified dates.&lt;/li&gt;
&lt;li&gt;IndexNow: honest &lt;code&gt;lastmod&lt;/code&gt; first, then automate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this maps to the technical and GEO pillars of the &lt;a href="https://www.modernwebseo.com/en/methodology" rel="noopener noreferrer"&gt;47-step methodology&lt;/a&gt; we use on client sites. If you want the background on the AI side, I wrote up &lt;a href="https://www.modernwebseo.com/en/blog/what-is-llms-txt-ai-optimization-guide" rel="noopener noreferrer"&gt;what llms.txt is and how to write one&lt;/a&gt;, and there's a free &lt;a href="https://www.modernwebseo.com/en/tools/robots-txt-generator" rel="noopener noreferrer"&gt;robots.txt generator with AI crawler rules&lt;/a&gt; on the site. The &lt;a href="https://www.modernwebseo.com/en/services/geo" rel="noopener noreferrer"&gt;GEO service page&lt;/a&gt; covers how we apply it for clients, and the &lt;a href="https://ismailgunaydn.github.io/ModernWebSEO/" rel="noopener noreferrer"&gt;ModernWebSEO hub&lt;/a&gt; links everything else.&lt;/p&gt;

&lt;p&gt;You can also look at the live files: &lt;a href="https://www.modernwebseo.com/llms.txt" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt; and &lt;a href="https://www.modernwebseo.com/robots.txt" rel="noopener noreferrer"&gt;robots.txt&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Questions about the hreflang maps or the Markdown route, ask in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>chatgpt</category>
      <category>claude</category>
      <category>gemini</category>
    </item>
    <item>
      <title>A 250-Product Catalog With No Database: Static Next.js 16, a WhatsApp Cart and One Price JSON</title>
      <dc:creator>Olivia</dc:creator>
      <pubDate>Fri, 02 Oct 2026 21:56:40 +0000</pubDate>
      <link>https://dev.to/olivia_342fsfsdgrere/a-250-product-catalog-with-no-database-static-nextjs-16-a-whatsapp-cart-and-one-price-json-a0e</link>
      <guid>https://dev.to/olivia_342fsfsdgrere/a-250-product-catalog-with-no-database-static-nextjs-16-a-whatsapp-cart-and-one-price-json-a0e</guid>
      <description>&lt;p&gt;&lt;a href="https://www.ledsusleme.com" rel="noopener noreferrer"&gt;ledsusleme.com&lt;/a&gt; sells outdoor LED decorations in Turkish: pole motifs for street lamps, light-up figures, arches, hanging ornaments and LED by the metre. The catalog has 250 products across 32 categories and 750 SKUs. Every SKU has a visible price.&lt;/p&gt;

&lt;p&gt;There is no database. There is no checkout, no payment form, no user account and no admin panel. The site is a static Next.js 16 build on Vercel, and every order closes in a WhatsApp chat.&lt;/p&gt;

&lt;p&gt;That sounds like a shortcut. For this business it is the right shape. Every item is made to order in a workshop, shipping depends on the size of the piece and the destination city, and installation is quoted separately. A checkout would have to pretend those numbers are known up front. A WhatsApp conversation does not.&lt;/p&gt;

&lt;p&gt;This post walks through the parts that make it work, with short excerpts from the real code. Identifiers and comments are partly Turkish; I translate as I go.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Catalog:&lt;/strong&gt; JSON files in &lt;code&gt;data/catalog/&lt;/code&gt;, typed and served through one access module. Every page is statically generated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prices:&lt;/strong&gt; one file, &lt;code&gt;data/fiyatlar.json&lt;/code&gt;, keyed by SKU, storing the net price (VAT excluded). The VAT-included figure shown on the page is computed at build time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cart:&lt;/strong&gt; SKU, quantity and metres in &lt;code&gt;localStorage&lt;/code&gt;. Prices are never stored in the cart; they are looked up on every render.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Order:&lt;/strong&gt; the cart becomes a pre-filled &lt;code&gt;wa.me&lt;/code&gt; message, with a compact fallback when the encoded URL gets too long.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI agents:&lt;/strong&gt; the same URL returns Markdown when the client sends &lt;code&gt;Accept: text/markdown&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content gate:&lt;/strong&gt; a prebuild script fails the build when product copy contains a number that is not in the product's own data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured data:&lt;/strong&gt; &lt;code&gt;ProductGroup&lt;/code&gt; with one &lt;code&gt;Offer&lt;/code&gt; per variant, priced with the same VAT-included number the page shows.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why no database?
&lt;/h2&gt;

&lt;p&gt;The catalog changes a few times a year. Prices change when exchange rates move. Nothing changes per user, and nothing has to be written at request time. That is a static site's job description.&lt;/p&gt;

&lt;p&gt;The dependency list in &lt;code&gt;package.json&lt;/code&gt; is three packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"16.3.8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"react"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"19.3.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"react-dom"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"19.3.0"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No ORM, no CMS client, no validation library, no UI kit. Data lives in the repository as JSON and TypeScript, and a change to the catalog is a commit that goes through the same build as a code change. That build is where the guard rails live.&lt;/p&gt;

&lt;h2&gt;
  
  
  One price file, net in storage, gross on the page
&lt;/h2&gt;

&lt;p&gt;Turkish price labelling rules expect the consumer price, VAT included, as the headline figure. The business thinks in net prices. So the file stores net, and everything else is derived from it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;data/fiyatlar.json&lt;/code&gt; holds one entry per SKU:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"LS-01001"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"net"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;24500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"approved"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"adet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"updatedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-02"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;unit&lt;/code&gt; is either &lt;code&gt;"adet"&lt;/code&gt; (piece) or &lt;code&gt;"m"&lt;/code&gt; (metre). The file is produced by a Python script, and a person approves the numbers in a CSV before they land here. Nobody types a price into a page.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;lib/pricing.ts&lt;/code&gt; is the only reader:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;FIYATLAR&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;@/data/fiyatlar.json&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;site&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;@/data/site&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/** KDV dahil tutar tam liraya yuvarlanır. */&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;gross&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;net&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&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;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="nx"&gt;net&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;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;terms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vatRate&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;function&lt;/span&gt; &lt;span class="nf"&gt;getPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sku&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;Price&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;const&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;sku&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;e&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&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;net&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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;net&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;const&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gross&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;net&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="na"&gt;net&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;net&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gross&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;vat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&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;net&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;unit&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;unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;updatedAt&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;updatedAt&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;vatRate&lt;/code&gt; is &lt;code&gt;0.2&lt;/code&gt;, defined once in &lt;code&gt;data/site.ts&lt;/code&gt; next to the warranty length and the return window. A missing or non-positive price returns &lt;code&gt;null&lt;/code&gt;, and the page falls back to "ask on WhatsApp" instead of rendering zero.&lt;/p&gt;

&lt;p&gt;The page then shows the gross figure large and the net figure under it, followed by one fixed line: installation, removal and shipping are not included. That line is not optional copy. It sits in the price component, so no product can ship without it.&lt;/p&gt;

&lt;p&gt;Line totals handle the two units with one branch:&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="cm"&gt;/** Satır tutarı: adet ürününde birim x adet, metre ürününde birim x metre x adet. */&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;lineTotal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;meters&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;net&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;gross&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;factor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unit&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;m&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;meters&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="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;qty&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;qty&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;net&lt;/span&gt; &lt;span class="o"&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="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;net&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;factor&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;net&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gross&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;gross&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;net&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;h2&gt;
  
  
  A cart that holds no prices
&lt;/h2&gt;

&lt;p&gt;The cart lives in &lt;code&gt;localStorage&lt;/code&gt; under one key. The comment at the top of &lt;code&gt;lib/cart.ts&lt;/code&gt; states the rule. In English: the cart never goes to a server and needs no sign-up; only SKU, quantity and metres are kept; the price is read from &lt;code&gt;data/fiyatlar.json&lt;/code&gt; on every render, so an old price does not survive a price list update; every function returns a new array.&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;// localStorage sepeti. Sunucuya hiç gitmez, kayıt gerekmez.&lt;/span&gt;
&lt;span class="c1"&gt;// Sadece sku + adet + metre tutulur; fiyat her render'da data/fiyatlar.json'dan okunur&lt;/span&gt;
&lt;span class="c1"&gt;// (fiyat listesi güncellenince eski fiyat sepette kalmasın diye).&lt;/span&gt;
&lt;span class="c1"&gt;// Tüm fonksiyonlar immutable: yeni dizi döner.&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;CART_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ls_cart_v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A line is four fields:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;CartLine&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;sku&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="nl"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="cm"&gt;/** Metre bazlı ürünlerde seçilen uzunluk (m). Adet ürünlerinde yok. */&lt;/span&gt;
  &lt;span class="nl"&gt;meters&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="cm"&gt;/** Müşterinin "özel ölçü" notu. */&lt;/span&gt;
  &lt;span class="nl"&gt;note&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping prices out of the cart is the important decision. If a visitor adds a motif in September and comes back after a price update in October, the cart shows October's price. There is no stale-price bug to fix because the cart has nothing to go stale.&lt;/p&gt;

&lt;p&gt;Everything that comes from outside (storage, or a shared cart URL) goes through &lt;code&gt;sanitize&lt;/code&gt; first. The comment on the last constant says control characters, newlines included, must not break the WhatsApp message:&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;MAX_LINES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&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;MAX_METERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&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;SKU_RE&lt;/span&gt; &lt;span class="o"&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;A-Za-z0-9._-&lt;/span&gt;&lt;span class="se"&gt;]{1,40}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Kontrol karakterleri (satır sonu dahil) WhatsApp mesajını bozmasın.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CONTROL_RE&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;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u0000-&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u001f&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u007f]+&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="s2"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unknown shapes are dropped, quantities are clamped to 1-999, invalid metre values remove the line, and notes are cut to 200 characters with control characters replaced. Duplicate lines merge through &lt;code&gt;addLine&lt;/code&gt;, which returns a new array instead of mutating the old one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sharing a cart without an account
&lt;/h3&gt;

&lt;p&gt;A contractor often builds a list and sends it to the person who pays. With no accounts, the cart itself has to travel. &lt;code&gt;encodeCart&lt;/code&gt; turns it into a short query string:&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="cm"&gt;/** Paylaşılabilir sepet: ?c=SKU*2,SKU~12.5*1  (~ metre) */&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;encodeCart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;CartLine&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;lines&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;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meters&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;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meters&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="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;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qty&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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Opening &lt;code&gt;/sepet?c=...&lt;/code&gt; decodes it, runs it through &lt;code&gt;sanitize&lt;/code&gt;, merges it into the local cart and removes the parameter from the address bar.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the cart page gets its prices
&lt;/h3&gt;

&lt;p&gt;The cart page is a client component, and I did not want the whole catalog in the client bundle. So a static route builds a compact lookup at build time, &lt;code&gt;app/sepet-veri.json/route.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;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;function&lt;/span&gt; &lt;span class="nf"&gt;GET&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;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="kr"&gt;string&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="kr"&gt;string&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="kr"&gt;string&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="kr"&gt;number&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;adet&lt;/span&gt;&lt;span class="dl"&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;m&lt;/span&gt;&lt;span class="dl"&gt;"&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="k"&gt;for &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;p&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;allProducts&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &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;v&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variants&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;pr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&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="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sizeLabel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;colorLabel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;gross&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="nx"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;unit&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;adet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pricing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unitLabel&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;adet&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="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="nx"&gt;out&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cache-Control&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="s2"&gt;public, max-age=3600, must-revalidate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tuples instead of objects keep the payload smaller: 750 SKUs come to roughly 150 KB before compression. The file comment also notes that internal fields (supplier references, cost data) never enter this payload. The cart fetches it once, resolves each line and computes totals locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  From cart to WhatsApp message
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;lib/whatsapp.ts&lt;/code&gt; builds &lt;code&gt;wa.me&lt;/code&gt; links. It deliberately imports nothing from the catalog or the price module, so it stays small in the client bundle.&lt;/p&gt;

&lt;p&gt;A product page button produces a message like this:&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;productMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WaLine&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;`Merhaba, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; üzerinden yazıyorum.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`Ürün: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sku&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="s2"&gt;`Seçim: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&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;belirtilmedi&lt;/span&gt;&lt;span class="dl"&gt;"&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="s2"&gt;`Miktar: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gross&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Sayfadaki fiyat: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gross&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; (KDV dahil, montaj ve nakliye hariç)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Teslimat il/ilçe: &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;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&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;Product, SKU, chosen size and colour, quantity, the price the customer saw (VAT included, installation and shipping excluded), and an empty "delivery city/district" line at the end for the customer to fill in. The person answering on the other side gets every fact needed to quote shipping in the first message, which is the whole point.&lt;/p&gt;

&lt;p&gt;The cart message has one wrinkle. Turkish characters cost six bytes each once percent-encoded (&lt;code&gt;ı&lt;/code&gt; becomes &lt;code&gt;%C4%B1&lt;/code&gt;), and very long &lt;code&gt;wa.me&lt;/code&gt; URLs are not reliable across clients. So there is a ceiling and a compact mode:&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;WA_MAX_ENCODED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1800&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;cartMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;WaLine&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;shareUrl&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="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;full&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`Merhaba, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; sepetimdeki ürünler için sipariş vermek istiyorum.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;l&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="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;compact&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&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;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="s2"&gt;) &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sku&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;amount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)}${&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gross&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;tl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gross&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="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="k"&gt;else&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;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="c1"&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;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;full&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&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;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;WA_MAX_ENCODED&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;full&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;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compact form keeps SKU, amount and line total per line. The cart page also passes the shareable cart link into the message, so nothing is lost: the person replying can open the link and see the full cart with names and sizes. Both forms are covered in &lt;code&gt;tests/whatsapp.test.ts&lt;/code&gt;: Turkish characters survive encoding, an empty selection prints "belirtilmedi" (not specified), a 15-line cart switches to compact mode and stays under the limit, and a short cart stays in full mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Markdown for AI agents, on the same URL
&lt;/h2&gt;

&lt;p&gt;Search assistants and agents read pages too, and a product page full of layout markup is a poor input. Every product and category has a Markdown twin, served by a statically generated route handler, &lt;code&gt;app/md/[...path]/route.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;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="p"&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="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;index.md&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;path&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="s2"&gt;urunler.md&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="nx"&gt;CATEGORIES&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;c&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="na"&gt;path&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="s2"&gt;kategori&lt;/span&gt;&lt;span class="dl"&gt;"&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;c&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;.md`&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="nf"&gt;allProducts&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;p&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="na"&gt;path&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="s2"&gt;urun&lt;/span&gt;&lt;span class="dl"&gt;"&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;p&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;.md`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Markdown is built from the same data as the HTML (&lt;code&gt;lib/markdown.ts&lt;/code&gt;), so the variant table with VAT-included prices matches the page exactly.&lt;/p&gt;

&lt;p&gt;A separate URL is useful, but agents should not need to know it. &lt;code&gt;proxy.ts&lt;/code&gt; (Next.js 16's replacement for &lt;code&gt;middleware.ts&lt;/code&gt;) does content negotiation on the normal URL:&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;wantsMarkdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="nx"&gt;boolean&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;accept&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;const&lt;/span&gt; &lt;span class="nx"&gt;md&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;q&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/markdown&lt;/span&gt;&lt;span class="dl"&gt;"&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;md&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;md&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nf"&gt;q&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/html&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;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;req&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="kd"&gt;const&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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;md&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;markdownPath&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;if &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;/sepet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;wantsMarkdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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="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;accept&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;res&lt;/span&gt; &lt;span class="o"&gt;=&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="nx"&gt;md&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&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="nx"&gt;res&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="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Vary&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="s2"&gt;Accept&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&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="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&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="s2"&gt;text/markdown; charset=utf-8&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;res&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;res&lt;/span&gt; &lt;span class="o"&gt;=&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;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;res&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="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Vary&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="s2"&gt;Accept&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// ...Link header: sitemap, llms.txt and the Markdown alternate&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&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;Two details matter. Markdown wins only when the client ranks it at least as high as HTML, and &lt;code&gt;*/*&lt;/code&gt; does not count as asking for Markdown (the &lt;code&gt;false&lt;/code&gt; argument turns off wildcard matching). A browser sending &lt;code&gt;text/html,*/*;q=0.8&lt;/code&gt; never gets Markdown by accident. And the cart page is excluded, because a cart is personal and &lt;code&gt;noindex&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can try it:&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;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: text/markdown"&lt;/span&gt; https://www.ledsusleme.com/urun/sacak-led
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That returns &lt;code&gt;200&lt;/code&gt; with &lt;code&gt;Content-Type: text/markdown; charset=utf-8&lt;/code&gt; and a variant table. Without the header, the same URL returns HTML with a &lt;code&gt;Link: &amp;lt;/md/urun/sacak-led.md&amp;gt;; rel="alternate"; type="text/markdown"&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One open issue, honestly reported.&lt;/strong&gt; The proxy sets &lt;code&gt;Vary: Accept&lt;/code&gt; on both branches, because a CDN that caches without it could serve Markdown to a browser. In the live response I checked on 3 October 2026, the &lt;code&gt;Vary&lt;/code&gt; header shows only Next.js's own router values (&lt;code&gt;rsc, next-router-state-tree, ...&lt;/code&gt;), not &lt;code&gt;Accept&lt;/code&gt;. Something downstream replaces the value instead of appending to it. Until that is fixed, the protection against cache mix-ups rests on Vercel's routing, not on the header I meant to send. It is on the fix list.&lt;/p&gt;

&lt;h2&gt;
  
  
  A content gate that checks numbers against data
&lt;/h2&gt;

&lt;p&gt;Product copy was written partly with AI help. The failure mode I worried about most was not style. It was a confident sentence with a wrong number: a motif described as 120 cm when the data says 100, or a wattage that belongs to a different size.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;scripts/audit-content.ts&lt;/code&gt; runs in &lt;code&gt;prebuild&lt;/code&gt;, and any finding exits with code 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"prebuild"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx tsx scripts/validate-data.ts &amp;amp;&amp;amp; npx tsx scripts/audit-content.ts"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The core is a regular expression that finds every number followed by a unit:&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;UNIT_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(?:[&lt;/span&gt;&lt;span class="sr"&gt;.,&lt;/span&gt;&lt;span class="se"&gt;]\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)?)\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;cm|mm|metre|m|w|watt|kg|v|volt|led|k|tl|yıl|gün|%&lt;/span&gt;&lt;span class="se"&gt;)(?![&lt;/span&gt;&lt;span class="sr"&gt;a-zçğıöşü&lt;/span&gt;&lt;span class="se"&gt;])&lt;/span&gt;&lt;span class="sr"&gt;/giu&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each product, the allowed set is built from that product's own variants: every number in the size label, plus &lt;code&gt;primaryCm&lt;/code&gt;, &lt;code&gt;maxCm&lt;/code&gt;, &lt;code&gt;wattW&lt;/code&gt;, &lt;code&gt;weightKg&lt;/code&gt;, &lt;code&gt;voltageV&lt;/code&gt; and &lt;code&gt;ledCount&lt;/code&gt;. Centimetre values also get their metre equivalents, so "250 cm" and "2,5 m" both pass. A short list of general numbers is allowed everywhere (colour temperatures, common voltages, the warranty years, the return window).&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;function&lt;/span&gt; &lt;span class="nf"&gt;factGate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bad&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="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &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;m&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;UNIT_RE&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;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;num&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&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;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;unitKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tl&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;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&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;m&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="s2"&gt; (fiyat metne yazılmaz, tablodan okunur)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;continue&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;general&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ALLOWED_NUMBERS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;ALLOWED_NUMBERS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;general&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&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="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;bad&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;Any amount in lira inside product copy is an error, full stop; the message reads "price is not written in copy, it is read from the table". Prices exist in one place.&lt;/p&gt;

&lt;p&gt;The same script runs a few other checks, each one a few lines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Banned phrases.&lt;/strong&gt; A list of Turkish marketing clichés plus the em-dash. The match uses Unicode letter boundaries, because one banned word, "şık" (chic), is also the tail of "ışık" (light), which appears on nearly every page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Required sentences.&lt;/strong&gt; Each product needs a recommendation sentence ("we recommend ... because ..."), at least one "not suitable for" item, and its own size somewhere in the text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Near-duplicates.&lt;/strong&gt; 5-word shingles with Jaccard similarity: below 0.15 between any two products, below 0.20 inside the same category.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentence rhythm.&lt;/strong&gt; At least 70% of sentences under 20 words, and a standard deviation of sentence length of at least 4, so the copy does not settle into one monotonous length.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A sibling script, &lt;code&gt;scripts/validate-data.ts&lt;/code&gt;, checks the data side: every SKU has a positive price, every image exists under &lt;code&gt;public/&lt;/code&gt;, slugs and SKUs are unique, and no supplier model name or code leaks into a visible field.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured data that matches the page
&lt;/h2&gt;

&lt;p&gt;Each product page emits a &lt;code&gt;ProductGroup&lt;/code&gt; with one &lt;code&gt;Product&lt;/code&gt; per variant (&lt;code&gt;lib/product-schema.ts&lt;/code&gt;). Size and colour drive &lt;code&gt;variesBy&lt;/code&gt;, and each variant gets its own &lt;code&gt;Offer&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;offers&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="s2"&gt;@type&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="s2"&gt;Offer&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;multi&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;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?v=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sku&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;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gross&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;priceCurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TRY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;priceValidUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;priceValidUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;availability&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://schema.org/MadeToOrder&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;itemCondition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://schema.org/NewCondition&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;seller&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="s2"&gt;@id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ORG_ID&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;hasMerchantReturnPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;returnPolicyStandard&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="c1"&gt;// ...metre ürünlerinde UnitPriceSpecification, unitCode "MTR"&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three choices worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;price&lt;/code&gt; is the gross figure, the same number the visitor sees. Structured data that disagrees with the visible price is worse than none.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;availability&lt;/code&gt; is &lt;code&gt;MadeToOrder&lt;/code&gt;, because nothing is in stock; every piece is built after the order.&lt;/li&gt;
&lt;li&gt;There is no &lt;code&gt;shippingDetails&lt;/code&gt;. The comment in the file explains why: shipping is quoted per order on WhatsApp, and a wrong shipping declaration is worse than a missing one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 37 products with a single variant skip the group and emit a plain &lt;code&gt;Product&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fix &lt;code&gt;Vary&lt;/code&gt; first, not last.&lt;/strong&gt; Content negotiation without a working &lt;code&gt;Vary&lt;/code&gt; header relies on the platform's routing. I would verify the live header on day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate the cart lookup per category.&lt;/strong&gt; About 150 KB is acceptable, but a cart rarely touches more than a few categories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the gate's allow-list short.&lt;/strong&gt; Every general number added to &lt;code&gt;ALLOWED_NUMBERS&lt;/code&gt; is a hole in the fact gate. So far it holds colour temperatures, voltages, the warranty and return windows, and two percentages.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The site has no database, no checkout and no accounts, and it does not miss them. One JSON file holds every price. The cart stores only what the customer chose, never what it cost, and turns into a WhatsApp message that answers the seller's first questions. The build refuses copy with numbers the data cannot back.&lt;/p&gt;

&lt;p&gt;The live site is &lt;a href="https://www.ledsusleme.com" rel="noopener noreferrer"&gt;www.ledsusleme.com&lt;/a&gt;. Product pages answer &lt;code&gt;Accept: text/markdown&lt;/code&gt;, and the site-wide summary for agents is at &lt;a href="https://www.ledsusleme.com/llms.txt" rel="noopener noreferrer"&gt;/llms.txt&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ojsxlueldo3l04macxa.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ojsxlueldo3l04macxa.jpg" alt=" " width="794" height="575"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>couchbase</category>
    </item>
    <item>
      <title>Build Gates for a Small Next.js 16 Site: Failing the Build on Bad Copy, Serving Markdown to AI Agents</title>
      <dc:creator>Olivia</dc:creator>
      <pubDate>Thu, 01 Oct 2026 11:13:57 +0000</pubDate>
      <link>https://dev.to/olivia_342fsfsdgrere/build-gates-for-a-small-nextjs-16-site-failing-the-build-on-bad-copy-serving-markdown-to-ai-1hbd</link>
      <guid>https://dev.to/olivia_342fsfsdgrere/build-gates-for-a-small-nextjs-16-site-failing-the-build-on-bad-copy-serving-markdown-to-ai-1hbd</guid>
      <description>&lt;p&gt;I recently built a small service site in Turkish: &lt;a href="https://www.villaisiksusleme.com" rel="noopener noreferrer"&gt;villaisiksusleme.com&lt;/a&gt;, outdoor Christmas lighting for villas and waterfront houses in Istanbul. About 40 pages: a home page, five service pages, ten neighbourhood pages, nine guides, a pricing page and the usual legal pages. Next.js 16 App Router on Vercel, Tailwind v4, vitest.&lt;/p&gt;

&lt;p&gt;The code was never the hard part. The hard part was keeping the content honest across 40 pages written partly with AI help: no filler phrases, no prices typed by hand that drift from the real price list, no two district pages that are the same page with the name swapped. Reviewing that by eye does not scale, even at 40 pages.&lt;/p&gt;

&lt;p&gt;So the rules live in the build. If a page breaks one, &lt;code&gt;npm run build&lt;/code&gt; exits with code 1 and nothing deploys. This post walks through the gates, with short excerpts from the real scripts (variable names are Turkish; I translate as I go).&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prebuild:&lt;/strong&gt; a 124-line Node script scans &lt;code&gt;app/&lt;/code&gt;, &lt;code&gt;components/&lt;/code&gt;, &lt;code&gt;data/&lt;/code&gt; and &lt;code&gt;lib/&lt;/code&gt; for banned phrases, em-dashes, emoji, icon library imports and hand-typed prices. Any finding fails the build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postbuild:&lt;/strong&gt; a second script reads the generated HTML in &lt;code&gt;.next/server/app&lt;/code&gt;, because the copy lives in JSX and only exists as text after rendering. It checks titles, descriptions, H1 count, a "short answer" box, image count and 8-word shingle similarity between pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One source of truth:&lt;/strong&gt; prices, company facts and the founding year live in &lt;code&gt;data/&lt;/code&gt;. Pages import them; the scanner rejects price literals in page code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI readability:&lt;/strong&gt; every page gets a Markdown twin generated from the build output, and the same URL returns Markdown when a client sends &lt;code&gt;Accept: text/markdown&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IndexNow:&lt;/strong&gt; a postbuild ping, with a fallback key so it does not depend on an environment variable I could not verify.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why put content rules in the build at all?
&lt;/h2&gt;

&lt;p&gt;Content rules belong in the build when the content is code. On this site every sentence sits in a &lt;code&gt;page.tsx&lt;/code&gt; file, written as JSX. A style guide in a Markdown file gets read once. A script in &lt;code&gt;prebuild&lt;/code&gt; runs on every deploy, including the deploys where I am tired and an AI assistant has just rewritten a paragraph.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;package.json&lt;/code&gt; wiring is plain npm lifecycle scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"prebuild"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node scripts/policy-scan.mjs"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"next build"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"postbuild"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node scripts/ai-mirror.mjs &amp;amp;&amp;amp; node scripts/content-audit.mjs &amp;amp;&amp;amp; node scripts/indexnow-ping.mjs"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vercel runs &lt;code&gt;npm run build&lt;/code&gt;, so &lt;code&gt;prebuild&lt;/code&gt; and &lt;code&gt;postbuild&lt;/code&gt; run there too. No CI config needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 1: scanning the source before the build
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;scripts/policy-scan.mjs&lt;/code&gt; walks four directories and checks every &lt;code&gt;.ts&lt;/code&gt;, &lt;code&gt;.tsx&lt;/code&gt;, &lt;code&gt;.mjs&lt;/code&gt; and &lt;code&gt;.json&lt;/code&gt; file line by line. The phrase list is the interesting part. It is in Turkish, so here are a few entries with translations:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PHRASES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="c1"&gt;// context.md §2.1 giriş kalıpları (stock openers)&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;günümüzde&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="s2"&gt;modern dünyada&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="s2"&gt;yılbaşı yaklaşırken&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="s2"&gt;bu yazıda&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="c1"&gt;// boş vurgu (empty emphasis)&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mükemmel&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="s2"&gt;eşsiz&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="s2"&gt;büyüleyici&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="s2"&gt;unutulmaz&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="s2"&gt;kusursuz&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="c1"&gt;// kapanış kalıpları (closing clichés)&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sonuç olarak&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="s2"&gt;özetle&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="s2"&gt;unutmayın ki&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is "nowadays", "in the modern world", "in this article", "perfect", "unique", "unforgettable", "in conclusion", "to sum up". Turkish marketing copy and AI-generated Turkish lean on the same phrases. The list also holds company-specific bans, for example claims that belong to a sister business and must not appear on this site.&lt;/p&gt;

&lt;p&gt;Three details made the scanner usable instead of annoying:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comments are not scanned.&lt;/strong&gt; The script blanks out comments before matching, keeping line numbers intact, so I can write a comment that explains &lt;em&gt;why&lt;/em&gt; a phrase is banned without tripping the rule:&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="cm"&gt;/** Yorumları boşlukla değiştirir (satır numaraları korunur). */&lt;/span&gt;
&lt;span class="c1"&gt;// "Replaces comments with spaces (line numbers are kept)."&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stripComments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&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;src&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/\*[\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;?\*\/&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&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;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[^\n]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&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;^|&lt;/span&gt;&lt;span class="se"&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;.*$/gm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p1&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;p1&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="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&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="nx"&gt;p1&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;There is an escape hatch.&lt;/strong&gt; A line containing &lt;code&gt;policy-ok&lt;/code&gt; is skipped. The convention is that the reason goes in a comment on the same line, so an exception is visible in review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phrases that span lines are caught too.&lt;/strong&gt; JSX wraps long sentences, so "sonuç olarak" can end up split across two lines. After the line pass, the script joins the whole file into one string and searches again.&lt;/p&gt;

&lt;p&gt;The same pass rejects em-dashes and en-dashes (a style choice for this site), emoji, imports from &lt;code&gt;lucide-react&lt;/code&gt;, &lt;code&gt;react-icons&lt;/code&gt; and similar packages (the design uses typography instead of icons), and one more thing worth its own section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 2: no prices typed by hand
&lt;/h2&gt;

&lt;p&gt;Prices live in exactly one file, &lt;code&gt;data/pricing.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PACKAGES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;Package&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sade&lt;/span&gt;&lt;span class="dl"&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;Sade&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="nx"&gt;_000&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;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bahce-cephe&lt;/span&gt;&lt;span class="dl"&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;Bahçe + Cephe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="nx"&gt;_000&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;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tam-dekor&lt;/span&gt;&lt;span class="dl"&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;Tam Dekor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;380&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pages import these numbers and format them. To stop anyone, me included, from typing "48.000 TL" into a page, the scanner runs a price regex over &lt;code&gt;app/&lt;/code&gt; and &lt;code&gt;components/&lt;/code&gt; only:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PRICE_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b\d{1,3}(?:\.\d{3})&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;TL|₺&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\b\d{2,3}\s&lt;/span&gt;&lt;span class="sr"&gt;*bin&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;TL|lira&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It matches Turkish number formatting ("48.000 TL") and the spoken form ("48 bin lira"). The &lt;code&gt;data/&lt;/code&gt; folder is excluded from this one check, because that is where prices are supposed to be.&lt;/p&gt;

&lt;p&gt;The postbuild audit closes the loop from the other side. It reads &lt;code&gt;data/pricing.ts&lt;/code&gt;, formats each band the Turkish way, and fails if &lt;code&gt;public/llms.txt&lt;/code&gt; does not contain it. When the price list changes and the AI-facing summary does not, the build stops.&lt;/p&gt;

&lt;p&gt;The same idea covers company facts. &lt;code&gt;data/site.ts&lt;/code&gt; holds the name, address, phone numbers and founding year, and the years-in-business figure is computed rather than written:&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;YEARS_EXPERIENCE&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;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;ENTITY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;foundingYear&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A hard-coded "17 years" would be wrong on 1 January.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 3: measuring the rendered HTML, not the source
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;scripts/content-audit.mjs&lt;/code&gt; runs after &lt;code&gt;next build&lt;/code&gt;. Its header comment explains the design choice: &lt;em&gt;"Metin JSX içinde yaşadığı için ölçüm kaynak yerine çıktıda yapılır"&lt;/em&gt;, which means "because the text lives inside JSX, we measure the output instead of the source". Counting words in a &lt;code&gt;.tsx&lt;/code&gt; file counts &lt;code&gt;className&lt;/code&gt; strings. Counting words inside the rendered &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt; counts what a reader sees.&lt;/p&gt;

&lt;p&gt;For each HTML file in &lt;code&gt;.next/server/app&lt;/code&gt; it checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; between 30 and 65 characters (a warning outside 50 to 62), meta description between 120 and 165 for indexable pages, a canonical link.&lt;/li&gt;
&lt;li&gt;Exactly one &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; inside &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Exactly one &lt;code&gt;HomeAndConstructionBusiness&lt;/code&gt; node in the JSON-LD, and no &lt;code&gt;aggregateRating&lt;/code&gt;, because the site has no review data to back one.&lt;/li&gt;
&lt;li&gt;A "short answer" box (&lt;code&gt;data-kisa-cevap&lt;/code&gt;) of 40 to 60 words containing at least one digit, on every page except legal and utility pages.&lt;/li&gt;
&lt;li&gt;At least five &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tags on service, district and guide pages.&lt;/li&gt;
&lt;li&gt;Service and district pages under 700 words must carry &lt;code&gt;noindex&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Every service page must contain a sentence about what the company does &lt;em&gt;not&lt;/em&gt; do. The check is a blunt regex for Turkish verbs like "yapmıyoruz" ("we don't do") and "önermiyoruz" ("we don't recommend").&lt;/li&gt;
&lt;li&gt;No broken internal links, no orphan pages, nothing deeper than three clicks from the home page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Near-duplicate pages
&lt;/h3&gt;

&lt;p&gt;Ten district pages invite templating: same layout, swap "Beykoz" for "Sarıyer". The audit compares every pair of indexable pages with 8-word shingles and Jaccard similarity:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BENZERLIK_ESIK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// similarity threshold&lt;/span&gt;

&lt;span class="k"&gt;for &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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;ws&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;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;s&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="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inter&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;inter&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;jac&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;BENZERLIK_ESIK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`benzerlik &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;jac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;keys&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="s2"&gt; ~ &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight words is long enough that shared navigation phrases and normal Turkish collocations do not count, and short enough that a pasted paragraph with one name changed does. Lowercasing uses &lt;code&gt;toLocaleLowerCase("tr")&lt;/code&gt;, because Turkish has a dotted and a dotless i, and plain &lt;code&gt;toLowerCase()&lt;/code&gt; gets "İ" wrong.&lt;/p&gt;

&lt;p&gt;The script prints the highest pair on every run, even when it passes, so I can see how close the site is to the line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making every page readable as Markdown
&lt;/h2&gt;

&lt;p&gt;The site is meant to be quoted by AI assistants, so it ships three things beyond HTML.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Markdown twin of every page.&lt;/strong&gt; &lt;code&gt;scripts/ai-mirror.mjs&lt;/code&gt; takes the &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt; of each built page, runs it through &lt;code&gt;turndown&lt;/code&gt; with the GFM plugin, adds front matter (title, description, canonical URL, dates, author) and writes &lt;code&gt;public/md/&amp;lt;path&amp;gt;.md&lt;/code&gt;. It also concatenates everything into &lt;code&gt;public/llms-full.txt&lt;/code&gt;. A few custom rules matter: image &lt;code&gt;src&lt;/code&gt; values are pulled back out of the &lt;code&gt;next/image&lt;/code&gt; optimizer URL, relative links become absolute so a quoted fragment still works, and lists inside table cells are flattened to &lt;code&gt;a; b; c&lt;/code&gt; so they do not break the Markdown table. Pages with &lt;code&gt;noindex&lt;/code&gt; are skipped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content negotiation on the same URL.&lt;/strong&gt; In Next.js 16, &lt;code&gt;middleware.ts&lt;/code&gt; is called &lt;code&gt;proxy.ts&lt;/code&gt;. Mine compares the &lt;code&gt;q&lt;/code&gt; values in the &lt;code&gt;Accept&lt;/code&gt; header:&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;markdownIster&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="nx"&gt;boolean&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;accept&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// Markdown yalnız açıkça istenirse; "*/*" tarayıcı ve botlara HTML kalır.&lt;/span&gt;
  &lt;span class="c1"&gt;// "Markdown only when asked for explicitly; */* keeps HTML for browsers and bots."&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;md&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;q&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/markdown&lt;/span&gt;&lt;span class="dl"&gt;"&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;md&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;md&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nf"&gt;q&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/html&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;The &lt;code&gt;false&lt;/code&gt; argument disables wildcard matching for Markdown. A crawler that sends &lt;code&gt;*/*&lt;/code&gt; gets HTML. Only a client that names &lt;code&gt;text/markdown&lt;/code&gt; gets the rewrite to &lt;code&gt;/md/...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Checked against production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -s -D - -o /dev/null -H "Accept: text/markdown" https://www.villaisiksusleme.com/paketler
HTTP/1.1 200 OK
Content-Type: text/markdown; charset=utf-8
Vary: Accept

$ curl -s -o /dev/null -w "%{http_code} %{content_type}\n" -H "Accept: */*" https://www.villaisiksusleme.com/paketler
200 text/html; charset=utf-8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Discovery headers.&lt;/strong&gt; HTML responses carry a &lt;code&gt;Link&lt;/code&gt; header pointing to the sitemap, &lt;code&gt;llms.txt&lt;/code&gt;, &lt;code&gt;llms-full.txt&lt;/code&gt; and the page's own Markdown twin with &lt;code&gt;rel="alternate"; type="text/markdown"&lt;/code&gt;. The &lt;code&gt;/md/&lt;/code&gt; files themselves are served with &lt;code&gt;X-Robots-Tag: noindex, follow&lt;/code&gt; so the copies do not compete with the HTML pages in search.&lt;/p&gt;

&lt;h2&gt;
  
  
  IndexNow, with a fallback key
&lt;/h2&gt;

&lt;p&gt;The last postbuild step pings IndexNow (used by Bing and Yandex) with every URL in the live sitemap. It only runs when &lt;code&gt;VERCEL_ENV&lt;/code&gt; is &lt;code&gt;production&lt;/code&gt; or when forced, so preview deploys do not ping.&lt;/p&gt;

&lt;p&gt;The first version read the key only from &lt;code&gt;INDEXNOW_KEY&lt;/code&gt;. When I went to submit the site, I could not confirm whether that variable was set in the Vercel project. A missing env var here fails silently: the script logs a line and moves on. So I generated a key, committed the key file to &lt;code&gt;public/&lt;/code&gt;, and made the env var optional:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;5ed03e268fe03568d4c4e1d62402aba2&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;KEY&lt;/span&gt; &lt;span class="o"&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;INDEXNOW_KEY&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;IndexNow keys are public by design, since the key file has to be reachable at the site root, so there is nothing secret in the repo. The first manual run (&lt;code&gt;INDEXNOW_FORCE=1 node scripts/indexnow-ping.mjs&lt;/code&gt;) returned &lt;code&gt;202&lt;/code&gt; for 41 URLs, which means accepted with key validation pending. I also made the script print the response body on non-2xx, which the first version did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A layout shift on the home page.&lt;/strong&gt; The commit log has &lt;code&gt;fix: ana sayfa hero butonları mobilde alt alta (CLS 0,108 -&amp;gt; 0)&lt;/code&gt;, "home hero buttons stacked on mobile". The two hero buttons sat side by side on phones; stacking them took the measured CLS from 0.108 to 0. None of my gates measure layout shift. That number came from measuring the page by hand, and it is still a manual check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two Link headers and a lost Vary.&lt;/strong&gt; While writing this post I ran &lt;code&gt;curl -I&lt;/code&gt; against an HTML page and saw two &lt;code&gt;Link&lt;/code&gt; headers: one from &lt;code&gt;headers()&lt;/code&gt; in &lt;code&gt;next.config.ts&lt;/code&gt; and one set in &lt;code&gt;proxy.ts&lt;/code&gt;. The second is a superset of the first, so nothing breaks, but it is redundant. The same response shows &lt;code&gt;Vary: rsc, next-router-state-tree, ...&lt;/code&gt; from Next.js rather than the &lt;code&gt;Vary: Accept&lt;/code&gt; that &lt;code&gt;proxy.ts&lt;/code&gt; sets, so on HTML responses my header is replaced. The Markdown response keeps &lt;code&gt;Vary: Accept&lt;/code&gt;. Both are open items; I would rather list them than pretend the setup is finished.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The phrase list keeps growing.&lt;/strong&gt; Every new page has added one or two entries. A scanner like this is only as good as the last time someone read the output and asked "why does this sentence sound wrong?"&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to copy the idea
&lt;/h2&gt;

&lt;p&gt;You do not need my phrase list. The pattern is small:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep facts that appear on many pages (prices, contact details, counts) in one data file, and fail the build when a page types them by hand.&lt;/li&gt;
&lt;li&gt;Scan the source for things you never want to ship, skip comments, and allow a visible, greppable escape hatch.&lt;/li&gt;
&lt;li&gt;Measure content on the build output, not in the JSX.&lt;/li&gt;
&lt;li&gt;Compare pages pairwise with long shingles if your site has templated sections.&lt;/li&gt;
&lt;li&gt;Generate the Markdown copy from the same HTML, so it can never drift from what humans see.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The live site is at &lt;a href="https://www.villaisiksusleme.com" rel="noopener noreferrer"&gt;villaisiksusleme.com&lt;/a&gt;. Request any page with &lt;code&gt;Accept: text/markdown&lt;/code&gt; to see the Markdown version, or read &lt;a href="https://www.villaisiksusleme.com/llms.txt" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt; for the site summary.&lt;br&gt;
&lt;code&gt;&lt;br&gt;
![ ](https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/knv5eylwq57okk59g7ea.jpg)&lt;/code&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>seo</category>
      <category>ai</category>
    </item>
    <item>
      <title>Making a Next.js Site Readable by AI Agents: llms.txt, a Markdown Mirror and Accept: text/markdown</title>
      <dc:creator>Olivia</dc:creator>
      <pubDate>Wed, 30 Sep 2026 11:46:51 +0000</pubDate>
      <link>https://dev.to/olivia_342fsfsdgrere/making-a-nextjs-site-readable-by-ai-agents-llmstxt-a-markdown-mirror-and-accept-textmarkdown-38e3</link>
      <guid>https://dev.to/olivia_342fsfsdgrere/making-a-nextjs-site-readable-by-ai-agents-llmstxt-a-markdown-mirror-and-accept-textmarkdown-38e3</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjuduoprydbww2a08v7rq.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjuduoprydbww2a08v7rq.jpg" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;a href="https://www.askedtheai.com/" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A growing share of people never see your HTML. They ask ChatGPT, Claude, Perplexity or Gemini, and an agent fetches your page on their behalf. That agent has a token budget, and your cookie banner, mega-menu and 40 KB of hydration script are not what it came for.&lt;/p&gt;

&lt;p&gt;I run &lt;a href="https://www.askedtheai.com" rel="noopener noreferrer"&gt;AskedTheAI&lt;/a&gt;, a shopping guide that asks four AI models the same buying question and has a human check every pick on Amazon. The site is about AI, so it would be odd if AI agents struggled to read it. Before launch I set up four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A Markdown copy of every page, generated from the build output&lt;/li&gt;
&lt;li&gt;Content negotiation: the same URL returns Markdown when an agent asks for it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;llms.txt&lt;/code&gt; and &lt;code&gt;llms-full.txt&lt;/code&gt; as a map&lt;/li&gt;
&lt;li&gt;IndexNow pings so Bing and Yandex hear about changes immediately&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The stack is Next.js 16 (App Router, fully static), deployed on Vercel. Everything below is the actual code from the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Generate the Markdown mirror from the build, not from source
&lt;/h2&gt;

&lt;p&gt;My first instinct was to render Markdown from the React components. That's a trap: every page is its own composition, and I'd be maintaining two renderers.&lt;/p&gt;

&lt;p&gt;Instead, the mirror is built from what Next already prerendered. After &lt;code&gt;next build&lt;/code&gt;, every static page exists as HTML under &lt;code&gt;.next/server/app&lt;/code&gt;. A small Node script walks that folder, pulls out &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt;, converts it with &lt;a href="https://www.npmjs.com/package/node-html-markdown" rel="noopener noreferrer"&gt;&lt;code&gt;node-html-markdown&lt;/code&gt;&lt;/a&gt; and writes &lt;code&gt;public/md/&amp;lt;route&amp;gt;.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/md/generate.mjs (excerpt)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;APP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.next/server/app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public/md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SKIP&lt;/span&gt; &lt;span class="o"&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;api|_not-found|_global-error|404|500|examples|search|templates|md&lt;/span&gt;&lt;span class="se"&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;/&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nhm&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;NodeHtmlMarkdown&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;ignore&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;script&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;style&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;noscript&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;svg&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;button&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;form&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;iframe&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;video&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;template&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;maxConsecutiveNewlines&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="na"&gt;useLinkReferenceDefinitions&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each file gets a small front matter block so an agent knows what it's holding and where it came from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;We&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Ask&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AI:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Our&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Research&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Method&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AskedTheAI"&lt;/span&gt;
&lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://www.askedtheai.com/ai-research&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AskedTheAI&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;puts&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;same&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;buying&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Claude,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ChatGPT,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Gemini&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Grok..."&lt;/span&gt;
&lt;span class="na"&gt;last_updated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-10-01&lt;/span&gt;
&lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AskedTheAI (https://www.askedtheai.com)&lt;/span&gt;
&lt;span class="na"&gt;affiliate_disclosure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;As an Amazon Associate we earn from qualifying purchases.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script also writes &lt;code&gt;src/data/md-routes.json&lt;/code&gt;, a list of every route that has a mirror. The middleware below imports it.&lt;/p&gt;

&lt;p&gt;The pipeline is two commands, and the output is committed so Vercel serves it as static files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run ai:files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The HTML is the source of truth. The Markdown can't drift from it, because it's derived from it on every run.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Content negotiation in middleware
&lt;/h2&gt;

&lt;p&gt;Agents that want Markdown can say so with an &lt;code&gt;Accept&lt;/code&gt; header. The page URL stays the same; only the representation changes. Here's the whole middleware:&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;// src/middleware.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="kd"&gt;type&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;mdRoutes&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;@/data/md-routes.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SITE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.askedtheai.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MD_ROUTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&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;mdRoutes&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&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;mdPathFor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&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;/md/index.md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/md&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.md`&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;prefersMarkdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accept&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;boolean&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="sr"&gt;/text&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;markdown/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&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="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;first&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;text/markdown&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="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/text&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;html/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accept&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;function&lt;/span&gt; &lt;span class="nf"&gt;middleware&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="kd"&gt;const&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="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;nextUrl&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pathname&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;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&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="nf"&gt;replace&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;$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&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;MD_ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&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;next&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;mdPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mdPathFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&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="nf"&gt;prefersMarkdown&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;headers&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;accept&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="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;url&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;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&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="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mdPath&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&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="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;res&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="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Vary&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;Accept&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;res&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="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&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;text/markdown; charset=utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;res&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="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Robots-Tag&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;noindex, follow&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;res&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;res&lt;/span&gt; &lt;span class="o"&gt;=&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;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;res&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="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Link&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SITE&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;mdPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;; rel="alternate"; type="text/markdown"`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&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="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Vary&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;Accept&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;res&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="c1"&gt;// pages only: skip api, Next internals, the mirror itself and any file with an extension&lt;/span&gt;
  &lt;span class="na"&gt;matcher&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;/((?!api/|_next/|md/|.*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;.[a-z0-9]+$).*)&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;A few decisions worth explaining:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;prefersMarkdown&lt;/code&gt; is strict.&lt;/strong&gt; Browsers never send &lt;code&gt;text/markdown&lt;/code&gt;, so they always get HTML. An agent gets Markdown if it lists it first, or lists it without &lt;code&gt;text/html&lt;/code&gt; at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Vary: Accept&lt;/code&gt;, and what actually survives.&lt;/strong&gt; The idea is to stop a CDN from caching the Markdown response and serving it to the next browser. When I checked the live headers, the Markdown response had &lt;code&gt;Vary: Accept&lt;/code&gt;, but on the HTML response Next.js replaced my value with its own list (&lt;code&gt;rsc, next-router-state-tree, ...&lt;/code&gt;). It's safe anyway: the Markdown branch is a rewrite to a different path (&lt;code&gt;/md/...&lt;/code&gt;), so Vercel caches the two representations under different keys. Still, check your real headers with &lt;code&gt;curl -I&lt;/code&gt; instead of trusting the code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;X-Robots-Tag: noindex, follow&lt;/code&gt; on the Markdown.&lt;/strong&gt; The mirror exists for agents, not for search results. I don't want &lt;code&gt;/md/best-dash-cams.md&lt;/code&gt; competing with &lt;code&gt;/best-dash-cams&lt;/code&gt; in Google.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;Link: rel="alternate"&lt;/code&gt; header on every HTML page.&lt;/strong&gt; Agents that fetched HTML first learn a lighter version exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The matcher skips anything with a file extension.&lt;/strong&gt; That turned out to matter later: search engine verification files like &lt;code&gt;BingSiteAuth.xml&lt;/code&gt; and the IndexNow key file pass straight through, untouched.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it yourself:&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; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: text/markdown"&lt;/span&gt; https://www.askedtheai.com/ai-research | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
curl &lt;span class="nt"&gt;-sI&lt;/span&gt; https://www.askedtheai.com/ai-research | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^link'&lt;/span&gt;
&lt;span class="c"&gt;# Link: &amp;lt;https://www.askedtheai.com/md/ai-research.md&amp;gt;; rel="alternate"; type="text/markdown"&lt;/span&gt;
curl &lt;span class="nt"&gt;-sI&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: text/markdown"&lt;/span&gt; https://www.askedtheai.com/ai-research | &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="s1"&gt;'^(content-type|vary|x-robots-tag)'&lt;/span&gt;
&lt;span class="c"&gt;# Content-Type: text/markdown; charset=utf-8&lt;/span&gt;
&lt;span class="c"&gt;# Vary: Accept&lt;/span&gt;
&lt;span class="c"&gt;# X-Robots-Tag: noindex, follow&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A note on naming: Next.js 16 renames &lt;code&gt;middleware.ts&lt;/code&gt; to &lt;code&gt;proxy.ts&lt;/code&gt;. The old name still works. I'm switching after launch rather than touching routing the night before.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. llms.txt as the map
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://llmstxt.org" rel="noopener noreferrer"&gt;&lt;code&gt;llms.txt&lt;/code&gt;&lt;/a&gt; is a plain Markdown file at the site root that tells a model what the site is and where the important pages are. Mine is generated by the same &lt;code&gt;ai:files&lt;/code&gt; step, from the same build output.&lt;/p&gt;

&lt;p&gt;Two things I'd recommend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Say what the site does NOT do, in the first lines.&lt;/strong&gt; My file states that the site doesn't lab-test products and lists the two review formats with page counts. A model summarizing the site should get that right on the first try.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship a long version too.&lt;/strong&gt; &lt;a href="https://www.askedtheai.com/llms-full.txt" rel="noopener noreferrer"&gt;&lt;code&gt;llms-full.txt&lt;/code&gt;&lt;/a&gt; lists every page with a one-line summary and its Markdown URL. The short file is the table of contents; the long one is the index.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. robots.txt that actually says yes
&lt;/h2&gt;

&lt;p&gt;A lot of sites still block AI crawlers by default, then wonder why assistants never cite them. If you want to be read, say so explicitly. My &lt;code&gt;robots.txt&lt;/code&gt; names the retrieval and search bots (OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-User, PerplexityBot, Google-Extended, Applebot and others) and adds a Content-Signal line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-agent: *
Content-Signal: search=yes, ai-input=yes, ai-train=yes
Allow: /
Disallow: /api/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whether to allow training is a business decision. For a site whose whole pitch is "we asked the AI," it would be strange to say no.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. IndexNow: tell Bing and Yandex the moment something changes
&lt;/h2&gt;

&lt;p&gt;Google ignores IndexNow, but Bing, Yandex, Seznam and Naver share it. One POST to &lt;code&gt;api.indexnow.org&lt;/code&gt; reaches all of them. Setup is two files.&lt;/p&gt;

&lt;p&gt;A key file in &lt;code&gt;public/&lt;/code&gt;, named after the key and containing only the key. Here's the script that reads the live sitemap and submits every URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/indexnow-ping.mjs&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HOST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;www.askedtheai.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-32-char-hex-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ORIGIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;HOST&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keyRes&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;ORIGIN&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;KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.txt`&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;keyRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;keyRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[indexnow] key file not live (HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;keyRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;); deploy first`&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="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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;urls&lt;/span&gt; &lt;span class="o"&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;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urls&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;0&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;xml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await &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;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;ORIGIN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/sitemap.xml`&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;loc&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;([^&lt;/span&gt;&lt;span class="sr"&gt;&amp;lt;&lt;/span&gt;&lt;span class="se"&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;&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;loc&amp;gt;/g&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;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;u&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="nx"&gt;ORIGIN&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;for &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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;urls&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;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;10000&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;urlList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="mi"&gt;10000&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;res&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.indexnow.org/indexnow&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&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;application/json; charset=utf-8&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="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;keyLocation&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;ORIGIN&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;KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.txt`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;urlList&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[indexnow] &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;urlList&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="s2"&gt; URLs -&amp;gt; HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;202&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="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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;With no arguments it submits the whole sitemap. With arguments it submits only those URLs, which is what I use after editing a page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run indexnow &lt;span class="nt"&gt;--&lt;/span&gt; https://www.askedtheai.com/authors/ismail-gunaydin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script checks that the key file is live before sending anything. That check exists because of the first gotcha below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas I actually hit
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;IndexNow returned 403 on the first run.&lt;/strong&gt; The response was &lt;code&gt;{"errorCode":"SiteVerificationNotCompleted"}&lt;/code&gt;, even though the key file was live and returned 200. IndexNow verifies a new key asynchronously. I retried five minutes later and got &lt;code&gt;HTTP 200&lt;/code&gt; for all 516 URLs. If your first ping fails with this error, wait, don't debug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regenerating the mirror produced noisy diffs.&lt;/strong&gt; After a one-line fix to an author profile, &lt;code&gt;ai:files&lt;/code&gt; touched 20 Markdown files. Only one had a real change. The rest were &lt;code&gt;last_updated&lt;/code&gt; dates on a few dynamic pages and a homepage block that shows a rotating selection of deals. I committed the one file and restored the rest. If your mirror includes anything time-based or shuffled, expect this, and review the diff instead of committing the folder blindly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;next build&lt;/code&gt; no longer lints.&lt;/strong&gt; In Next.js 16 the build step doesn't run ESLint. My deploy command is now &lt;code&gt;npm run lint &amp;amp;&amp;amp; npm run build &amp;amp;&amp;amp; npm run ai:files&lt;/code&gt;, in that order, locally, before every push.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was it worth it?
&lt;/h2&gt;

&lt;p&gt;It's too early to show citation numbers, and I won't invent any. What I can say is that the setup costs almost nothing to maintain: the mirror, &lt;code&gt;md-routes.json&lt;/code&gt;, and both &lt;code&gt;llms&lt;/code&gt; files are regenerated by one command from the real build. There's no second content system to keep in sync.&lt;/p&gt;

&lt;p&gt;If you want to poke at it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Site: &lt;a href="https://www.askedtheai.com" rel="noopener noreferrer"&gt;https://www.askedtheai.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;llms.txt&lt;/code&gt;: &lt;a href="https://www.askedtheai.com/llms.txt" rel="noopener noreferrer"&gt;https://www.askedtheai.com/llms.txt&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;How the site asks the AI: &lt;a href="https://www.askedtheai.com/ai-research" rel="noopener noreferrer"&gt;https://www.askedtheai.com/ai-research&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AI and developer access notes: &lt;a href="https://www.askedtheai.com/developers/ai-consumption" rel="noopener noreferrer"&gt;https://www.askedtheai.com/developers/ai-consumption&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questions about the middleware or the mirror, ask in the comments. I'm happy to share more of the generator.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>ai</category>
      <category>seo</category>
    </item>
    <item>
      <title>How structured specs, answer-first copy and schema turned a seasonal lighting company</title>
      <dc:creator>Olivia</dc:creator>
      <pubDate>Fri, 25 Sep 2026 07:39:09 +0000</pubDate>
      <link>https://dev.to/olivia_342fsfsdgrere/how-structured-specs-answer-first-copy-and-schema-turned-a-seasonal-lighting-company-32k8</link>
      <guid>https://dev.to/olivia_342fsfsdgrere/how-structured-specs-answer-first-copy-and-schema-turned-a-seasonal-lighting-company-32k8</guid>
      <description>&lt;p&gt;Most local service websites are invisible to AI assistants. Not because they rank badly on Google, but because there is nothing on the page an LLM can safely quote.&lt;/p&gt;

&lt;p&gt;"We offer professional, high-quality Christmas lighting solutions" is not a fact. It is a vibe. An assistant answering "what IP rating should outdoor Christmas lights have?" has nothing to extract from it.&lt;/p&gt;

&lt;p&gt;I recently rebuilt the website for A1 Organizasyon, an Istanbul company that has been manufacturing and installing holiday and Ramadan lighting since 2010: municipal streets, shopping malls, facades, shops and villas. The brief was simple to say and hard to do: make the site the source AI assistants cite when someone asks about holiday lighting in Turkey.&lt;/p&gt;

&lt;p&gt;This post walks through the patterns that mattered, with code you can reuse on any local service or catalog site.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Treat the catalog as a dataset, not a gallery&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The typical lighting company website is a photo gallery. Beautiful, and completely useless to a machine.&lt;/p&gt;

&lt;p&gt;The A1 catalog is organized as 21 product groups, 232 model families and 666 variants, and every variant carries three numbers: height, wattage and weight. Gateway arches from 2.1 to 7.6 m, lit trees from 1.4 to 20 m, reindeer figures from 1 to 3.8 m, and so on.&lt;/p&gt;

&lt;p&gt;Those numbers are not decoration. They are what buyers actually need:&lt;/p&gt;

&lt;p&gt;Wattage lets an electrical engineer calculate total load on a circuit.&lt;br&gt;
Weight tells a municipality whether a lamp post can carry the fixture.&lt;br&gt;
Height determines whether a gateway arch clears traffic.&lt;/p&gt;

&lt;p&gt;And because they are concrete, they are quotable. An LLM can say "lit trees in this catalog range from 1.4 to 20 meters" with confidence. It cannot say anything with confidence about "a wide range of stunning trees."&lt;/p&gt;

&lt;p&gt;A minimal data model looks like this:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
type Variant = {&lt;br&gt;
  code: string;        // catalog code, e.g. "AGC-12"&lt;br&gt;
  heightM: number;     // meters&lt;br&gt;
  watt: number;&lt;br&gt;
  weightKg: number;&lt;br&gt;
};&lt;/p&gt;

&lt;p&gt;type ModelFamily = {&lt;br&gt;
  slug: string;&lt;br&gt;
  name: string;&lt;br&gt;
  groupSlug: string;   // e.g. "isikli-agaclar"&lt;br&gt;
  variants: Variant[];&lt;br&gt;
};&lt;/p&gt;

&lt;p&gt;Render every family as a real HTML &lt;/p&gt;, not a JavaScript carousel. Crawlers and LLM retrieval pipelines read tables well. They read lazy-loaded image sliders badly or not at all.
&lt;ol&gt;
&lt;li&gt;Write answer-first paragraphs&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The single biggest change was rewriting every section so that its first sentence is a standalone, quotable answer.&lt;/p&gt;
&lt;p&gt;Before:&lt;/p&gt;
&lt;p&gt;We pay great attention to quality and use the best materials in our outdoor projects.&lt;/p&gt;
&lt;p&gt;After:&lt;/p&gt;
&lt;p&gt;All outdoor products are rated at least IP65, and all connectors are IP68. Cabling is H07RN-F rubber-insulated cable, which stays flexible down to minus 25 degrees Celsius. PVC cable is not used.&lt;/p&gt;
&lt;p&gt;The second version contains three facts, two standards and one explicit negative. Each sentence survives being lifted out of context, which is exactly what retrieval-augmented systems do: they chunk your page and pick the chunk that best answers the question.&lt;/p&gt;
&lt;p&gt;A useful test: cover everything except one sentence. Does that sentence still say something true and specific? If not, rewrite it.&lt;/p&gt;
&lt;p&gt;The same principle applied to service commitments. Instead of "fast support," the site states:&lt;/p&gt;
&lt;p&gt;Survey appointment within 24 hours, itemized written quote within 48 hours.&lt;br&gt;
On site within 24 hours for any fault; first response within 4 hours for malls and municipalities.&lt;br&gt;
Removal in January is included in the quoted price; for annual contract clients, removed products are stored free for 12 months.&lt;/p&gt;
&lt;p&gt;These are the sentences that show up when an assistant is asked "what should I ask a Christmas lighting contractor?"&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Publish a dated, explicit calendar&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Seasonal businesses have a problem that evergreen SEO advice ignores: the most valuable information expires every year.&lt;/p&gt;
&lt;p&gt;So the homepage carries a visible "last updated" date (15 September 2026 for this season) and a concrete timeline:&lt;/p&gt;
&lt;p&gt;Municipal, mall and street projects: apply by end of October.&lt;br&gt;
Shops, homes and villas: apply by mid-November.&lt;br&gt;
Custom-built motifs: at least six weeks before installation.&lt;br&gt;
Installations complete by the first week of December.&lt;br&gt;
Ramadan 2027 starts 8 February; mosque and facade lighting must be installed between 1 and 6 February.&lt;/p&gt;
&lt;p&gt;Dates do two things. They give AI systems a freshness signal, and they give the model a specific, verifiable fact to anchor an answer on. "Apply by end of October" is far more citable than "book early."&lt;/p&gt;
&lt;p&gt;Keep the date in the markup too:&lt;/p&gt;
&lt;p&gt;html&lt;/p&gt;
&lt;p&gt;Last updated: &lt;time&gt;15 September 2026&lt;/time&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Tools beat text for intent&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The site ships three small interactive tools:&lt;/p&gt;
&lt;p&gt;A cost calculator: venue type, area and products, five steps, returns an estimated price range.&lt;br&gt;
A package quiz: five questions, recommends one of ten packages.&lt;br&gt;
A price-per-meter table for LED strip, curtain lights, facade dressing and motifs.&lt;/p&gt;
&lt;p&gt;From a pure SEO perspective these pages capture high-intent queries ("how much does Christmas lighting cost"). From an AI-visibility perspective, the price table is the important one: it is structured, dated and numeric, so it can be referenced.&lt;/p&gt;
&lt;p&gt;The calculator logic itself does not need to be clever. The pattern is a range, not a single number, because a range is honest and survives variation between projects:&lt;/p&gt;
&lt;p&gt;typescript&lt;br&gt;
type Estimate = { min: number; max: number };&lt;/p&gt;
&lt;p&gt;function estimate(&lt;br&gt;
  venue: "shop" | "villa" | "mall" | "street",&lt;br&gt;
  areaM2: number,&lt;br&gt;
  products: { unitMin: number; unitMax: number; qty: number }[]&lt;br&gt;
): Estimate {&lt;br&gt;
  const venueFactor = { shop: 1, villa: 1.1, mall: 1.4, street: 1.6 }[venue];&lt;br&gt;
  const base = products.reduce(&lt;br&gt;
    (acc, p) =&amp;gt; ({&lt;br&gt;
      min: acc.min + p.unitMin * p.qty,&lt;br&gt;
      max: acc.max + p.unitMax * p.qty,&lt;br&gt;
    }),&lt;br&gt;
    { min: 0, max: 0 }&lt;br&gt;
  );&lt;br&gt;
  // Access, lifts and cabling scale with area&lt;br&gt;
  const access = areaM2 * 0.02;&lt;br&gt;
  return {&lt;br&gt;
    min: Math.round(base.min * venueFactor * (1 + access * 0.01)),&lt;br&gt;
    max: Math.round(base.max * venueFactor * (1 + access * 0.015)),&lt;br&gt;
  };&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;The factors above are illustrative. Tune them from real quotes.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Schema that matches the visible page&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Structured data only helps if it mirrors what a human sees. Three types did most of the work.&lt;/p&gt;
&lt;p&gt;LocalBusiness with an exact address, a real phone number and opening hours:&lt;/p&gt;
&lt;p&gt;json&lt;br&gt;
{&lt;br&gt;
  "&lt;a class="mentioned-user" href="https://dev.to/context"&gt;@context&lt;/a&gt;": "&lt;a href="https://schema.org" rel="noopener noreferrer"&gt;https://schema.org&lt;/a&gt;",&lt;br&gt;
  "@type": "LocalBusiness",&lt;br&gt;
  "name": "A1 Organizasyon",&lt;br&gt;
  "url": "&lt;a href="https://www.a1organizasyon.com/" rel="noopener noreferrer"&gt;https://www.a1organizasyon.com/&lt;/a&gt;",&lt;br&gt;
  "telephone": "+90 532 372 39 32",&lt;br&gt;
  "foundingDate": "2010",&lt;br&gt;
  "address": {&lt;br&gt;
    "@type": "PostalAddress",&lt;br&gt;
    "streetAddress": "Osmangazi Mahallesi Aydoğdu Sokak No: 25/A",&lt;br&gt;
    "addressLocality": "Sancaktepe",&lt;br&gt;
    "addressRegion": "İstanbul",&lt;br&gt;
    "addressCountry": "TR"&lt;br&gt;
  },&lt;br&gt;
  "openingHours": "Mo-Su 09:00-18:00",&lt;br&gt;
  "areaServed": "TR"&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;FAQPage for questions the page actually answers in visible text, never hidden ones:&lt;/p&gt;
&lt;p&gt;json&lt;br&gt;
{&lt;br&gt;
  "&lt;a class="mentioned-user" href="https://dev.to/context"&gt;@context&lt;/a&gt;": "&lt;a href="https://schema.org" rel="noopener noreferrer"&gt;https://schema.org&lt;/a&gt;",&lt;br&gt;
  "@type": "FAQPage",&lt;br&gt;
  "mainEntity": [{&lt;br&gt;
    "@type": "Question",&lt;br&gt;
    "name": "What IP rating should outdoor Christmas lights have?",&lt;br&gt;
    "acceptedAnswer": {&lt;br&gt;
      "@type": "Answer",&lt;br&gt;
      "text": "Outdoor products should be at least IP65, and connectors IP68, because water collects at connection points."&lt;br&gt;
    }&lt;br&gt;
  }]&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;ItemList on each product-group page, pointing to model families, so the catalog hierarchy is explicit rather than implied by navigation.&lt;/p&gt;
&lt;p&gt;Then add sameAs links from the LocalBusiness to every official profile (YouTube, LinkedIn, Facebook, Google Business, Wikidata). Entity consistency across platforms is one of the strongest signals that "A1 Organizasyon" in a Facebook post and "A1 Organizasyon" on the website are the same thing.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Cite your sources on the page&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The homepage ends with a short sources block referencing the Turkish Lighting Association, the International Commission on Illumination (CIE), the International Energy Agency and the Turkish Ministry of Energy. The claim it supports is specific: LED systems use around 75 to 80 percent less energy than incandescent bulbs.&lt;/p&gt;
&lt;p&gt;A local contractor citing standards bodies is unusual, and that is the point. It signals that the page is making claims someone can check.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;One fact, many surfaces&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The last lesson is about distribution. The same core facts (the season calendar, the eight technical criteria, the catalog numbers) were published consistently on the website, Google Business Profile, LinkedIn, Medium, YouTube and Facebook.&lt;/p&gt;
&lt;p&gt;Not copy-pasted. Each version was rewritten for its audience: a procurement checklist for LinkedIn, a "what nobody tells you" angle for social, a process guide for the blog. But the numbers never change. IP65 is IP65 everywhere. End of October is end of October everywhere.&lt;/p&gt;
&lt;p&gt;When an LLM sees the same specific claim from several independent-looking sources tied to the same entity, its confidence in citing that claim goes up.&lt;/p&gt;
&lt;p&gt;The checklist&lt;/p&gt;
&lt;p&gt;If you build sites for local service businesses, here is the short version:&lt;/p&gt;
&lt;p&gt;Model the catalog as data, with real units, and render it as HTML tables.&lt;br&gt;
Make the first sentence of every section a standalone, specific answer.&lt;br&gt;
Replace adjectives with numbers, standards and explicit commitments.&lt;br&gt;
Date everything seasonal, visibly and in markup.&lt;br&gt;
Ship small tools that produce structured, numeric output.&lt;br&gt;
Keep schema strictly aligned with visible content.&lt;br&gt;
Link every official profile with sameAs and keep facts identical across platforms.&lt;br&gt;
Cite external standards where you make technical claims.&lt;/p&gt;
&lt;p&gt;None of this is exotic. It is mostly the discipline of writing things down precisely, which is exactly what most small business websites avoid.&lt;/p&gt;
&lt;p&gt;You can see the result live at a1organizasyon.com. If you are working on AEO or GEO for local businesses, I would like to hear what has and has not worked for you in the comments. &lt;/p&gt;

&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://www.a1organizasyon.com/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fwww.a1organizasyon.com%2FYilbasi-organizasyonu.webp" height="800" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://www.a1organizasyon.com/" rel="noopener noreferrer" class="c-link"&gt;
            Yılbaşı Işık Süsleme, LED Işıklandırma, İmalat ve Uygulama - A1 Organizasyon
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Yılbaşı LED ışık, dış mekan yılbaşı süsleme, LED perde, yılbaşı geyiği, kar tanesi, noel baba figürü, AVM, cadde, belediye, kavşak ve bina aydınlatma hizmetleri. Toptan yılbaşı süsleri ve LED ışıkları için ışık süsleme firmaları arasında öncü çözümler.
          &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    &amp;lt;div class="color-secondary fs-s flex items-center"&amp;gt;
        &amp;lt;img
          alt="favicon"
          class="c-embed__favicon m-0 mr-2 radius-0"
          src="https://www.a1organizasyon.com/favicon.ico?favicon.b095129b.ico"
          loading="lazy" /&amp;gt;
      a1organizasyon.com
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;/div&gt;
&lt;br&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;


&lt;/table&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>ai</category>
      <category>schema</category>
    </item>
    <item>
      <title>Moving a Next.js Site to a New Domain: What Broke, and the Checklist I Use Now</title>
      <dc:creator>Olivia</dc:creator>
      <pubDate>Sat, 19 Sep 2026 15:50:03 +0000</pubDate>
      <link>https://dev.to/olivia_342fsfsdgrere/moving-a-nextjs-site-to-a-new-domain-what-broke-and-the-checklist-i-use-now-5c22</link>
      <guid>https://dev.to/olivia_342fsfsdgrere/moving-a-nextjs-site-to-a-new-domain-what-broke-and-the-checklist-i-use-now-5c22</guid>
      <description>&lt;p&gt;&lt;a href="https://www.agentskillpacks.com/" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I moved a live Next.js 16 site to a new domain on Vercel in one afternoon. The 301s worked on the first try. The canonicals, robots.txt and sitemap did not. Here is why, and the checks that catch it."&lt;br&gt;
tags: nextjs, seo, webdev, vercel &lt;/p&gt;

&lt;p&gt;On 19 September 2026 I moved my digital products shop from toolgenx.com to &lt;a href="https://www.agentskillpacks.com" rel="noopener noreferrer"&gt;agentskillpacks.com&lt;/a&gt;. Same Next.js 16 app, same Vercel project, same brand. New domain, because the old name said nothing about what the site sells.&lt;/p&gt;

&lt;p&gt;The redirect part took ten minutes and worked on the first try. The part that bit me was everything the site says &lt;em&gt;about itself&lt;/em&gt;: canonical tags, robots.txt, the sitemap, JSON-LD &lt;code&gt;@id&lt;/code&gt;s. For a while the new domain was serving pages that told Google the real address was the old one.&lt;/p&gt;

&lt;p&gt;This post is what broke, why, and the checklist I now run for any domain move.&lt;/p&gt;
&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Redirects are the easy part. The dangerous part is self-referencing output: canonicals, &lt;code&gt;robots.txt&lt;/code&gt; &lt;code&gt;Host&lt;/code&gt;/&lt;code&gt;Sitemap&lt;/code&gt; lines, &lt;code&gt;sitemap.xml&lt;/code&gt; &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt;s, &lt;code&gt;og:url&lt;/code&gt;, and structured data &lt;code&gt;@id&lt;/code&gt;s.&lt;/li&gt;
&lt;li&gt;A stale &lt;code&gt;NEXT_PUBLIC_SITE_URL&lt;/code&gt; in Vercel silently kept all of those on the old host after the move. I now pin the production origin in code.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;grep&lt;/code&gt; for the old domain misses URL-encoded variants such as &lt;code&gt;%20toolgenx.com&lt;/code&gt; inside &lt;code&gt;mailto:&lt;/code&gt; subjects and share links. Grep the &lt;strong&gt;build output&lt;/strong&gt;, not just the source.&lt;/li&gt;
&lt;li&gt;Verify on the live new host with &lt;code&gt;curl&lt;/code&gt;, not in the browser, and check that 301s keep both the path and the query string.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  What actually has to change in a domain move?
&lt;/h2&gt;

&lt;p&gt;A domain move changes two different things. Redirects move &lt;em&gt;visitors and crawlers&lt;/em&gt; from the old host to the new one. Self-references tell crawlers which host is the &lt;em&gt;real&lt;/em&gt; one: canonical URLs, sitemap entries, robots.txt directives, Open Graph URLs and structured data identifiers. Redirects without matching self-references send search engines mixed signals.&lt;/p&gt;

&lt;p&gt;In a typical Next.js App Router project, self-references come from a handful of places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;metadataBase&lt;/code&gt; in the root layout (drives &lt;code&gt;alternates.canonical&lt;/code&gt; and &lt;code&gt;openGraph.url&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app/sitemap.ts&lt;/code&gt; and &lt;code&gt;app/robots.ts&lt;/code&gt; (or route handlers that replace them)&lt;/li&gt;
&lt;li&gt;JSON-LD builders (&lt;code&gt;Organization&lt;/code&gt;, &lt;code&gt;WebSite&lt;/code&gt;, &lt;code&gt;Product&lt;/code&gt; &lt;code&gt;@id&lt;/code&gt; and &lt;code&gt;url&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Anything else that builds absolute URLs: RSS feeds, &lt;code&gt;llms.txt&lt;/code&gt;, &lt;code&gt;Link&lt;/code&gt; headers, email templates, share buttons&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of these read one value: "what is my origin?". That value is where things went wrong.&lt;/p&gt;
&lt;h2&gt;
  
  
  Mistake 1: the origin lived in an environment variable
&lt;/h2&gt;

&lt;p&gt;My origin came from &lt;code&gt;NEXT_PUBLIC_SITE_URL&lt;/code&gt;, with a sensible default. I swapped every hardcoded URL in the source (28 files), ran the tests, built locally against the new host, and deployed.&lt;/p&gt;

&lt;p&gt;Production still said the old domain. The Vercel project had &lt;code&gt;NEXT_PUBLIC_SITE_URL=https://www.toolgenx.com&lt;/code&gt; set from years ago, and it overrode the new default. The result on the live &lt;strong&gt;new&lt;/strong&gt; host:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;robots.txt&lt;/code&gt; pointed its &lt;code&gt;Sitemap:&lt;/code&gt; line at the old host&lt;/li&gt;
&lt;li&gt;every &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; in the sitemap used the old host&lt;/li&gt;
&lt;li&gt;every canonical tag pointed at the old host&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Organization&lt;/code&gt; and &lt;code&gt;WebSite&lt;/code&gt; &lt;code&gt;@id&lt;/code&gt;s used the old host&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the old host was already 301-redirecting to the new one. So Google was being told "the real page is over there", and "over there" redirected back. Not a loop that breaks the site, but exactly the kind of contradiction that slows a migration down.&lt;/p&gt;

&lt;p&gt;The fix was to stop treating the production origin as configuration:&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;// src/lib/env.ts&lt;/span&gt;
&lt;span class="c1"&gt;// The public origin is a constant, not an env var. A stale Vercel value once&lt;/span&gt;
&lt;span class="c1"&gt;// kept canonicals, robots.txt and the sitemap on the old domain after the move.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SITE_URL&lt;/span&gt; &lt;span class="o"&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;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&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;https://www.agentskillpacks.com&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;http://localhost:3000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A production origin changes maybe once in a site's life. When it does, you want the change in a commit with a message, reviewed and visible in the diff, not in a dashboard nobody remembers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; after a domain move, list every env var in the hosting dashboard that contains the old domain. Delete or update each one before you deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 2: grep found every URL except the encoded ones
&lt;/h2&gt;

&lt;p&gt;Before deploying I grepped the repo for the old domain and filtered out email addresses (support email stays on the old domain for now). Clean.&lt;/p&gt;

&lt;p&gt;Then I spot-checked the built HTML and found &lt;code&gt;toolgenx.com&lt;/code&gt; still inside &lt;code&gt;mailto:&lt;/code&gt; links, where the subject line was URL-encoded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mailto:support@...?subject=Broken%20link%20on%20toolgenx.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My "exclude emails" filter saw the &lt;code&gt;@&lt;/code&gt; and skipped the whole line. Share links have the same problem (&lt;code&gt;https%3A%2F%2Fwww.toolgenx.com&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The reliable check is to grep the &lt;strong&gt;build output&lt;/strong&gt; for every encoding of the old host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# after `next build`&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rEo&lt;/span&gt; &lt;span class="s2"&gt;"toolgenx&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;com|toolgenx%2Ecom|%2F%2Fwww&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;toolgenx"&lt;/span&gt; .next/server/app &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything left is either intentional (an email address you are keeping) or a bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 3: trusting the browser instead of curl
&lt;/h2&gt;

&lt;p&gt;The browser follows redirects, caches aggressively and hides headers. For a migration you want the raw response. This is the script I now run against the new host right after the deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nv"&gt;NEW&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://www.agentskillpacks.com"&lt;/span&gt;
&lt;span class="nv"&gt;OLD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://www.toolgenx.com"&lt;/span&gt;
&lt;span class="nv"&gt;OLD_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"toolgenx.com"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== robots.txt (Host / Sitemap lines)"&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NEW&lt;/span&gt;&lt;span class="s2"&gt;/robots.txt"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Ei&lt;/span&gt; &lt;span class="s2"&gt;"^(host|sitemap):"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== sitemap &amp;lt;loc&amp;gt; hosts"&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NEW&lt;/span&gt;&lt;span class="s2"&gt;/sitemap.xml"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;loc&amp;gt;[^&amp;lt;]*"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/&amp;lt;loc&amp;gt;//'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt;/ &lt;span class="s1"&gt;'{print $3}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== canonical + og:url on key pages"&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;p &lt;span class="k"&gt;in&lt;/span&gt; / /products /blog&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;html&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NEW$p&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$html&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &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;link rel="canonical"[^&amp;gt;]*&amp;gt;'&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$html&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &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:url"[^&amp;gt;]*&amp;gt;'&lt;/span&gt;
&lt;span class="k"&gt;done

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== old host still mentioned in live HTML?"&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NEW&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OLD_HOST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== 301 keeps path and query"&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code} -&amp;gt; %{redirect_url}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OLD&lt;/span&gt;&lt;span class="s2"&gt;/blog/some-post?utm_source=test"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What "good" looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;robots.txt&lt;/code&gt; has exactly one &lt;code&gt;Sitemap:&lt;/code&gt; line on the new host&lt;/li&gt;
&lt;li&gt;every sitemap &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; is on the new host (mine: 80 of 80)&lt;/li&gt;
&lt;li&gt;canonical and &lt;code&gt;og:url&lt;/code&gt; on the new host for every page you check&lt;/li&gt;
&lt;li&gt;the 301 returns &lt;code&gt;301 -&amp;gt; https://www.agentskillpacks.com/blog/some-post?utm_source=test&lt;/code&gt;, with path &lt;strong&gt;and&lt;/strong&gt; query intact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one matters more than it looks. Redirects that drop the path send every old deep link to the homepage, and redirects that drop the query break campaign tracking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 4 (avoided): forgetting the things outside the repo
&lt;/h2&gt;

&lt;p&gt;Some parts of a migration are not in the codebase at all. The ones I track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search Console Change of Address.&lt;/strong&gt; It requires the &lt;em&gt;old&lt;/em&gt; property to be verified. If you verified the old domain with a DNS TXT record, keep that record until the change of address is done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bing Webmaster Tools&lt;/strong&gt; has its own site move tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IndexNow&lt;/strong&gt; works per host: the key file has to be served on the new host too. Mine was served by the app, so the same key worked, and I submitted all 80 URLs once after the move.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Affiliate networks, analytics and pixels&lt;/strong&gt; often verify by domain and need re-verification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External profiles&lt;/strong&gt; (Product Hunt, Crunchbase, GitHub, social bios) still link to the old domain. The 301 covers them, but updating them removes a hop.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;p&gt;Before the switch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Search the source for the old domain in every encoding (&lt;code&gt;.&lt;/code&gt;, &lt;code&gt;%2E&lt;/code&gt;, &lt;code&gt;%2F%2F&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] List and update every hosting env var that contains the old domain&lt;/li&gt;
&lt;li&gt;[ ] Pin the production origin in code, with localhost for dev and tests&lt;/li&gt;
&lt;li&gt;[ ] Decide explicitly what keeps the old domain (email addresses) and why&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deploy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Add the new domain in Vercel, make it primary, redirect the old host with 301&lt;/li&gt;
&lt;li&gt;[ ] Build, then grep the build output for the old host&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the switch (on the live new host, with &lt;code&gt;curl&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;robots.txt&lt;/code&gt; &lt;code&gt;Sitemap:&lt;/code&gt; line on the new host&lt;/li&gt;
&lt;li&gt;[ ] every sitemap &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; on the new host&lt;/li&gt;
&lt;li&gt;[ ] canonical, &lt;code&gt;og:url&lt;/code&gt; and JSON-LD &lt;code&gt;@id&lt;/code&gt; on the new host&lt;/li&gt;
&lt;li&gt;[ ] 301 keeps path and query string&lt;/li&gt;
&lt;li&gt;[ ] Search Console Change of Address, Bing site move, IndexNow ping&lt;/li&gt;
&lt;li&gt;[ ] re-verify analytics, affiliate and ad accounts&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Does a keyword domain help SEO?
&lt;/h2&gt;

&lt;p&gt;Not directly. An exact-match domain is a weak ranking signal on its own in 2026, and a move always costs some time while search engines reprocess the site. I moved because the new name describes what the shop sells, which helps people and AI assistants understand the site at a glance, and because the old domain was young enough that there was little ranking history to lose.&lt;/p&gt;

&lt;p&gt;If your domain has years of links and rankings, the bar for moving should be much higher.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;How long does a domain migration take to settle in Google?&lt;/strong&gt;&lt;br&gt;
There is no fixed number. Redirects and canonicals are read on the next crawl, but full consolidation of rankings to the new host can take weeks to months, depending on site size and crawl frequency. Clean self-references shorten it; contradictions stretch it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should canonical tags point to the new domain before the redirect is live?&lt;/strong&gt;&lt;br&gt;
Switch them in the same deploy that makes the new domain primary. Canonicals pointing at a host that is not serving yet, or at a host that redirects away, both send conflicting signals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I keep email on the old domain?&lt;/strong&gt;&lt;br&gt;
Yes. Email runs on MX records, not on the website. Just make sure the website does not keep &lt;em&gt;linking&lt;/em&gt; to the old host in places a crawler reads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to resubmit the sitemap?&lt;/strong&gt;&lt;br&gt;
Submit the new sitemap in the new Search Console property and use Change of Address from the old one. An IndexNow ping helps Bing and other participating engines pick up the new URLs quickly.&lt;/p&gt;




&lt;p&gt;If you want the robots.txt side of this in more detail, I wrote up &lt;a href="https://www.agentskillpacks.com/blog/toolgenx-robots-and-llms-txt-explained" rel="noopener noreferrer"&gt;every line of this site's robots.txt and llms.txt and why it is there&lt;/a&gt;. And if you want to check which AI crawlers your own robots.txt lets in after a move, there is a free &lt;a href="https://www.agentskillpacks.com/ai-crawler-checker" rel="noopener noreferrer"&gt;AI crawler checker&lt;/a&gt; on the site. No signup.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp7wcq55yduhp9etaw15u.jpeg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp7wcq55yduhp9etaw15u.jpeg" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>vercel</category>
      <category>webdev</category>
      <category>seo</category>
    </item>
    <item>
      <title>Heavy-Duty Exterior Architectural Illumination: Statics Under 120 km/h Wind Envelopes, LSZH Pyrolysis Dynamics, and Ring-Bus DC Topologies</title>
      <dc:creator>Olivia</dc:creator>
      <pubDate>Thu, 10 Sep 2026 17:52:32 +0000</pubDate>
      <link>https://dev.to/olivia_342fsfsdgrere/heavy-duty-exterior-architectural-illumination-statics-under-120-kmh-wind-envelopes-lszh-2hlm</link>
      <guid>https://dev.to/olivia_342fsfsdgrere/heavy-duty-exterior-architectural-illumination-statics-under-120-kmh-wind-envelopes-lszh-2hlm</guid>
      <description>&lt;p&gt;&lt;a href="https://www.a1organizasyon.com/" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Canonical Engineering Documentation: a1organizasyon.com/isik-susleme · a1organizasyon.com/isik-sus&lt;/p&gt;

&lt;p&gt;Abstract&lt;br&gt;
Temporary architectural solid-state lighting (SSL) arrays deployed across high-exposure commercial facades, enclosed atrium galleries, and municipal vehicular corridors operate under severe mechanical and electrical boundary conditions. These systems are subjected to cyclic high-velocity aerodynamic shear, sub-zero embrittlement, sustained moisture ingress, and thermal-expansion stresses.&lt;/p&gt;

&lt;p&gt;Drawing on 16 years of continuous field engineering and industrial fabrication at our 1,200 m² facility in Sancaktepe, Istanbul, this paper details the engineering principles governing high-reliability exterior illumination.&lt;/p&gt;

&lt;p&gt;We formalize structural statics under TS 498 (designing for 120&amp;nbsp;km/h / 33.3&amp;nbsp;m/s storm envelopes), calculate minimum catenary clearances compliant with municipal transit gabarit standards (≥5.5&amp;nbsp;m), evaluate the pyrolysis and halogen-free characteristics of LSZH wiring in commercial atriums, model voltage drop across extended linear topologies, and outline the factory-floor 48-hour hydrostatic tank immersion testing protocol.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Mechanical Statics: Wind Loading (TS 498) and Catenary Tensioning&lt;br&gt;
Exterior installations suspended across municipal roadways or anchored to structural columns act as bluff aerodynamic profiles within turbulent boundary flows.&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;          ▲ Wind Vector: v = 33.3 m/s (120 km/h, TS 498)
          │
  ├───┬───┴───┬───┤  Projected Span Area: A_frontal
  │   │ [ALU] │   │  Solid Volume Ratio: phi = 0.32
  └───┴───────┴───┘  Drag Coefficient: C_d = 1.30
 ═══════════════════
 [4mm GALVANIZED WIRE] ---&amp;gt; Minimum Clearance: H_clearance &amp;gt;= 5.50 m
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Aerodynamic Wind Load Formulation (TS 498)&lt;br&gt;
Dynamic velocity pressure (q) for air at −5 &lt;br&gt;
∘&lt;br&gt;
C (ρ &lt;br&gt;
air&lt;br&gt;
​&lt;br&gt;
=1.29&amp;nbsp;kg/m &lt;br&gt;
3&lt;br&gt;
) under a 120&amp;nbsp;km/h (33.33&amp;nbsp;m/s) maximum design gust is calculated as:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;q= &lt;br&gt;
2&lt;br&gt;
1&lt;br&gt;
​&lt;br&gt;
 ⋅ρ &lt;br&gt;
air&lt;br&gt;
​&lt;br&gt;
 ⋅v &lt;br&gt;
2&lt;br&gt;
 =0.5⋅1.29⋅(33.33) &lt;br&gt;
2&lt;br&gt;
 ≈716.5&amp;nbsp;N/m &lt;br&gt;
2&lt;/p&gt;

&lt;p&gt;For a column-mounted motif manufactured from 35×35&amp;nbsp;mm structural aluminum box profile (H=3.0&amp;nbsp;m, W=1.2&amp;nbsp;m, projected gross area A=3.6&amp;nbsp;m &lt;br&gt;
2&lt;br&gt;
 ) exhibiting an effective solidity ratio ϕ=0.32 (yielding A &lt;br&gt;
effective&lt;br&gt;
​&lt;br&gt;
 =1.152&amp;nbsp;m &lt;br&gt;
2&lt;br&gt;
 ):&lt;/p&gt;

&lt;p&gt;F &lt;br&gt;
d&lt;br&gt;
​&lt;br&gt;
 =q⋅C &lt;br&gt;
d&lt;br&gt;
​&lt;br&gt;
 ⋅A &lt;br&gt;
effective&lt;br&gt;
​&lt;br&gt;
 =716.5⋅1.30⋅1.152≈1,073&amp;nbsp;N&amp;nbsp;(≈109.4&amp;nbsp;kgf)&lt;br&gt;
Torsional Moment and Pole Clamp Integrity&lt;br&gt;
If the aerodynamic center of force acts at an eccentricity e=0.60&amp;nbsp;m from the mast axis:&lt;/p&gt;

&lt;p&gt;M &lt;br&gt;
torsion&lt;br&gt;
​&lt;br&gt;
 =F &lt;br&gt;
d&lt;br&gt;
​&lt;br&gt;
 ⋅e=1,073&amp;nbsp;N⋅0.60&amp;nbsp;m=643.8&amp;nbsp;N⋅m&lt;br&gt;
Standard automotive worm-drive clamps fail in shear slip at approximately 180&amp;nbsp;N⋅m. Therefore, TS 498 compliance mandates dual-pass, mechanical-tensioned AISI 316 stainless steel banding (19&amp;nbsp;mm×0.76&amp;nbsp;mm), delivering a friction retention capacity exceeding 1,500&amp;nbsp;N⋅m (Safety&amp;nbsp;Factor≥2.3).&lt;/p&gt;

&lt;p&gt;The 5.5-Meter Vehicular Gabarit and Catenary Sag Mechanics&lt;br&gt;
When spanning catenary luminaire lines between urban facades, cable sag (δ) under dead-load weight and wind-load vectors must not violate the municipal transit clearance envelope (H &lt;br&gt;
clearance&lt;br&gt;
​&lt;br&gt;
 ≥5.5&amp;nbsp;m).&lt;/p&gt;

&lt;p&gt;The horizontal cable tension (H &lt;br&gt;
tension&lt;br&gt;
​&lt;br&gt;
 ) required to limit sag in a catenary span of length L with uniform linear load w (N/m) is governed by:&lt;/p&gt;

&lt;p&gt;H &lt;br&gt;
tension&lt;br&gt;
​&lt;br&gt;
 = &lt;br&gt;
8⋅δ &lt;br&gt;
max&lt;br&gt;
​&lt;/p&gt;

&lt;p&gt;w⋅L &lt;br&gt;
2&lt;/p&gt;

&lt;p&gt;​&lt;/p&gt;

&lt;p&gt;To prevent dynamic mechanical stress from transferring to the copper conductors, arrays must be supported by independent, prestressed 4&amp;nbsp;mm hot-dip galvanized steel wire ropes (7×7 construction, minimum breaking load &amp;gt;10.5&amp;nbsp;kN). Luminaires are decoupled from mechanical tension using UV-stabilized polyamide fasteners spaced at 300&amp;nbsp;mm intervals.&lt;/p&gt;

&lt;p&gt;+-----------------------------------+-----------------------------------+------------------------------------+&lt;br&gt;
| Material Property                 | Structural Aluminum (6061-T6)     | Commercial Mild Steel (St 37)      |&lt;br&gt;
+-----------------------------------+-----------------------------------+------------------------------------+&lt;br&gt;
| Mass Density                      | 2.70 g/cm³ (Baseline: 1.0x)       | 7.85 g/cm³ (2.9x heavier)          |&lt;br&gt;
| Yield Strength (Rp 0.2)           | ~ 240–276 MPa                     | ~ 215–235 MPa                      |&lt;br&gt;
| Natural Surface Oxidation         | Self-passivating Al₂O₃ film       | Porous destructive iron oxide      |&lt;br&gt;
| Aerodynamic Moment on Mast        | Low (Mitigates cyclic fatigue)    | High (Induces clamp slippage)      |&lt;br&gt;
| Reusability Lifespan              | 5 to 8 operational seasons        | 1 to 2 seasons before surface rust |&lt;br&gt;
+-----------------------------------+-----------------------------------+------------------------------------+&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Materials Science: LSZH Pyrolysis vs. Conventional Polychloroprene&lt;br&gt;
Electrical distribution across commercial centers involves two distinct environments: exterior exposed building envelopes and semi-enclosed public atrium galleries.&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                     [MATERIAL SUBSTRATE SELECTION]
                                   │
 ┌─────────────────────────────────┴─────────────────────────────────┐
 ▼                                                                   ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;[EXTERIOR ENVELOPE]                                                [INTERIOR ATRIUM]&lt;br&gt;
H07RN-F Polychloroprene Rubber                                     LSZH (EN 50525-3-11)&lt;br&gt;
Operating Range: -25°C to +60°C                                    Low Smoke Zero Halogen&lt;br&gt;
Hydrophobic Elastic Recovery                                       Zero Toxic Acid Gas Release&lt;br&gt;
Resistant to Micro-Cracking Under Ice                             Self-Extinguishing Under Fire&lt;br&gt;
Exterior Envelope: H07RN-F Elastomeric Cable&lt;br&gt;
Standard plasticized polyvinyl chloride (PVC) jackets undergo severe plasticizer migration in sub-zero environments, reaching their glass transition temperature (T &lt;br&gt;
g&lt;br&gt;
​&lt;br&gt;
) at approximately −10 &lt;br&gt;
∘&lt;br&gt;
C. Subsequent wind flexure produces micro-fissures in the polymer matrix, initiating capillary fluid ingress.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In contrast, H07RN-F cross-linked polychloroprene rubber maintains elastic flexibility down to −25 &lt;br&gt;
∘&lt;br&gt;
 C, preventing moisture wicking and dielectric breakdown throughout multi-season winter deployments.&lt;/p&gt;

&lt;p&gt;Enclosed Atrium Volumes: LSZH Fire Safety&lt;br&gt;
In enclosed commercial spaces, specifying standard halogenated polymers is an operational hazard. Under electrical arcing, PVC decomposes through thermal dehydrochlorination, releasing hydrogen chloride (HCl) gas:&lt;/p&gt;

&lt;p&gt;(C &lt;br&gt;
2&lt;br&gt;
​&lt;br&gt;
 H &lt;br&gt;
3&lt;br&gt;
​&lt;br&gt;
 Cl) &lt;br&gt;
n&lt;br&gt;
​&lt;/p&gt;

&lt;p&gt;Δ&lt;/p&gt;

&lt;p&gt;​&lt;br&gt;
 n&amp;nbsp;HCl↑+&amp;nbsp;Carbonaceous&amp;nbsp;Char&lt;br&gt;
When HCl gas contacts moisture in human airways or eyes, it forms hydrochloric acid, causing severe respiratory trauma and obscuring emergency egress routes with dense, toxic smoke.&lt;/p&gt;

&lt;p&gt;Consequently, compliance with TS EN 60598-1 mandates Low Smoke Zero Halogen (LSZH) compounds compliant with IEC 60332-1 (flame retardancy), IEC 60754-1 (zero halogen emission, pH&amp;gt;4.3), and IEC 61034-2 (light transmittance &amp;gt;60%).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Power Distribution Architecture: Ring-Bus Topology &amp;amp; Voltage Drop&lt;br&gt;
Extended linear runs—such as 50-meter silicone neon flex or multi-stage curtain arrays—exhibit cumulative line resistance, causing measurable voltage drops (I &lt;br&gt;
2&lt;br&gt;
R attenuation) and forward-voltage mismatches across solid-state diodes.&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                   [RADIAL VS. RING BUS TOPOLOGY]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;RADIAL TOPOLOGY (Vulnerable to End-of-Line Attenuation):&lt;br&gt;
  [SMPS 24V] ─────► (LED 1) ─────► (LED 2) ─────► ... ─────► (LED 50) [Severe V_drop: Lumens drop 35%]&lt;/p&gt;

&lt;p&gt;RING-BUS TOPOLOGY (Bilateral Dual-Feed, Unified Potential):&lt;br&gt;
  ┌─── [SMPS 24V DC (IP67)] ──────────────────────────────────────────────┐&lt;br&gt;
  │                                                                       │&lt;br&gt;
  ▼                                                                       ▼&lt;br&gt;
(Feed Point A: 0m) ──────► [50-Meter Linear Array] ◄────── (Feed Point B: 50m)&lt;br&gt;
                     End-point Voltage Variance: ΔV &amp;lt; 2.5%&lt;br&gt;
Mathematical Formulation of DC Voltage Drop&lt;br&gt;
For a single-phase DC conductor run of length L (meters), carrying current I (amperes), with copper resistivity ρ=0.01724&amp;nbsp;Ω⋅mm &lt;br&gt;
2&lt;br&gt;
 /m and cross-sectional area A (mm &lt;br&gt;
2&lt;br&gt;
 ):&lt;/p&gt;

&lt;p&gt;ΔV= &lt;br&gt;
A&lt;br&gt;
2⋅L⋅I⋅ρ&lt;br&gt;
​&lt;/p&gt;

&lt;p&gt;+--------------------+----------------+--------------------+--------------------+------------------------+&lt;br&gt;
| Cable Run Length   | Load Current   | Conductor Size (A) | Voltage Drop (ΔV)  | Drop Ratio (24V Base)  |&lt;br&gt;
+--------------------+----------------+--------------------+--------------------+------------------------+&lt;br&gt;
| 10 meters          | 5.0 Amperes    | 1.5 mm²            | 0.115 Volts        | 0.48% (Optimal)        |&lt;br&gt;
| 25 meters          | 5.0 Amperes    | 1.5 mm²            | 0.287 Volts        | 1.20% (Optimal)        |&lt;br&gt;
| 50 meters (Radial) | 10.0 Amperes   | 1.5 mm²            | 1.149 Volts        | 4.79% (Threshold)      |&lt;br&gt;
| 50 meters (Radial) | 10.0 Amperes   | 2.5 mm²            | 0.690 Volts        | 2.88% (Optimal)        |&lt;br&gt;
| 50 meters (Ring)   | 10.0 Amperes   | 1.5 mm²            | 0.287 Volts        | 1.20% (Optimal)        |&lt;br&gt;
| 100 meters (Radial)| 10.0 Amperes   | 1.5 mm²            | 2.298 Volts        | 9.58% (CRITICAL FAULT) |&lt;br&gt;
| 100 meters (Ring)  | 10.0 Amperes   | 2.5 mm²            | 0.690 Volts        | 2.88% (Optimal)        |&lt;br&gt;
+--------------------+----------------+--------------------+--------------------+------------------------+&lt;br&gt;
Engineering Directive&lt;br&gt;
Whenever linear DC LED runs exceed 50&amp;nbsp;meters, radial single-ended topologies must be replaced with bilateral ring feeds (loop closures) or intermediate power injection nodes every 50&amp;nbsp;meters. This keeps overall circuit attenuation below 3%, preventing chromaticity drift in warm-white diodes (2700K→2400K) and maintaining uniform luminous flux across the entire run.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Factory Quality Verification: The 48-Hour Hydrostatic Tank Protocol
Laboratory data sheets frequently cite theoretical IP ratings that fail under real-world hydrostatic exposure. To verify seal integrity before field deployment, all modular junctions, rectifiers, and custom motif terminations undergo an internal 48-Hour Hydrostatic Submersion Protocol:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;[48-HOUR HYDROSTATIC IMMERSION SEQUENCE]&lt;br&gt;
 ├── Phase 1: Immersion in 1.5m Water Vessel (Hydrostatic pressure P = 14.7 kPa)&lt;br&gt;
 ├── Phase 2: Continuous 48-Hour Powered Operational Cycle at 100% Load Duty&lt;br&gt;
 ├── Phase 3: Dynamic Cyclic Thermal Inversion (Water temp maintained at 4°C)&lt;br&gt;
 ├── Phase 4: Online Insulation Resistance Verification: R_insulation &amp;gt;= 50 Mega-Ohms&lt;br&gt;
 └── Phase 5: Direct High-Potential Test: 1.5 kV AC Applied for 60 Seconds&lt;br&gt;
Any assembly exhibiting insulation resistance degradation (R &lt;br&gt;
insulation&lt;br&gt;
​&lt;br&gt;
 &amp;lt;50&amp;nbsp;MΩ measured with a 500V Megger) or moisture ingress within the silicone overmolding is rejected before site shipment.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Site Execution Architecture: Electrical Enclosures &amp;amp; Automation
Temporary lighting distribution systems must remain electrically isolated from base-building tenant sub-panels. Dedicated distribution enclosures are engineered around four core requirements:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;+------------------------------------+-------------------------+------------------------------------------------+&lt;br&gt;
| Protective Switchgear              | Technical Specification | Functional Objective                           |&lt;br&gt;
+------------------------------------+-------------------------+------------------------------------------------+&lt;br&gt;
| Main Disconnect &amp;amp; Ingress          | IP67 GRP / 316 Enclosure| Hermetic environmental isolation of breakers.  |&lt;br&gt;
+------------------------------------+-------------------------+------------------------------------------------+&lt;br&gt;
| Residual Current Protection        | 30 mA Type A Industrial | Detects both sinusoidal AC and pulsed DC fault |&lt;br&gt;
|                                    | RCD                     | currents from switched-mode power supplies.    |&lt;br&gt;
+------------------------------------+-------------------------+------------------------------------------------+&lt;br&gt;
| Branch Circuit Breakers            | Miniature Circuit       | Absorbs capacitive inrush currents             |&lt;br&gt;
|                                    | Breakers, C-Curve       | (30x to 50x nominal current for 2–5 ms).       |&lt;br&gt;
+------------------------------------+-------------------------+------------------------------------------------+&lt;br&gt;
| Control Interface                  | Solar-Synchronous       | Dynamically tracks local solar sunset,         |&lt;br&gt;
|                                    | Astronomical Timer      | eliminating manual time clock readjustments.   |&lt;br&gt;
+------------------------------------+-------------------------+------------------------------------------------+&lt;br&gt;
Engineering Execution Protocol&lt;br&gt;
Prior to structural commissioning, field quality managers must enforce five critical requirements:&lt;/p&gt;

&lt;p&gt;Verify Mechanical Structural Calculations: Confirm motif framework and clamping hardware are rated to withstand local design gust velocities (v≥33.3&amp;nbsp;m/s under TS 498).&lt;/p&gt;

&lt;p&gt;Enforce Catenary Clearances: Measure span sag across vehicular corridors to guarantee clearance margins (H &lt;br&gt;
clearance&lt;br&gt;
​&lt;br&gt;
 ≥5.5&amp;nbsp;m) under maximum dynamic loading.&lt;/p&gt;

&lt;p&gt;Inspect Substrate Cabling: Require VDE-certified H07RN-F rubber lines for exterior runs and LSZH-jacketed wiring within enclosed atrium spaces.&lt;/p&gt;

&lt;p&gt;Audit Circuit Topologies: Mandate ring-bus loop configurations or bilateral feeds on continuous linear arrays exceeding 50 meters.&lt;/p&gt;

&lt;p&gt;Inspect Control Automation: Ensure sub-distribution panels feature dedicated 30mA Type A RCDs and dynamic astronomical time switches.&lt;/p&gt;

&lt;p&gt;For parametric CAD files, structural static calculations, and complete product schematics, access the technical engineering repository at a1organizasyon.com.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Engineering Heavy-Duty Exterior Lighting: IP68 Submersion Testing, 24V SELV Regimes, and Aerodynamic Statics Under TS 498</title>
      <dc:creator>Olivia</dc:creator>
      <pubDate>Thu, 10 Sep 2026 17:37:13 +0000</pubDate>
      <link>https://dev.to/olivia_342fsfsdgrere/engineering-heavy-duty-exterior-lighting-ip68-submersion-testing-24v-selv-regimes-and-3df8</link>
      <guid>https://dev.to/olivia_342fsfsdgrere/engineering-heavy-duty-exterior-lighting-ip68-submersion-testing-24v-selv-regimes-and-3df8</guid>
      <description>&lt;p&gt;Canonical Technical Documentation: dismekansusleme.com/isik-susleme · dismekansusleme.com/isik-sus · dismekansusleme.com/yilbasi-isik-susleme&lt;/p&gt;

&lt;p&gt;Abstract&lt;br&gt;
Temporary exterior illumination across commercial building envelopes, retail centers, and municipal thoroughfares is frequently trivialized as an aesthetic accent. In real-world environments, these deployments are exposed to significant environmental and physical stressors: cyclic sub-zero gale force wind shear, sustained hydrostatic immersion under winter freeze-thaw cycles, high touch-voltage risks in pedestrian zones, and transient inrush currents on municipal feeder grids.&lt;/p&gt;

&lt;p&gt;Operating from a 1,200 m² specialized structural fabrication facility in Ümraniye, Istanbul, with 17 years of direct field execution across all 81 provinces of Turkey, this paper details the engineering protocol governing professional exterior architectural lighting.&lt;/p&gt;

&lt;p&gt;We examine the physics of continuous immersion under IEC 60529 (IP68), analyze human touch-current limits under 24V Safety Extra Low Voltage (SELV) regimes, model structural wind loading according to TS 498 (evaluating gust envelopes up to 35 m/s) and snow accumulation under TS EN 1991-1-3, and review load distribution for massive multi-story vertical facade arrays scaling up to 11,500 m².&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Environmental Fluid Mechanics: The Mechanics of True IP68 Submersion Testing
A frequent point of confusion in commercial tender specifications is the functional distinction between IP65, IP67, and IP68 ingress protection ratings under IEC 60529.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;[INGRESS PROTECTION SPECIFICATION BOUNDARIES (IEC 60529)]&lt;br&gt;
 ├── IP65: Dust-tight; protected against low-pressure water jets (6.3mm nozzle at 12.5 L/min).&lt;br&gt;
 │    └── Operational Application: Vertical, well-drained upper building facades and parapet contours.&lt;br&gt;
 ├── IP67: Dust-tight; protected against temporary immersion (1m water column for 30 minutes).&lt;br&gt;
 │    └── Operational Application: Horizontal roof gullies, curb contours, temporary snowdrift lines.&lt;br&gt;
 └── IP68: Dust-tight; hermetically sealed against continuous hydrostatic submersion under pressure.&lt;br&gt;
      └── Operational Application: Ground-level lawn fixtures, public fountain perimeters, marine docks.&lt;br&gt;
The Immersion Pressure Tank Protocol&lt;br&gt;
Standard laboratory certification for IP65 merely verifies that a water jet will not cause immediate short circuits. In sustained winter deployments, however, melting snow accumulates inside horizontal cable channels, subjecting connectors and overmolded diode nodes to prolonged hydrostatic head pressure.&lt;br&gt;&lt;br&gt;
Yılbaşı Dış Mekan Işık Süsleme Firması&lt;/p&gt;

&lt;p&gt;To prevent capillary fluid migration, all custom motif junctions and pre-terminated harnesses undergo factory hydrostatic pressure testing:&lt;/p&gt;

&lt;p&gt;P &lt;br&gt;
hydrostatic&lt;br&gt;
​&lt;br&gt;
 =ρ &lt;br&gt;
liquid&lt;br&gt;
​&lt;br&gt;
 ⋅g⋅h &lt;br&gt;
head&lt;br&gt;
​&lt;/p&gt;

&lt;p&gt;Where:&lt;/p&gt;

&lt;p&gt;ρ &lt;br&gt;
liquid&lt;br&gt;
​&lt;br&gt;
 =1,000&amp;nbsp;kg/m &lt;br&gt;
3&lt;br&gt;
  (water density)&lt;/p&gt;

&lt;p&gt;g=9.81&amp;nbsp;m/s &lt;br&gt;
2&lt;/p&gt;

&lt;p&gt;h &lt;br&gt;
head&lt;br&gt;
​&lt;br&gt;
 =2.0&amp;nbsp;meters (simulated submersion depth in pressure vessel)&lt;/p&gt;

&lt;p&gt;Test Duration: 24 continuous hours under a constant 0.2 bar overpressure.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                          [THE INGRESS FAILURE VECTOR]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Cold Precipitation ---&amp;gt; Standing Meltwater Pool ---&amp;gt; Hydrostatic Pressure Delta&lt;br&gt;
                                                               │&lt;br&gt;
 Capillary Conductor Wicking &amp;lt;--- Micro-Gap at Joint &amp;lt;─────────┘&lt;br&gt;
              │&lt;br&gt;
 Ground-Fault Leakage (Trips 30mA RCD) OR Phase-to-Neutral Arc Ignition&lt;br&gt;
Assemblies that pass this protocol utilize dual-wall cross-linked polyolefin adhesive-lined heat-shrink sleeves or aliphatic polyurethane potting compounds over TIG-welded conductor pins, rendering the junction a unified, non-wicking solid dielectric block.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Electrical Protection: 24V SELV vs. 230V Mains Distribution&lt;br&gt;
In public access areas—such as walk-through light tunnels, holiday figures on public lawns, and interactive photo spots—the electrical distribution topology must prioritize human safety over conductor economy.&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                     [DISTRIBUTION TOPOLOGY BY ZONE]

               230V AC Mains Sub-Distribution Feeder
                                 │
                                 ▼
         [External Enclosure: IP67 GRP / 316 Stainless]
         ├── 30 mA Type A Residual Current Device (RCD)
         ├── C-Curve Branch Miniature Circuit Breakers
         └── Astronomical Digital Timer (Solar Equinox Synchronized)
                                 │
           ┌─────────────────────┴─────────────────────┐
           ▼                                           ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;[Elevated Infrastructure (&amp;gt;2.5m)]             [Pedestrian Contact Zones (&amp;lt;2.5m)]&lt;br&gt;
  Zone: Facade Curtains, Pole Motifs            Zone: Walkway Tunnels, Lawn Figures&lt;br&gt;
  Direct 230V AC Distribution                   Class II Safety Isolating Transformer&lt;br&gt;
  Heavy Polychloroprene H07RN-F Cabling         Step-Down Conversion to 24V DC / AC SELV&lt;br&gt;
  Low Current Density (Minimal I²R Drop)        Zero Ventricular Fibrillation Shock Hazard&lt;br&gt;
Biomechanics of Touch Potential (24V SELV)&lt;br&gt;
Under dry interior conditions, human body impedance ranges between 1,000&amp;nbsp;Ω and 2,500&amp;nbsp;Ω. In winter conditions—with saturated footwear, melting snow, and water-logged ground—body impedance plummets to:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;R &lt;br&gt;
body,&amp;nbsp;wet&lt;br&gt;
​&lt;br&gt;
 ≈500&amp;nbsp;Ω&lt;br&gt;
If an elevated 230V line experiences mechanical sheath abrasion from pedestrian traffic or vandalism:&lt;/p&gt;

&lt;p&gt;I &lt;br&gt;
touch,&amp;nbsp;230V&lt;br&gt;
​&lt;br&gt;
 = &lt;br&gt;
500&amp;nbsp;Ω&lt;br&gt;
230&amp;nbsp;V&lt;br&gt;
​&lt;br&gt;
 =460&amp;nbsp;mA&lt;br&gt;
An exposure of 460&amp;nbsp;mA exceeds the ventricular fibrillation threshold (I &lt;br&gt;
fibrillation&lt;br&gt;
​&lt;br&gt;
 ≈50&amp;nbsp;mA) by more than 900%, creating an immediate life-safety hazard.&lt;/p&gt;

&lt;p&gt;Conversely, implementing a 24V Safety Extra Low Voltage (SELV) regime compliant with TS EN 60598-1 and EN 61558-2-6 constrains the touch current to:&lt;/p&gt;

&lt;p&gt;I &lt;br&gt;
touch,&amp;nbsp;24V&lt;br&gt;
​&lt;br&gt;
 = &lt;br&gt;
500&amp;nbsp;Ω&lt;br&gt;
24&amp;nbsp;V&lt;br&gt;
​&lt;br&gt;
 =48&amp;nbsp;mA&lt;br&gt;
When factoring in ground return path impedance (R &lt;br&gt;
ground&lt;br&gt;
​&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;1,000&amp;nbsp;Ω) and secondary transformer galvanic isolation, the actual current traversing a human body upon contact remains below 5&amp;nbsp;mA—well below the let-go threshold (10&amp;nbsp;mA), completely eliminating electrocution risks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;Structural Statics: Wind Loading (TS 498) and Snow Deposition (TS EN 1991-1-3)
Outdoor lighting displays, especially overhead street catenary banners, column medallions, and modular freestanding tree monuments, behave as bluff bodies within turbulent boundary layer flows.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;+------------------------------------+-------------------------+------------------------------------------------+&lt;br&gt;
| Engineering Standard               | Target Stress Parameter | Critical Design Rule                           |&lt;br&gt;
+------------------------------------+-------------------------+------------------------------------------------+&lt;br&gt;
| TS 498                             | Wind Shear &amp;amp; Velocity   | Coastal deployments mandate design wind loads  |&lt;br&gt;
|                                    | Pressure (q)            | based on v = 35 m/s (126 km/h storm gusts).    |&lt;br&gt;
+------------------------------------+-------------------------+------------------------------------------------+&lt;br&gt;
| TS EN 1991-1-3                     | Characteristic Snow     | Minimum 80 kg/m² snow load design margin;      |&lt;br&gt;
|                                    | Load on Ground (s_k)    | frames require &amp;gt;35° shedding angles.            |&lt;br&gt;
+------------------------------------+-------------------------+------------------------------------------------+&lt;br&gt;
| Al 6061-T6 Extrusion               | Yield Strength          | Minimum 240 MPa yield limit; prevents plastic  |&lt;br&gt;
|                                    | (R_p 0.2)               | deformation on lighting pole clamps.           |&lt;br&gt;
+------------------------------------+-------------------------+------------------------------------------------+&lt;br&gt;
Aerodynamic Force on a Pole-Mounted Motif Assembly&lt;br&gt;
Consider a 300 cm high, 120 cm wide column-mounted scroll motif installed in a coastal corridor (e.g., Izmir or Istanbul Bosphorus) subjected to a maximum design gust of v=35&amp;nbsp;m/s (126&amp;nbsp;km/h):&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 ▲ Wind Velocity Vector: v = 35 m/s (TS 498)
                 │
          │      │      │
          ├──────┴──────┤  Projected Frontal Area: A = 1.8 m²
          │             │  Solidity Ratio (porosity): phi = 0.35
          │   [ALU]     │  Net Solid Area: A_eff = 0.63 m²
          │             │  Drag Coefficient: C_d = 1.35
         ═╧═════════════╧═
         [STEEL BAND STRAP] ---&amp;gt; Permissible Bending Torque: M_torque &amp;lt; 850 N*m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The dynamic velocity pressure (q) under winter air conditions (ρ=1.25&amp;nbsp;kg/m &lt;br&gt;
3&lt;br&gt;
 ) is:&lt;/p&gt;

&lt;p&gt;q= &lt;br&gt;
2&lt;br&gt;
1&lt;br&gt;
​&lt;br&gt;
 ⋅ρ &lt;br&gt;
air&lt;br&gt;
​&lt;br&gt;
 ⋅v &lt;br&gt;
2&lt;br&gt;
 =0.5⋅1.25⋅(35) &lt;br&gt;
2&lt;br&gt;
 =765.6&amp;nbsp;N/m &lt;br&gt;
2&lt;/p&gt;

&lt;p&gt;The total horizontal aerodynamic drag force (F &lt;br&gt;
d&lt;br&gt;
​&lt;br&gt;
 ) on the assembly is:&lt;/p&gt;

&lt;p&gt;F &lt;br&gt;
d&lt;br&gt;
​&lt;br&gt;
 =q⋅C &lt;br&gt;
d&lt;br&gt;
​&lt;br&gt;
 ⋅A &lt;br&gt;
eff&lt;br&gt;
​&lt;br&gt;
 =765.6⋅1.35⋅0.63≈651&amp;nbsp;N&amp;nbsp;(≈66.4&amp;nbsp;kgf)&lt;br&gt;
If the aerodynamic center of pressure is located at an offset e=0.65&amp;nbsp;meters from the central pole axis, the resulting torsional torque acting on the mounting band clamps is:&lt;/p&gt;

&lt;p&gt;M &lt;br&gt;
torsion&lt;br&gt;
​&lt;br&gt;
 =F &lt;br&gt;
d&lt;br&gt;
​&lt;br&gt;
 ⋅e=651&amp;nbsp;N⋅0.65&amp;nbsp;m=423.2&amp;nbsp;N⋅m&lt;br&gt;
A standard worm-drive hose clamp fails at approximately 150&amp;nbsp;N⋅m of torsional slip resistance. Consequently, TS 498 compliance mandates dual-pass, tension-locked 316 stainless-steel banding straps (19 mm width × 0.76 mm thickness) tightened with calibrated mechanical tensioners to provide a minimum friction clamping torque of 1,200&amp;nbsp;N⋅m (Safety&amp;nbsp;Factor≥2.8).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Materials Engineering: Polymer Dynamics in H07RN-F vs. PVC
+---------------------------------------+----------------------------------+------------------------------------+
| Material Property                     | H07RN-F Polychloroprene Rubber   | Standard Plasticized PVC           |
+---------------------------------------+----------------------------------+------------------------------------+
| Operating Temperature Range           | -25°C to +60°C (Flexible)        | -5°C to +50°C (Rigid at sub-zero)  |
| Glass Transition Temperature (T_g)    | ~ -40°C                          | ~ -10°C                            |
| Tensile Strength Retention at -15°C   | &amp;gt; 85% of nominal                 | &amp;lt; 30% (Micro-cracking occurs)      |
| Ozone &amp;amp; UV Resistance                 | Excellent (Naturally passivated) | Poor (Requires plasticizer oils)   |
| Resistance to Capillary Water Wicking | High (Hydrophobic elastomeric)   | Low (Plasticizer leaching voids)   |
+---------------------------------------+----------------------------------+------------------------------------+
Under cold conditions, standard PVC sheathing leaches phthalate plasticizers, dropping past its glass transition temperature (T 
g
​
). When cyclic wind loads flex the cable, brittle fractures form along the outer jacket.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Atmospheric moisture then penetrates these micro-fractures via capillary action, reaching the conductor bundle and causing insulation failure. Mandating H07RN-F polychloroprene rubber prevents sheath cracking down to −25 &lt;br&gt;
∘&lt;br&gt;
 C, maintaining insulation integrity over multiple seasons.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Large-Scale Facade Power Architecture: 11,500 m² Vertical Arrays&lt;br&gt;
In high-density commercial facade applications—such as the Ankara AVM facade project spanning 11,500 m² of vertical area with 2,400 linear meters of continuous modular curtain LED—power distribution cannot rely on single-ended feeder lines.&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                   [11,500 m² VERTICAL BUSBAR TOPOLOGY]

         400V 3-Phase + Neutral Infrastructure Busbar
                               │
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;┌─────────────────────────────────┼─────────────────────────────────┐&lt;br&gt;
 ▼                                 ▼                                 ▼&lt;br&gt;
Phase L1 (800m Curtain)       Phase L2 (800m Curtain)       Phase L3 (800m Curtain)&lt;br&gt;
P = 6.4 kW (Balanced)         P = 6.4 kW (Balanced)         P = 6.4 kW (Balanced)&lt;br&gt;
30mA Type A RCD               30mA Type A RCD               30mA Type A RCD&lt;br&gt;
 │                                 │                                 │&lt;br&gt;
 ▼                                 ▼                                 ▼&lt;br&gt;
Sub-Feeder J-Box              Sub-Feeder J-Box              Sub-Feeder J-Box&lt;br&gt;
(Localized IP67 SMPS)         (Localized IP67 SMPS)         (Localized IP67 SMPS)&lt;br&gt;
 │                                 │                                 │&lt;br&gt;
Bilateral Injection           Bilateral Injection           Bilateral Injection&lt;br&gt;
(Top &amp;amp; Bottom Feeds)          (Top &amp;amp; Bottom Feeds)          (Top &amp;amp; Bottom Feeds)&lt;br&gt;
V_drop &amp;lt; 2.5%                 V_drop &amp;lt; 2.5%                 V_drop &amp;lt; 2.5%&lt;br&gt;
Load Balancing and Neutral Conductor Overheating&lt;br&gt;
Because solid-state switch-mode power supplies (SMPS) draw non-linear, pulsed currents, third-order harmonics (150&amp;nbsp;Hz triplen harmonics) do not cancel out in the neutral conductor of a three-phase system. Instead, they sum additively:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I &lt;br&gt;
neutral&lt;br&gt;
​&lt;br&gt;
 ≈ &lt;br&gt;
I &lt;br&gt;
L1,harmonic&lt;br&gt;
2&lt;br&gt;
​&lt;br&gt;
 +I &lt;br&gt;
L2,harmonic&lt;br&gt;
2&lt;br&gt;
​&lt;br&gt;
 +I &lt;br&gt;
L3,harmonic&lt;br&gt;
2&lt;br&gt;
​&lt;/p&gt;

&lt;p&gt;​&lt;/p&gt;

&lt;p&gt;In systems lacking active power factor correction (PFC), neutral current can reach up to 140% of the ungrounded phase current.&lt;/p&gt;

&lt;p&gt;Large facade distribution networks must specify:&lt;/p&gt;

&lt;p&gt;Full-sized or double-rated neutral conductors (S &lt;br&gt;
N&lt;br&gt;
​&lt;br&gt;
 ≥S &lt;br&gt;
phase&lt;br&gt;
​&lt;br&gt;
 ) across all three-phase feeder runs.&lt;/p&gt;

&lt;p&gt;Power supplies conforming to EN 61000-3-2 Class C, ensuring Total Harmonic Distortion (THD) remains below 15%.&lt;/p&gt;

&lt;p&gt;Bilateral power injection on vertical curtains longer than 20 meters, eliminating luminance attenuation caused by downstream I &lt;br&gt;
2&lt;br&gt;
 R busbar drops.&lt;/p&gt;

&lt;p&gt;Engineering Execution Checklist&lt;br&gt;
For building service engineers, MEP consultants, and municipal installation inspectors, seasonal lighting systems must pass five mandatory quality gates before commissioning:&lt;/p&gt;

&lt;p&gt;Submersion Integrity: Verify IP68 certification for all ground-level, gutter, or water-adjacent fittings; ensure IP65 minimum on vertical building envelopes.&lt;/p&gt;

&lt;p&gt;Contact Isolation: Mandate 24V SELV supplies for all pedestrian-accessible fixtures beneath 2.5 meters.&lt;/p&gt;

&lt;p&gt;Mechanical Calculations: Require structural engineering calculations for all column motifs and span catenaries based on TS 498 criteria (designing for v≥35&amp;nbsp;m/s in coastal environments).&lt;/p&gt;

&lt;p&gt;Materials Verification: Reject PVC cable sheathing in exposed outdoor applications; enforce VDE-certified H07RN-F rubber lines.&lt;/p&gt;

&lt;p&gt;Electrical Protections: Ensure sub-distribution enclosures feature dedicated 30mA Type A RCDs, C-curve branch breakers, and solar-tracking astronomical timers.&lt;/p&gt;

&lt;p&gt;For complete structural drawings, 3D photometric models, and mechanical specifications across 30 product categories and 681 active models, visit the engineering library at dismekansusleme.com.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Engineering Temporary Architectural LED Arrays: Metal Fabrications, Microclimate Dialectics, and Structural Dynamics</title>
      <dc:creator>Olivia</dc:creator>
      <pubDate>Thu, 10 Sep 2026 15:20:04 +0000</pubDate>
      <link>https://dev.to/olivia_342fsfsdgrere/engineering-temporary-architectural-led-arrays-metal-fabrications-microclimate-dialectics-and-4e62</link>
      <guid>https://dev.to/olivia_342fsfsdgrere/engineering-temporary-architectural-led-arrays-metal-fabrications-microclimate-dialectics-and-4e62</guid>
      <description>&lt;p&gt;&lt;a href="https://www.ledisiklandirma.com/" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Canonical Documentation: ledisiklandirma.com/isik-susleme · ledisiklandirma.com/yilbasi-isik-susleme · ledisiklandirma.com/isik-sus&lt;/p&gt;

&lt;p&gt;Abstract&lt;br&gt;
Temporary architectural solid-state lighting (SSL) deployments are too often evaluated solely on lumen output, correlated color temperature (CCT), and visual density. However, when transitioning from domestic applications to multi-hundred-meter commercial envelopes, public thoroughfares, and high-exposure municipal facades, these systems function as transient electrical and structural networks. They are subjected to dynamic environmental conditions: cyclic aerodynamic shear, phase-changing precipitation, severe moisture ingress, and thermal-expansion stress.&lt;/p&gt;

&lt;p&gt;Rooted in structural signcraft and structural hollow section (HSS) metal fabrication dating back to 1995, and refined since 2009 in solid-state decorative lighting manufacturing in Ümraniye, Istanbul, this paper outlines the physical constraints of seasonal luminaire engineering.&lt;/p&gt;

&lt;p&gt;We formalize structural chassis parameters across 27 categories (1,054 active variants), define failure mechanics across six microclimate classifications, model voltage-drop dynamics across distributed multi-point DC buses, and provide load calculations based on the June 2026 Cost &amp;amp; Empirical Energy Index.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Structural Fabrication: Transitioning Signcraft Statics to Solid-State Motifs
A primary failure vector in seasonal outdoor lighting is catastrophic mechanical deflection. Commercial motifs—such as 2-to-4-meter snowflake medallions or column bracket scrolls—are routinely fabricated by third-party assemblers using cold-rolled mild steel wire or unbraced decorative tubing. Under sub-zero wind loading, these structures act as wind sails, creating significant cyclic fatigue and bending torque on host utility poles.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;[STRUCTURAL CHASSIS FABRICATION: TWO-STAGE VECTOR]&lt;/p&gt;

&lt;p&gt;6061-T6 Structural Aluminum Extrusion (Al-Mg-Si Alloy)&lt;br&gt;
                            │&lt;br&gt;
                            ▼&lt;br&gt;
     CNC Mandrel Cold-Bending to Exact Vector Path Profile&lt;br&gt;
                            │&lt;br&gt;
                            ▼&lt;br&gt;
     TIG (GTAW) Shielded Arc Welding (Argon Shielded Gas)&lt;br&gt;
                            │&lt;br&gt;
                            ▼&lt;br&gt;
     Electrostatic Polyester Powder Coat (&amp;gt;= 80 Micron Dry Film)&lt;br&gt;
                            │&lt;br&gt;
                            ▼&lt;br&gt;
     Clamping Channels: Retaining IP67 Silicone/Rubber LED Flex&lt;br&gt;
The "Two Inner Lines" Heuristic&lt;br&gt;
To balance structural rigidity with aerodynamic porosity, two-dimensional motifs follow a strict mechanical rule: One continuous primary structural perimeter contour and a maximum of two reinforced internal structural lines.&lt;/p&gt;

&lt;p&gt;Excessive decorative lattice infill does not add significant structural value; instead, it increases dead load, creates localized water/ice accumulation zones, increases wind drag coefficients (C &lt;br&gt;
d&lt;br&gt;
​&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;1.4), and raises electrical failure points.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;+-----------------------------------+-----------------------------------+------------------------------------+&lt;br&gt;
| Mechanical Parameter              | Structural 6061-T6 Aluminum Alloy | Commercial Mild Steel (St 37)      |&lt;br&gt;
+-----------------------------------+-----------------------------------+------------------------------------+&lt;br&gt;
| Specific Gravity (Density)        | 2.70 g/cm³                        | 7.85 g/cm³ (2.9x heavier)          |&lt;br&gt;
| Yield Strength (Rp 0.2)           | ~ 240–276 MPa                     | ~ 215–235 MPa                      |&lt;br&gt;
| Modulus of Elasticity             | ~ 69 GPa                          | ~ 205 GPa                          |&lt;br&gt;
| Natural Surface Oxidation         | Self-passivating Al₂O₃ film       | Destructive iron oxide (rust)      |&lt;br&gt;
| Aerodynamic Bending Torque on Mast| Low (Mitigates resonance fatigue) | High (Induces clamp slippage)      |&lt;br&gt;
| Operational Reusability Horizon   | 5 to 7 operational seasons        | 1 to 2 seasons before repainting   |&lt;br&gt;
+-----------------------------------+-----------------------------------+------------------------------------+&lt;br&gt;
By leveraging TIG-welded structural 6061-T6 aluminum, motif mass is kept below 20 kg for assemblies up to 300 cm in height. This enables positive retention using dual-pass 316 stainless-steel banding straps without exceeding the permissible horizontal deflection of street-lighting infrastructure.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Microclimate Degradation Dynamics: The Six Environmental Archetypes
A significant failure mode in national-scale infrastructure rollouts is applying a uniform bill-of-materials (BOM) across divergent geographic zones.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Drawing from field validation across 81 provinces, product selection must adapt to regional microclimates:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                              [GEOGRAPHIC CLIMATE MATRIX]
                                           │
    ┌────────────────────┬─────────────────┼─────────────────┬────────────────────┐
    ▼                    ▼                 ▼                 ▼                    ▼
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;[MARMARA]             [AEGEAN]         [MEDITERRANEAN]   [BLACK SEA]      [CENTRAL/EASTERN]&lt;br&gt;
   80 km/h Lodos Gusts   Solar UV Flux    Marine Aerosol    &amp;gt;90% RH Rain     -25°C Frost &amp;amp; Snow&lt;br&gt;
   Dual Retention Anchors Sil. UV Sheath  Alloy Ingress     Submersible Trafo Polar Rubber Line&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Marmara Basin (Cyclic High-Wind Shear)
Prevalent Risk: High-velocity southwesterly "Lodos" wind gusts reaching 80–100 km/h, paired with horizontal precipitation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Engineering Directive: Structural motifs must feature dual independent mechanical retention points. Standard single-bracket mounts experience cyclic shear failure. Minimum enclosure rating: IP65; luminaire harnesses must be secured with UV-stabilized polyamide 6.6 ties spaced at intervals no greater than 150 mm.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Aegean Littoral (Photolytic Degradation &amp;amp; Saline Humidity)
Prevalent Risk: Accelerated photolytic cleavage of polymers via high UV-A/UV-B indices, compounded by saline humidity.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Engineering Directive: Standard transparent plasticized PVC jackets yellow, embrittle, and crack within 60 days of exposure. Formulations require cross-linked polyethylene (XLPE) or silicone jacketing treated with carbon-black or benzotriazole UV stabilizers. Metal fasteners must be strictly specified as A4 (AISI 316) marine-grade stainless steel.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Mediterranean Strip (Corrosive Electrolytic Aerosol)
Prevalent Risk: Micro-particulate marine aerosol deposition acting as an electrolyte, accelerating galvanic corrosion.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Engineering Directive: Ferrous components are prohibited. Enclosures require cast aluminum (AlSi12) or glass-reinforced polyester (GRP). Cable interconnects must feature screw-locked overmolded elastomeric seals rated to IP67. Near-waterfront installations mandate IP68.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Black Sea Belt (High Relative Humidity &amp;amp; Hydrostatic Ingress)
Prevalent Risk: Continuous ambient relative humidity exceeding 90%, frequent cloudbursts, and slow evaporation rates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Engineering Directive: Capillary siphoning through stranded conductors is the primary electrical hazard. Cable breakouts and terminal junction enclosures must be backfilled with re-enterable two-component polyurethane or aliphatic dielectric gel. Cable raceways must be laid with a minimum 2% decline to prevent standing-water pockets.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Central Anatolian Steppe (Sub-Zero Embrittlement &amp;amp; Static Snow Loads)
Prevalent Risk: Sustained sub-zero temperatures (down to -15°C), high diurnal thermal deltas (ΔT&amp;gt;25 
∘
C), and dense vertical snow accumulation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Engineering Directive: PVC insulation is strictly prohibited as it reaches its glass transition temperature and fractures under flexure. Installations mandate VDE-certified H07RN-F heavy polychloroprene rubber. Structural frames must incorporate a minimum 35° shedding pitch to prevent snow accumulation exceeding 0.5 kN/m².&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Eastern Continental Plateau (Cryogenic Thermal Cycling)
Prevalent Risk: Extreme cold down to -25°C to -30°C.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Engineering Directive: Switched-mode power supply (SMPS) aluminum electrolytic capacitors suffer electrolyte freezing, causing capacitance drops and loop instability. Industrial-grade drivers rated for cold starts at -40°C with solid tantalum or specialized low-ESR polymer capacitors are mandatory.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Power Electronics &amp;amp; Bus Distribution Architecture&lt;br&gt;
A common operational error in large installations—such as a 400-meter commercial center facade or a 450-meter street string—is feeding extended luminaire runs from single-ended low-voltage DC rails without calculating intermediate conductor resistance.&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                   [DISTRIBUTION ENCLOSURE (IP67)]
                   ├── 4-Pole Main Disconnect
                   ├── 30mA Type A Industrial RCD
                   ├── C-Curve Branch MCBs (Compensates Inrush)
                   └── Digital Astronomical Solar-Sync Chronometer
                                        │
                                        ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;┌──────────────────────────────────────────┴──────────────────────────────────────────┐&lt;br&gt;
 │ 230V AC Isolated Mains Bus (H07RN-F 3G2.5mm² Heavy-Duty Rubber)                     │&lt;br&gt;
 └──────────────┬───────────────────────────────────────────────────────┬──────────────┘&lt;br&gt;
                ▼                                                       ▼&lt;br&gt;
    [Decentralized SMPS 1]                                  [Decentralized SMPS 2]&lt;br&gt;
    IP67 Sealed Aluminum Core                               IP67 Sealed Aluminum Core&lt;br&gt;
    Sized: P_rated &amp;gt;= P_load * 1.20                         Sized: P_rated &amp;gt;= P_load * 1.20&lt;br&gt;
                │                                                       │&lt;br&gt;
                ▼                                                       ▼&lt;br&gt;
 [Branch Run 1: 50m Curtain]                             [Branch Run 2: 50m Curtain]&lt;br&gt;
 End-point V_drop &amp;lt; 5%                                   End-point V_drop &amp;lt; 5%&lt;br&gt;
Derating Factor &amp;amp; Headroom Calculation&lt;br&gt;
To avoid thermal runaway inside sealed non-ventilated IP67 cast enclosures, drivers must be sized with an operational safety coefficient (K &lt;br&gt;
safety&lt;br&gt;
​&lt;br&gt;
≥1.20):&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;P &lt;br&gt;
driver&lt;br&gt;
​&lt;br&gt;
 ≥( &lt;br&gt;
i=1&lt;br&gt;
∑&lt;br&gt;
n&lt;br&gt;
​&lt;br&gt;
 P &lt;br&gt;
module,i&lt;br&gt;
​&lt;br&gt;
 )×1.20&lt;br&gt;
DC Busbar Voltage Drop Equation (Two-Wire Model)&lt;br&gt;
When low-voltage DC (e.g., 24V or 30V SELV) is used across long structural spans, the line resistance of the copper conductor induces a measurable voltage drop:&lt;/p&gt;

&lt;p&gt;ΔV= &lt;br&gt;
A&lt;br&gt;
2⋅L⋅I⋅ρ&lt;br&gt;
​&lt;/p&gt;

&lt;p&gt;Where:&lt;/p&gt;

&lt;p&gt;ΔV = Total circuit voltage drop (Volts)&lt;/p&gt;

&lt;p&gt;L = Conductor single-run distance (Meters)&lt;/p&gt;

&lt;p&gt;I = Operating load current (Amperes)&lt;/p&gt;

&lt;p&gt;ρ = Resistivity of annealed copper (0.01724&amp;nbsp;Ω⋅mm &lt;br&gt;
2&lt;br&gt;
 /m at 20 &lt;br&gt;
∘&lt;br&gt;
 C)&lt;/p&gt;

&lt;p&gt;A = Conductor cross-sectional area (mm &lt;br&gt;
2&lt;br&gt;
 )&lt;/p&gt;

&lt;p&gt;+-------------------+-----------------+-------------------+-------------------+------------------------+&lt;br&gt;
| Run Length (L)    | Load (24V DC)   | Cross-Section (A) | Voltage Drop (ΔV) | Normalized Drop Ratio  |&lt;br&gt;
+-------------------+-----------------+-------------------+-------------------+------------------------+&lt;br&gt;
| 10 meters         | 5.0 Amperes     | 1.5 mm²           | 0.115 Volts       | 0.48% (Optimal)        |&lt;br&gt;
| 25 meters         | 5.0 Amperes     | 1.5 mm²           | 0.287 Volts       | 1.20% (Optimal)        |&lt;br&gt;
| 50 meters         | 5.0 Amperes     | 1.5 mm²           | 0.575 Volts       | 2.40% (Acceptable)     |&lt;br&gt;
| 50 meters         | 10.0 Amperes    | 1.5 mm²           | 1.149 Volts       | 4.79% (Borderline)     |&lt;br&gt;
| 50 meters         | 10.0 Amperes    | 2.5 mm²           | 0.690 Volts       | 2.88% (Optimal)        |&lt;br&gt;
| 100 meters        | 5.0 Amperes     | 1.5 mm²           | 1.149 Volts       | 4.79% (Borderline)     |&lt;br&gt;
| 100 meters        | 10.0 Amperes    | 1.5 mm²           | 2.298 Volts       | 9.58% (CRITICAL FAULT) |&lt;br&gt;
| 100 meters        | 10.0 Amperes    | 4.0 mm²           | 0.862 Volts       | 3.59% (Optimal)        |&lt;br&gt;
+-------------------+-----------------+-------------------+-------------------+------------------------+&lt;br&gt;
A relative voltage drop exceeding 5% triggers operational anomalies: asymmetric chromaticity shifts along warm-white diodes (2700K shifting toward an amber cast due to diode forward voltage mismatches), visible luminous intensity drops, and communication errors within serial NRZ microcontrollers on addressable IC lines.&lt;/p&gt;

&lt;p&gt;To maintain photometric uniformity across runs exceeding 30 meters, system designs must implement bilateral power feeding or decentralized 230V AC trunk lines paired with localized IP67 power supplies.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Empirical Energy Metrics &amp;amp; Load Profiling
Based on data from the June 2026 Cost and Energy Index, seasonal deployments follow predictable consumption patterns when regulated by automated controls. The table below outlines empirical models for standard medium-density layouts (operating on an 8-hour window from 17:00 to 01:00 over a 30-day baseline):&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;+--------------------------+------------------------------+----------+-------------------+--------------------+&lt;br&gt;
| Archetype                | Primary Technical Ensemble   | Scope    | Daily Energy (8h) | Seasonal Total kWh |&lt;br&gt;
+--------------------------+------------------------------+----------+-------------------+--------------------+&lt;br&gt;
| Retail Boutique Store    | Facade Curtain + Fringe Band | 40 m     | 4.16 kWh          | ~ 125 kWh          |&lt;br&gt;
| Standalone Structure     | Perimeter + Foliage Strings  | 45 m     | 4.66 kWh          | ~ 140 kWh          |&lt;br&gt;
| Dining &amp;amp; Hospitality     | Eaves Drop + Motif Accents   | 88 m     | 10.10 kWh         | ~ 303 kWh          |&lt;br&gt;
| Estate Complex           | Roof Facade + Tree Wrap      | 175 m    | 18.60 kWh         | ~ 558 kWh          |&lt;br&gt;
| Municipal Highway Span   | Eaves + Column Assemblies    | 450 m    | 19.93 kWh         | ~ 598 kWh          |&lt;br&gt;
| Commercial Center Facade | Atrium + Surface Wall Canopy | 400 m    | 26.96 kWh         | ~ 809 kWh          |&lt;br&gt;
| Civic Square &amp;amp; Boulevard | Portals + Main Avenue Tree   | 700 m    | 30.33 kWh         | ~ 910 kWh          |&lt;br&gt;
+--------------------------+------------------------------+----------+-------------------+--------------------+&lt;br&gt;
Automation &amp;amp; Switchgear Protections&lt;br&gt;
Operating systems beyond 01:00 triples total energy consumption while accelerating lumen depreciation through thermal accumulation. Automated distribution panels must incorporate:&lt;/p&gt;

&lt;p&gt;Digital Astronomical Clocks: Synchronized to local sunrise and sunset tables, dynamically updating trigger events throughout the winter equinox.&lt;/p&gt;

&lt;p&gt;Type A 30mA Residual Current Protection: Required to detect both sinusoidal AC leakage and pulsed DC fault currents introduced by half-wave and full-wave solid-state driver circuits.&lt;/p&gt;

&lt;p&gt;C-Curve Miniature Circuit Breakers (MCB): Sized to withstand the capacitive inrush current spikes (30×to&amp;nbsp;50×I &lt;br&gt;
nominal&lt;br&gt;
​&lt;br&gt;
  for 2&amp;nbsp;to&amp;nbsp;5&amp;nbsp;ms) without false tripping.&lt;/p&gt;

&lt;p&gt;Summary and Quality Gates&lt;br&gt;
For field engineers and systems architects managing structural seasonal lighting installations, project lifecycles depend on four key requirements:&lt;/p&gt;

&lt;p&gt;Material Composition: Insist on TIG-welded structural 6061-T6 aluminum; do not use mild steel framing on elevated municipal poles.&lt;/p&gt;

&lt;p&gt;Environmental Matching: Match cable and enclosure materials to regional microclimates (e.g., mandatory H07RN-F rubber in cold regions, marine-grade A4 stainless steel along shorelines).&lt;/p&gt;

&lt;p&gt;Electrical Safety: Maintain busbar voltage drops below 5% through bilateral feeds or localized conversion, while isolating public contact zones using 30V SELV architecture.&lt;/p&gt;

&lt;p&gt;Control Topologies: Automate distribution through standalone IP67 sub-panels utilizing Type A 30mA RCDs and Class C circuit breakers.&lt;/p&gt;

&lt;p&gt;For complete technical documentation on the 1,054 model variants, full mechanical drawings, and photometric files, visit ledisiklandirma.com/isik-susleme.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Municipal &amp; Commercial Lighting Engineering: Spatial Zone Architecture, 30V SELV Safety, and Aerodynamic Moment Statics</title>
      <dc:creator>Olivia</dc:creator>
      <pubDate>Thu, 10 Sep 2026 14:03:15 +0000</pubDate>
      <link>https://dev.to/olivia_342fsfsdgrere/municipal-commercial-lighting-engineering-spatial-zone-architecture-30v-selv-safety-and-1734</link>
      <guid>https://dev.to/olivia_342fsfsdgrere/municipal-commercial-lighting-engineering-spatial-zone-architecture-30v-selv-safety-and-1734</guid>
      <description>&lt;p&gt;&lt;a href="https://www.a1organizasyon.com/" rel="noopener noreferrer"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Canonical Technical Documentation: a1organizasyon.com/yilbasi-isik-susleme · isiklisusleme.com/isik-suslemeAbstractDeploying temporary solid-state lighting arrays across metropolitan street grids, commercial plazas, and large public spaces is frequently oversimplified as an aesthetic design challenge. From an electrical, civil, and mechanical perspective, municipal holiday displays represent a transient infrastructure installation that must operate under extreme environmental constraints: persistent wind loading, sub-zero thermal cycling, icing, and public physical interaction.This technical monograph details the engineering methodology developed across 16 years of field execution (standardized across 21 product groups, 232 model families, and 666 variants). We present the spatial zone classification model, galvanic isolation dynamics under the 30V Safety Extra Low Voltage (SELV) regime, structural overturning moment mechanics for freestanding monuments up to 20 meters, and distribution panel topologies with transient suppression.1. Spatial Zone Architecture: Decoupling Mechanical &amp;amp; Electrical ConstraintsField failures in seasonal exterior installations typically stem from deploying uniform electrical and mechanical specifications across non-uniform environments. A lighting unit suspended 6 meters above a vehicular roadway experiences vastly different stress vectors than an interactive illuminated structure accessible to children in a public square.To resolve this, sites are engineered into six discrete operational zones:                                  [SPATIAL ZONE TOPOLOGY]&lt;br&gt;
                                             │&lt;br&gt;
      ┌──────────────────┬───────────────────┼───────────────────┬──────────────────┐&lt;br&gt;
      ▼                  ▼                   ▼                   ▼                  ▼&lt;br&gt;
  [ZONE 1]           [ZONE 2]            [ZONE 3]            [ZONE 4]           [ZONE 5 &amp;amp; 6]&lt;br&gt;
  Utility Pole       Portals &amp;amp; Tunnels   Catenary Spans      Ground Structures  Canopy &amp;amp; Facade&lt;br&gt;
  Mounted Motifs     Public Flow Paths   Street Crossings    Freestanding Trees Wall Sculptures&lt;br&gt;
  230V AC Grid       30V SELV Extra-Low  High-Tension Steel  Ballasted Monuments Rigid Brackets&lt;br&gt;
  High Wind Shear    Zero Touch Risk     Catenary Dynamics   Up to 2,500 kg Mass Aerodynamic Gap&lt;br&gt;
Technical Matrix Across Operational Zones+------------------------------------+---------------------+------------------+---------------------+-------------------+&lt;br&gt;
| Spatial Zone                       | Voltage Class       | Dimension Range  | Power Demand (W)    | Mass Envelope     |&lt;br&gt;
+------------------------------------+---------------------+------------------+---------------------+-------------------+&lt;br&gt;
| Zone 1: Column / Pole Motifs       | 230V AC Mains       | 40 cm – 300 cm   | 13.7 W – 240 W      | 3.5 kg – 20.0 kg  |&lt;br&gt;
| Zone 2: Portals &amp;amp; Walkway Tunnels  | 30V SELV Extra-Low  | 215 cm – 760 cm  | 150 W – 1,800 W     | 45 kg – 320 kg    |&lt;br&gt;
| Zone 3: Overhead Catenary Spans    | 230V AC Mains       | 400 cm – 820 cm  | 46 W – 960 W        | 6.5 kg – 135 kg   |&lt;br&gt;
| Zone 4: Ground Plazas &amp;amp; Sculptures | 30V / 230V Hybrid   | 120 cm – 2000 cm | 9.6 W – 6,467 W     | 5.5 kg – 2,500 kg |&lt;br&gt;
| Zone 5: Natural Canopy / Foliage   | 30V / 230V Hybrid   | 40 cm – 350 cm   | 3.6 W – 183 W       | 0.8 kg – 81.5 kg  |&lt;br&gt;
| Zone 6: Vertical Building Facades  | 230V AC Mains       | 100 cm – 500 cm  | 14 W – 320 W        | 3.2 kg – 145 kg   |&lt;br&gt;
+------------------------------------+---------------------+------------------+---------------------+-------------------+&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Electrical Safety Dynamics: 30V SELV vs. 230V DistributionThe most critical safety decision in high-traffic public installations is voltage regime partitioning.                              [ELECTRICAL ISOLATION TOPOLOGY]&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;230V AC Mains Feed (Utility Sub-Station / Municipal Grid Pillar)
                              │
                              ▼
     [Distribution Enclosure: IP67 Stainless / GRP Chassis]
     ├── 4-Pole 30mA Type A Industrial Residual Current Device (RCD)
     ├── Class C Branch Circuit Breakers (Compensating for Inrush)
     └── Astronomical Digital Astronomical Timer (Solar-Tracked)
                              │
           ┌──────────────────┴──────────────────┐
           ▼                                     ▼
[Elevated Circuits (&amp;gt;2.5m)]             [Public Contact Zones (&amp;lt;2.5m)]
Zone 1 &amp;amp; Zone 3 (Pole &amp;amp; Catenary)       Zone 2 &amp;amp; Zone 4 (Walkways &amp;amp; Sculptures)
Direct 230V Distribution                EN 61558-2-6 Isolation Transformer
Low Line Current (I = P / V)            Step-Down Conversion to 30V SELV
H07RN-F Rubber Trunk Lines              Zero Ingress Shock Hazard Potential
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Physics of Safety Extra Low Voltage (30V SELV)Under dry atmospheric conditions, human skin exhibits an electrical impedance between $1,000\ \Omega$ and $2,000\ \Omega$. Under winter precipitation (slush, saturated snowfall, salt spray), skin resistance plummets below $500\ \Omega$.If a 230V conductor suffers insulation shearing due to crowd pressure or pedestrian vandalism:$$I_{\text{fault}} = \frac{V_{\text{phase}}}{R_{\text{body}} + R_{\text{ground}}} \approx \frac{230\text{ V}}{500\ \Omega} \approx 460\text{ mA}$$A fault current of $460\text{ mA}$ exceeds the ventricular fibrillation threshold ($\approx 50\text{ mA}$) by more than nine-fold.Deploying a 30V SELV system governed by EN 61558-2-6 safety isolating transformers limits the theoretical maximum contact current to:$$I_{\text{touch, max}} = \frac{30\text{ V}}{500\ \Omega} \approx 60\text{ mA}$$In practical deployments with grounding impedances and transformer secondary isolation, the actual leakage current through an accidental human touch vector is constrained below $10\text{ mA}$—preventing muscular tetany and entirely eliminating lethal shock hazards.3. Structural Mechanics: Aerodynamic Drag &amp;amp; Ballast Dynamics for 20m MonumentsZone 4 structures—such as freestanding modular cone trees scaling up to 20 meters ($6.47\text{ kW}$ load, $2.5\text{ metric tons}$ self-mass)—act as large, blunt aerodynamic obstructions. In municipal plazas paved with granite or andezite, mechanical penetration (concrete anchors or core drilling) is strictly prohibited.Stability must therefore be maintained purely through gravitational ballast counter-moments.                           ▲ Wind Vector (v = 27.8 m/s / 100 km/h)&lt;br&gt;
                       │&lt;br&gt;
                \      │      /&lt;br&gt;
                 \     ▼     /     Height H = 20.0m&lt;br&gt;
                  \  [Mesh] /      Effective Projected Area A_eff = 36 m²&lt;br&gt;
                   \       /       Drag Coefficient C_d = 1.25&lt;br&gt;
                    \     /&lt;br&gt;
                     \   /         Center of Aerodynamic Pressure: hc = 7.2m&lt;br&gt;
                    ==│═│==&lt;br&gt;
                   / [   ] \       Base Diameter D = 6.0m (Pivot Radius r = 3.0m)&lt;br&gt;
                 [CONCRETE BALLAST]&lt;br&gt;
Aerodynamic Drag Force Equation$$F_d = \frac{1}{2} \cdot \rho_{\text{air}} \cdot v^2 \cdot C_d \cdot A_{\text{effective}}$$Where:$\rho_{\text{air}} = 1.29\ \text{kg/m}^3$ (air density at $-5^\circ\text{C}$ winter conditions)$v = 27.8\ \text{m/s}$ ($100\ \text{km/h}$ maximum municipal design storm gust)$C_d = 1.25$ (aerodynamic drag coefficient for porous lattice cone configurations)$A_{\text{effective}} = 36.0\ \text{m}^2$ (net projected area accounting for a 35% mesh solidity ratio)Calculating total horizontal shear:$$F_d = 0.5 \cdot 1.29 \cdot (27.8)^2 \cdot 1.25 \cdot 36.0 \approx 22,437\text{ N} \ (\approx 2,287\text{ kgf})$$Overturning Moment CalculationWith the center of aerodynamic force acting at $h_c = 7.2\text{ meters}$ above the base ring:$$M_{\text{overturn}} = F_d \cdot h_c = 22,437\text{ N} \cdot 7.2\text{ m} = 161,546\text{ N}\cdot\text{m}$$Required Counter-Weight Ballast for Safety Factor (SF = 1.5)The stabilizing moment is generated by the combined mass of the structural chassis plus dedicated precast concrete ballast blocks acting around the base pivot edge ($r_{\text{pivot}} = 3.0\text{ meters}$):$$M_{\text{stabilizing}} \ge \mathbf{SF} \cdot M_{\text{overturn}} = 1.5 \cdot 161,546 = 242,319\text{ N}\cdot\text{m}$$$$\text{Total Required Mass } (M_{\text{total}}) = \frac{M_{\text{stabilizing}}}{r_{\text{pivot}} \cdot g} = \frac{242,319\text{ N}\cdot\text{m}}{3.0\text{ m} \cdot 9.81\text{ m/s}^2} \approx 8,234\text{ kg}$$Subtracting the self-mass of the 20-meter aluminum and steel skeleton ($2,500\text{ kg}$):$$\mathbf{M_{\text{ballast, net}}} = 8,234\text{ kg} - 2,500\text{ kg} = \mathbf{5,734\text{ kg}}$$Engineering Directive:To prevent structural toppling during a $100\text{ km/h}$ winter storm event, the base chassis of a 20-meter installation must incorporate at least $5.75\text{ metric tons}$ of engineered, encapsulated concrete ballast blocks, symmetrically distributed across the anchoring perimeter.4. Materials Science: Structural Aluminum 6061 vs. Ferrous FrameworksA core failure mode in temporary municipal installations is structural fatigue caused by dead-load weight and galvanic corrosion.+---------------------------------------+----------------------------------+------------------------------------+&lt;br&gt;
| Material Property                     | Structural Aluminum (Al 6061-T6) | Commercial Mild Steel (St 37)      |&lt;br&gt;
+---------------------------------------+----------------------------------+------------------------------------+&lt;br&gt;
| Density (Mass Ratio)                  | 2.70 g/cm³ (Baseline: 1.0x)      | 7.85 g/cm³ (2.9x heavier)          |&lt;br&gt;
| Yield Strength (Rp 0.2)               | ~ 276 MPa                        | ~ 235 MPa                          |&lt;br&gt;
| Corrosion Resistance                  | Naturally passivating Al₂O₃ film | Requires dip galvanizing or rusts  |&lt;br&gt;
| Impact on Utility Pole Infrastructure | Minimal bending torque on mast   | Induces cyclic fatigue on anchors  |&lt;br&gt;
| Maintenance &amp;amp; Reusability Cycle       | 5 to 7 operational seasons       | 1 to 2 seasons before surface rust |&lt;br&gt;
+---------------------------------------+----------------------------------+------------------------------------+&lt;br&gt;
By engineering motifs with TIG-welded structural grade 6061 aluminum alloy, individual module weights are constrained below $15\text{ kg}$ for up to 3-meter spans. This allows installation teams to utilize standard dual-band stainless steel strapping (Band-It style) without introducing uncalculated bending moments to municipal utility poles.5. Inrush Current Management and Reactive Power CompensationA common operational error in large-scale LED arrays is sizing distribution protection solely against nominal thermal power draws.A 20-meter tree drawing $6.47\text{ kW}$ utilizes multiple switched-mode power supplies. During initial cold-start power-up, input bulk capacitors act as instantaneous short circuits:$$I_{\text{inrush}} \approx 30 \text{ to } 50 \times I_{\text{nominal}}$$For an array drawing $28.1\text{ A}$ nominal at 230V single-phase, the cumulative inrush current spike can surge past $1,000\text{ A}$ for $2$ to $5\text{ milliseconds}$. Standard Type B distribution circuit breakers will trip immediately on magnetic release.Protection &amp;amp; Switching Topology GuidelinesBreaker Curve Selection: Branch circuits supplying capacitive SMPS drivers must standardize on Type C or Type D miniature circuit breakers (MCBs) calibrated to withstand instantaneous surges up to $10 \times I_n$ and $20 \times I_n$ respectively.Zero-Crossing Switching: Programmable astronomical time clocks must trigger distribution linyas through zero-voltage-crossing solid-state contactors. Engaging the load at the sine wave zero-crossing point ($V_{\text{instant}} = 0\text{ V}$) limits peak $di/dt$ inrush current.Harmonic Mitigation: Total Harmonic Distortion (THD) of the overall LED driver suite must conform to EN 61000-3-2 Class C standards ($THD &amp;lt; 15\%$), preventing neutral conductor overheating in balanced 3-phase commercial grids.Engineering Implementation ProtocolEngineers executing large-scale seasonal deployments should maintain a strict quality gate:Map Zones First: Never specify luminaires before defining pedestrian interaction boundaries (Zone 2/4 = Mandatory 30V SELV).Calculate Aerodynamics: Mandate ballast moment reports for all freestanding structures exceeding 3 meters in height.Isolate Controls: Never tie transient exterior loads directly to building core switchboards without independent 30mA Type A RCDs and Class C circuit breakers.Enforce Structural Rigidity: Insist on 6061-T6 aluminum alloys over ferrous hollow sections to protect third-party utility infrastructure from bending strain.For full access to the 232 model families, detailed parametric dimensional tables, and mechanical schematics across all 6 installation zones, review the reference documentation at a1organizasyon.com/yilbasi-isik-susleme.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>software</category>
      <category>mojo</category>
      <category>webdev</category>
      <category>electronics</category>
    </item>
    <item>
      <title>The Engineering Behind Temporary Architectural Lighting: Load Calculations, Ingress Protection, and Environmental Failure Modes</title>
      <dc:creator>Olivia</dc:creator>
      <pubDate>Thu, 10 Sep 2026 12:47:09 +0000</pubDate>
      <link>https://dev.to/olivia_342fsfsdgrere/the-engineering-behind-temporary-architectural-lighting-load-calculations-ingress-protection-and-381n</link>
      <guid>https://dev.to/olivia_342fsfsdgrere/the-engineering-behind-temporary-architectural-lighting-load-calculations-ingress-protection-and-381n</guid>
      <description>&lt;p&gt;&lt;a href="https://www.isiklisusleme.com/" rel="noopener noreferrer"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Abstract&lt;br&gt;
Temporary outdoor LED installations—ranging from commercial facade curtains and high-wind structural motifs to municipal light tunnels and 12-meter commercial trees—are routinely perceived as trivial decorative applications. In reality, deploying hundreds of linear meters of solid-state lighting across multi-story commercial envelopes constitutes a high-risk electro-mechanical engineering task.&lt;/p&gt;

&lt;p&gt;These systems operate at the intersection of extreme environmental stressors: sub-zero thermal cycling, mechanical wind shear, transient voltage spikes, and hydrostatic pressure from rain and melting snow.&lt;/p&gt;

&lt;p&gt;This paper breaks down the technical fundamentals required to engineer and commission commercial-grade temporary architectural lighting networks: Ingress Protection physics (IEC 60529), thermal degradation of polymers in cold climates, switched-mode power supply (SMPS) sizing, busbar voltage drop equations, and electrical protection schemes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Environmental Mechanics: Polymer Failure &amp;amp; Ingress Protection (IEC 60529)
The primary cause of field failure in seasonal lighting networks is not diode burnout—it is insulation degradation and seal compromise caused by temperature cycling.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;+---------------------------------------------------------------------------------------+&lt;br&gt;
|                               INGRESS PROTECTION MATRIX (IEC 60529)                    |&lt;br&gt;
+---------+--------------------+--------------------------------------------------------+&lt;br&gt;
| Rating  | Dust Protection    | Liquid Ingress Protection &amp;amp; Operational Boundary       |&lt;br&gt;
+---------+--------------------+--------------------------------------------------------+&lt;br&gt;
| IP20    | Fingers/Large tools| None. Strictly indoor or climate-controlled vitrines.   |&lt;br&gt;
| IP44    | Objects &amp;gt; 1mm      | Splash resistant. Unsuitable for exposed wind-driven rain|&lt;br&gt;
| IP65    | Dust-tight         | Low-pressure water jets (6.3mm nozzle, 12.5 L/min).    |&lt;br&gt;
|         | (Vacuum tested)    | Required minimum for building facades and roof eaves.  |&lt;br&gt;
| IP67    | Dust-tight         | Temporary immersion (1m depth for 30 minutes).         |&lt;br&gt;
|         |                    | Mandatory for ground-level beds, lawn statues, snow.  |&lt;br&gt;
| IP68    | Dust-tight         | Continuous underwater operation under pressure.        |&lt;br&gt;
|         |                    | Mandatory for submerged fountains and marine docks.   |&lt;br&gt;
+---------+--------------------+--------------------------------------------------------+&lt;br&gt;
The Capillary Ingress Mechanism in PVC vs. H07RN-F Polychloroprene&lt;br&gt;
Low-tier commercial products commonly utilize transparent or white Polyvinyl Chloride (PVC) jackets. At temperatures below −5 &lt;br&gt;
∘&lt;br&gt;
 C, the plasticizers within standard PVC undergo phase transition and glass embrittlement. Cyclic mechanical strain induced by aerodynamic wind loading induces micro-fissures along the conductor boundary.&lt;/p&gt;

&lt;p&gt;[MIGRATION PATHWAY IN LOW-GRADE PVC SYSTEMS]&lt;/p&gt;

&lt;p&gt;Wind-Induced Flexure ---&amp;gt; Micro-Fissures in Embrittled PVC ---&amp;gt; Capillary Draw&lt;br&gt;
                                                                       |&lt;br&gt;
 Short Circuit / Ground Fault &amp;lt;--- PCB Corrosion &amp;lt;--- Hydrostatic Pressure&lt;br&gt;
Once micro-fissures form, hydrostatic pressure from freezing and thawing snow draws moisture directly into the stranded conductor bundle via capillary action. This moisture traverses along the copper strands directly into the diode injection molding, causing localized galvanic corrosion and instantaneous tripping of Residual Current Devices (RCD).&lt;/p&gt;

&lt;p&gt;The Engineering Fix:&lt;/p&gt;

&lt;p&gt;Industrial deployments must mandate H07RN-F heavy-duty polychloroprene rubber cabling (compliant with VDE 0282-4). Polychloroprene maintains elastic tensile properties down to −25 &lt;br&gt;
∘&lt;br&gt;
 C, completely resisting micro-fissuring and cyclic shear strain.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Power Architecture: Headroom, SMPS Sizing, and Inrush Current Dynamics
A classic design flaw in commercial LED arrays is dimensioning the Switched-Mode Power Supply (SMPS) purely based on nominal steady-state wattage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Solid-state drivers deployed in sealed exterior enclosures (typically die-cast aluminum or polycarbonate junction boxes rated IP67) face severe internal thermal build-up during extended continuous runs (6 to 12 hours).&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  ┌─────────────────────────────────────────┐
                  │    POWER DISTRIBUTION ARCHITECTURE      │
                  └────────────────────┬────────────────────┘
                                       │ 230V AC Mains
                                       ▼
                  ┌─────────────────────────────────────────┐
                  │  IP67 Junction: Type A 30mA RCD + C-MCB │
                  └────────────────────┬────────────────────┘
                                       │
                                       ▼
                  ┌─────────────────────────────────────────┐
                  │ Industrial SMPS (Constant Voltage 24V)  │
                  │ Sized with &amp;gt;= 20% Thermal Headroom      │
                  └────────────────────┬────────────────────┘
                                       │ 
                ┌──────────────────────┴──────────────────────┐
                ▼                                             ▼
      [Branch Run 1: 50m]                           [Branch Run 2: 50m]
      End-point Vdrop &amp;lt; 5%                          End-point Vdrop &amp;lt; 5%
      AWG 16 / 1.5mm² Rubber                        AWG 16 / 1.5mm² Rubber
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Thermal Derating Formula for Sealed SMPS Enclosures&lt;br&gt;
P &lt;br&gt;
supply_rated&lt;br&gt;
​&lt;br&gt;
 ≥( &lt;br&gt;
i=1&lt;br&gt;
∑&lt;br&gt;
n&lt;br&gt;
​&lt;br&gt;
 P &lt;br&gt;
load,i&lt;br&gt;
​&lt;br&gt;
 )×K &lt;br&gt;
safety&lt;br&gt;
​&lt;/p&gt;

&lt;p&gt;Where:&lt;/p&gt;

&lt;p&gt;P &lt;br&gt;
load,i&lt;br&gt;
​&lt;br&gt;
  is the steady-state consumption of individual modules, strings, or motifs.&lt;/p&gt;

&lt;p&gt;K &lt;br&gt;
safety&lt;br&gt;
​&lt;br&gt;
  is the engineering headroom coefficient (≥1.20).&lt;/p&gt;

&lt;p&gt;Operating an SMPS above 80% of its rated capacity inside an unventilated outdoor enclosure elevates ambient junction temperatures beyond 65 &lt;br&gt;
∘&lt;br&gt;
 C, causing premature dry-out of primary electrolytic filter capacitors and triggering over-temperature protection (OTP) oscillations.&lt;/p&gt;

&lt;p&gt;Inrush Current Management&lt;br&gt;
Capacitive inrush currents during cold-start power-on can exceed steady-state operating currents by a factor of 30 to 50 for a duration of 2 to 5&amp;nbsp;milliseconds.&lt;/p&gt;

&lt;p&gt;Standard household circuit breakers (Type B) will trip on instantaneous magnetic release. Temporary lighting distribution panels must specify Type C (or Type D for multi-kilowatt transformer banks) miniature circuit breakers (MCBs) paired with NTC inrush current limiters or zero-crossing solid-state relays.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Voltage Drop Analysis Across Extended DC Trunks
In large-scale commercial installations (such as a 60-meter hotel eaves perimeter or a 200-meter street festival run), supplying low-voltage DC (12V or 24V) over extended distances introduces severe ohmic losses (I 
2
R).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Mathematical Model for Two-Wire DC Distribution&lt;br&gt;
ΔV= &lt;br&gt;
A&lt;br&gt;
2⋅L⋅I⋅ρ&lt;br&gt;
​&lt;/p&gt;

&lt;p&gt;Where:&lt;/p&gt;

&lt;p&gt;ΔV = Voltage drop in volts (V)&lt;/p&gt;

&lt;p&gt;L = One-way length of the cable run in meters (m)&lt;/p&gt;

&lt;p&gt;I = Total load current in amperes (A)&lt;/p&gt;

&lt;p&gt;ρ = Resistivity of copper (0.0172&amp;nbsp;Ω⋅mm &lt;br&gt;
2&lt;br&gt;
 /m at 20 &lt;br&gt;
∘&lt;br&gt;
 C)&lt;/p&gt;

&lt;p&gt;A = Conductor cross-sectional area in mm &lt;br&gt;
2&lt;/p&gt;

&lt;p&gt;+---------------------------------------------------------------------------------------+&lt;br&gt;
|            VOLTAGE DROP COEFFICIENTS (24V DC SYSTEM, 10A LOAD, COPPER)                 |&lt;br&gt;
+-------------------+--------------------+------------------------+---------------------+&lt;br&gt;
| One-Way Run (L)   | Cross-Section (A)  | Voltage Drop (ΔV)      | Percentage Drop     |&lt;br&gt;
+-------------------+--------------------+------------------------+---------------------+&lt;br&gt;
| 10 meters         | 1.5 mm²            | 0.23 V                 | 0.95% (Acceptable)  |&lt;br&gt;
| 25 meters         | 1.5 mm²            | 0.57 V                 | 2.38% (Acceptable)  |&lt;br&gt;
| 50 meters         | 1.5 mm²            | 1.15 V                 | 4.79% (Borderline)  |&lt;br&gt;
| 50 meters         | 2.5 mm²            | 0.69 V                 | 2.87% (Optimal)     |&lt;br&gt;
| 100 meters        | 1.5 mm²            | 2.30 V                 | 9.58% (FAILURE)     |&lt;br&gt;
| 100 meters        | 4.0 mm²            | 0.86 V                 | 3.58% (Optimal)     |&lt;br&gt;
+-------------------+--------------------+------------------------+---------------------+&lt;br&gt;
A voltage drop exceeding 5% across a constant-current or constant-voltage LED string results in visible luminous flux depreciation, chromaticity coordinate shifts (e.g., 2700K warm white shifting towards reddish-amber due to unequal diode forward voltages), and driver IC resetting in addressable RGB arrays.&lt;/p&gt;

&lt;p&gt;To maintain uniform luminance across runs exceeding 30 meters, designers must implement ring-bus topology or bilateral power injection (feeding the array from both terminals simultaneously).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Mechanical Statics: Wind Shear, Ballast, and Structural Anchoring
Large municipal displays and standalone commercial figures behave aerodynamically as bluff bodies exposed to fluid drag. Underestimating wind load profiles during winter storms leads to mechanical catastrophic failure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Aerodynamic Drag Force Equation&lt;br&gt;
F &lt;br&gt;
d&lt;br&gt;
​&lt;br&gt;
 = &lt;br&gt;
2&lt;br&gt;
1&lt;br&gt;
​&lt;br&gt;
 ⋅ρ &lt;br&gt;
air&lt;br&gt;
​&lt;br&gt;
 ⋅v &lt;br&gt;
2&lt;br&gt;
 ⋅C &lt;br&gt;
d&lt;br&gt;
​&lt;br&gt;
 ⋅A &lt;br&gt;
effective&lt;br&gt;
​&lt;/p&gt;

&lt;p&gt;Where:&lt;/p&gt;

&lt;p&gt;F &lt;br&gt;
d&lt;br&gt;
​&lt;br&gt;
  = Total horizontal drag force (Newtons)&lt;/p&gt;

&lt;p&gt;ρ &lt;br&gt;
air&lt;br&gt;
​&lt;br&gt;
  = Air density (1.25&amp;nbsp;kg/m &lt;br&gt;
3&lt;br&gt;
  at 0 &lt;br&gt;
∘&lt;br&gt;
 C)&lt;/p&gt;

&lt;p&gt;v = Design wind velocity in m/s (e.g., 25&amp;nbsp;m/s≈90&amp;nbsp;km/h for storm criteria)&lt;/p&gt;

&lt;p&gt;C &lt;br&gt;
d&lt;br&gt;
​&lt;br&gt;
  = Drag coefficient of the structure (typically 1.2 to 1.4 for complex lattice frameworks)&lt;/p&gt;

&lt;p&gt;A &lt;br&gt;
effective&lt;br&gt;
​&lt;br&gt;
  = Projected solid frontal area (accounting for LED mesh solidity ratio)&lt;/p&gt;

&lt;p&gt;Overturning Moment Calculation for a 12-Meter Conical Tree Structure&lt;br&gt;
For a 12-meter commercial tree structure with an effective wind projection area of 14&amp;nbsp;m &lt;br&gt;
2&lt;br&gt;
 , operating in a 90&amp;nbsp;km/h gust envelope (v=25&amp;nbsp;m/s):&lt;/p&gt;

&lt;p&gt;F &lt;br&gt;
d&lt;br&gt;
​&lt;br&gt;
 =0.5⋅1.25⋅(25) &lt;br&gt;
2&lt;br&gt;
 ⋅1.3⋅14≈7,109&amp;nbsp;Newtons&amp;nbsp;(≈725&amp;nbsp;kgf)&lt;br&gt;
Assuming the center of aerodynamic pressure acts at h &lt;br&gt;
c&lt;br&gt;
​&lt;br&gt;
 =4.5&amp;nbsp;meters above the base plane, the overturning moment (M &lt;br&gt;
overturn&lt;br&gt;
​&lt;br&gt;
 ) is:&lt;/p&gt;

&lt;p&gt;M &lt;br&gt;
overturn&lt;br&gt;
​&lt;br&gt;
 =F &lt;br&gt;
d&lt;br&gt;
​&lt;br&gt;
 ×h &lt;br&gt;
c&lt;br&gt;
​&lt;br&gt;
 =7,109&amp;nbsp;N×4.5&amp;nbsp;m≈31,990&amp;nbsp;N⋅m&lt;br&gt;
                          ▲ Wind Shear Vector (25 m/s)&lt;br&gt;
                          │&lt;br&gt;
                   \      │      /&lt;br&gt;
                    \     ▼     /&lt;br&gt;
                     \  [Mesh] /       Center of Pressure: hc = 4.5m&lt;br&gt;
                      \       /&lt;br&gt;
                       \     /&lt;br&gt;
                        \   /&lt;br&gt;
                      ===│═│===        Base Ring Diameter = 4.0m&lt;br&gt;
                     [Concrete Bal]    Req. Ballast &amp;gt;= 2,100 kg&lt;br&gt;
To achieve a structural factor of safety (SF≥1.5) against a base ring diameter of 4.0&amp;nbsp;meters (r &lt;br&gt;
pivot&lt;br&gt;
​&lt;br&gt;
 =2.0&amp;nbsp;m):&lt;/p&gt;

&lt;p&gt;Required&amp;nbsp;Stabilizing&amp;nbsp;Moment≥1.5×31,990=47,985&amp;nbsp;N⋅m&lt;br&gt;
Required&amp;nbsp;Base&amp;nbsp;Mass≥ &lt;br&gt;
2.0×9.81&lt;br&gt;
47,985&lt;br&gt;
​&lt;br&gt;
 ≈2,445&amp;nbsp;kg&lt;br&gt;
Placing standalone structural trees on open plaza tiles without structural anchor bolts or at least 2.5 metric tons of engineered concrete ballast blocks constitutes an immediate public safety hazard under municipal building regulations.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Typical Power and Empirical Data Reference
Field engineers and estimators can rely on the following empirical power benchmarks for modular layout planning:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;+------------------------------------+---------------------+------------------+-------------------+&lt;br&gt;
| Configuration Profile              | Nominal Draw (W)    | Daily kWh (6h)   | Peak Inrush (A)   |&lt;br&gt;
+------------------------------------+---------------------+------------------+-------------------+&lt;br&gt;
| 5m Eaves Module (Commercial IP65)  | 8 W                 | 0.048 kWh        | &amp;lt; 1.5 A           |&lt;br&gt;
| 20m Facade Curtain + 2 Motifs      | 120 W               | 0.720 kWh        | ~ 8.0 A           |&lt;br&gt;
| Villa Complete Envelope (60m)      | 350 W               | 2.100 kWh        | ~ 22.0 A          |&lt;br&gt;
| 6m Engineered Municipal Tree       | 900 W               | 5.400 kWh        | ~ 45.0 A          |&lt;br&gt;
| 10m Walk-Through Illuminated Tunnel| 2,000 W             | 12.000 kWh       | ~ 90.0 A          |&lt;br&gt;
| 12m Commercial Plaza Tree (Atrium) | 5,000 W             | 30.000 kWh       | ~ 180.0 A         |&lt;br&gt;
+------------------------------------+---------------------+------------------+-------------------+&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Electrical Safety Regimes: RCDs, Grounding, and Thermal Fire Codes
Unlike fixed building services, temporary outdoor electrical infrastructure must adhere to heightened safety standards due to direct human contact risks:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Residual Current Devices (RCD): Every outdoor lighting sub-distribution branch must route through a dedicated 30mA Type A RCD (sensitive to sinusoidal AC and pulsating DC fault currents generated by rectifier bridges).&lt;/p&gt;

&lt;p&gt;Earth Loop Impedance: Ground continuity must be tested prior to energization. Maximum earth electrode resistance should remain within local regulatory thresholds (&amp;lt;5&amp;nbsp;Ω under dry conditions).&lt;/p&gt;

&lt;p&gt;Thermal Protection &amp;amp; Flame Retardance: All junction boxes, zip-ties, and polycarbonate diffusers must meet UL94 V-0 flammability criteria to prevent propagation during high-energy arcing events.&lt;/p&gt;

&lt;p&gt;Summary and Key Takeaways&lt;br&gt;
High-reliability temporary exterior lighting is an engineering discipline defined by strict physical boundaries:&lt;/p&gt;

&lt;p&gt;Insulation: Mandate H07RN-F rubber; eliminate exposed PVC in sub-zero thermal envelopes.&lt;/p&gt;

&lt;p&gt;Ingress Protection: Adhere strictly to IEC 60529—IP65 for building vertical surfaces, IP67 for horizontal surfaces prone to standing water or snow accumulation.&lt;/p&gt;

&lt;p&gt;Driver Headroom: Apply a minimum 20% headroom factor on SMPS loads inside sealed enclosures to mitigate thermal failure.&lt;/p&gt;

&lt;p&gt;Structural Safety: Always verify wind overturning moments on structural displays exceeding 3 meters; never rely on guesswork for base ballast calculations.&lt;/p&gt;

&lt;p&gt;For comprehensive product data, CAD motifs, and photometric charts, consult the engineering documentation at isiklisusleme.com.&lt;/p&gt;

</description>
      <category>software</category>
      <category>mojo</category>
      <category>electronics</category>
      <category>iot</category>
    </item>
    <item>
      <title>Architectural LED Illumination &amp; Commercial Holiday Decor: Engineering Efficacy, Thermal Dynamics, and IP68 Systems</title>
      <dc:creator>Olivia</dc:creator>
      <pubDate>Sun, 09 Aug 2026 13:30:57 +0000</pubDate>
      <link>https://dev.to/olivia_342fsfsdgrere/architectural-led-illumination-commercial-holiday-decor-engineering-efficacy-thermal-dynamics-1afe</link>
      <guid>https://dev.to/olivia_342fsfsdgrere/architectural-led-illumination-commercial-holiday-decor-engineering-efficacy-thermal-dynamics-1afe</guid>
      <description>&lt;p&gt;&lt;a href="https://www.ledisiklandirma.com/" rel="noopener noreferrer"&gt;&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fctwg1h7flf1e3mbjtsqa.jpeg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fctwg1h7flf1e3mbjtsqa.jpeg" alt=" " width="800" height="437"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Large-scale exterior LED installations for corporate headquarters, shopping centers, municipal plazas, and luxury estates demand an enterprise-grade approach to structural integrity, thermal management, and electrical safety.Transitioning from conceptual 3D renders to a reliable field installation requires overcoming severe environmental variables: heavy rain, freezing weather, UV exposure, and dynamic wind shear loads.Below is an engineering overview of the hardware specifications, electrical topology calculations, mounting frameworks, and execution pipelines required to deploy high-reliability exterior LED decoration systems.1. Primary Physical &amp;amp; Electrical Failure ModesWhen deploying continuous LED systems outdoors, ambient moisture and thermal cycling introduce critical vulnerabilities:+-------------------------------------------------------------------------+&lt;br&gt;
|                  Exterior LED Infrastructure Failures                   |&lt;br&gt;
+------------------------------------+------------------------------------+&lt;br&gt;
| Electrical &amp;amp; Environmental         | Structural &amp;amp; Photometric           |&lt;br&gt;
+------------------------------------+------------------------------------+&lt;br&gt;
| • Moisture Capillary Ingress       | • Wind Shear Fatigue on Rigging    |&lt;br&gt;
| • Sub-Zero PVC Embrittlement       | • Terminal-End Voltage Attenuation |&lt;br&gt;
| • Galvanic Pin Joint Corrosion     | • Thermal Dissipation Bottlenecks  |&lt;br&gt;
+------------------------------------+------------------------------------+&lt;br&gt;
A. Moisture Ingress &amp;amp; Dielectric BreakdownConsumer-grade LED strings (IP44 or lower) rely on friction-fit pin junctions and thin PVC jackets. Capillary action draws water into non-hermetic housings during heavy rain or thermal contraction cycles. This drops dielectric insulation resistance, causing ground leaks that trip Residual Current Devices (RCD/GFCI) across the distribution panel.B. Sub-Zero Thermal EmbrittlementStandard Polyvinyl Chloride (PVC) cable jackets lose plasticizer flexibility at sub-zero temperatures ($&amp;lt;0^\circ\text{C}$). Wind-induced flexion causes micro-fractures in frozen PVC jackets, exposing internal copper conductors to ambient moisture and accelerating galvanic corrosion.C. Voltage Drop &amp;amp; Far-End Color Temperature (CCT) ShiftOn long low-voltage DC or uncompensated AC runs, conductor resistance converts electrical energy to heat. This voltage drop causes visible output attenuation (dimming) and correlated color temperature (CCT) shifts at the far end of the run.2. Industrial Material &amp;amp; Hardware Specification MatrixTo build an exterior LED installation capable of enduring extreme seasonal conditions, deploy components that meet industrial architectural benchmarks:Engineering ParameterConsumer / Entry-Level GradeIndustrial Architectural StandardIngress Protection (IP)IP44 (Splash-resistant)IP65 (Main LED runs) / IP68 (Molded screw-lock couplings)Cable Jacket CompoundThin PVC / Non-rated rubberH07RN-F Synthetic Neoprene Rubber (-25°C flex rating)LED EngineUnbinned low-lumen SMDEpistar / Sanan High-Lumen Chips (50,000 hrs L70 lifespan)Luminous EfficacyClass B/C Energy RatingA++ Efficacy Drivers (Up to 90% power reduction vs halogen)Coupling MechanismFriction-fit pin jointsThreaded compression couplings with silicone O-ringsStructural RiggingZip ties / Direct nails316 Stainless Steel Aircraft Cable ($3\,\text{mm}$-$6\,\text{mm}$) + TurnbucklesPre-Deployment ValidationDirect field assembly3D Digital Mockup &amp;amp; Electrical Load Modeling3. Conductor Sizing &amp;amp; Voltage Drop CalculationsTo maintain uniform luminous flux across extended LED strings, calculate voltage drop $\Delta V$ using the standard two-wire DC/single-phase AC conductor formula:$$\Delta V = \frac{2 \cdot L \cdot I \cdot \rho}{A}$$Where:$L$ = One-way conductor length ($\text{meters}$)$I$ = Circuit load current ($\text{amperes}$)$\rho$ = Copper resistivity ($\approx 0.0175\,\Omega\cdot\text{mm}^2/\text{m}$ at $20^\circ\text{C}$)$A$ = Conductor cross-sectional area ($\text{mm}^2$)             PARALLEL BUS POWER DISTRIBUTION TOPOLOGY&lt;/p&gt;

&lt;p&gt;[ 230V AC Power Feed ]&lt;br&gt;
            │&lt;br&gt;
            ├───► [ IP67 Driver ] ───► [ Main Bus Line (2.5mm²) ]&lt;br&gt;
                                              │&lt;br&gt;
                                              ├───► [ IP68 Segment 1 ]&lt;br&gt;
                                              ├───► [ IP68 Segment 2 ]&lt;br&gt;
                                              └───► [ IP68 Segment 3 ]&lt;br&gt;
Mitigation Strategies:Parallel Bus Feed: Instead of daisy-chaining light strings end-to-end in series, run a heavy-gauge trunk line ($1.5\,\text{mm}^2$ or $2.5\,\text{mm}^2$) and tap into individual light segments in parallel.High-Voltage AC Topology: Utilize 230V AC inline-rectified LED strings with localized IP68 current-limiting ICs over long distances to minimize current draw ($I$) and reduce line losses.High Power Factor Drivers: Deploy industrial power supplies with Power Factor Correction (PFC $&amp;gt; 0.95$) to reduce reactive power losses across the network.4. Structural Rigging &amp;amp; Substrate Attachment MethodsDifferent facade substrates require tailored mounting strategies to support wind shear without causing structural damage:                      ┌──────────────────────────────────────────┐&lt;br&gt;
                      │    Exterior Structural Mounting Types    │&lt;br&gt;
                      └────────────────────┬─────────────────────┘&lt;br&gt;
                                           │&lt;br&gt;
            ┌──────────────────────────────┼──────────────────────────────┐&lt;br&gt;
            ▼                              ▼                              ▼&lt;br&gt;
    ┌───────────────┐              ┌───────────────┐              ┌───────────────┐&lt;br&gt;
    │ Masonry &amp;amp;     │              │ Glass/Alum    │              │ Living Trees  │&lt;br&gt;
    │ Stone Cladding│              │ Curtain Walls │              │ &amp;amp; Softscapes  │&lt;br&gt;
    └───────┬───────┘              └───────┬───────┘              └───────┬───────┘&lt;br&gt;
            │                              │                              │&lt;br&gt;
            ├─ 316 Stainless Tension Grids ├─ Structural Mullion Clamps   ├─ Spring-Loaded Expansion Wraps&lt;br&gt;
            ├─ Chemical Anchor Eye-Bolts   ├─ High-Tension Vacuum Rigs    ├─ Helical Earth Screw Anchors&lt;br&gt;
            └─ Non-Destructive Cleats      └─ Custom Aluminum Framework   └─ Low-Voltage IP65 Harnesses&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Curtain Wall Glass &amp;amp; Composite CladdingStructural Mullion Clamping: Custom-machined aluminum clamps lock directly onto vertical structural mullions without drilling into rain-screens or breaching weather seals.316 Stainless Tension Grids: High-aspect-ratio LED curtains are supported by $316$ stainless steel perimeter wire ropes ($3\,\text{mm}$-$6\,\text{mm}$) held under tension by heavy-duty turnbuckles, isolating the electrical components from structural loads.2. Living Trees &amp;amp; Urban Landscape FeaturesExpansion-Spring Trunk Wraps: Avoid rigid wire ties or nails. Use UV-stabilized, spring-loaded tensioning bands that expand as the trunk grows, preventing cambium layer damage.Helical Earth Anchors: Secure large 3D ground structures (e.g., custom light sculptures) using helical ground screws rated for regional wind shear forces rather than standard stakes.5. Deployment &amp;amp; Execution WorkflowSite Audit &amp;amp; Infrastructure Analysis: Inspect main distribution panel capacity, calculate peak current draw, and verify structural anchor load ratings.CAD &amp;amp; 3D Digital Mockup: Superimpose lighting layouts onto CAD models or high-resolution imagery to verify spatial proportions, optical density, and daytime wire concealment pathways.Bench Pre-Assembly &amp;amp; Megohmmeter Testing: Pre-assemble hardware in a controlled environment. Test insulation resistance using a Megohmmeter ($500\text{V DC}$) across all IP68 connections prior to lifting.Certified High-Access Rigging: Execute installation using articulated boom lifts operated by certified high-altitude technicians working under strict safety compliance.Post-Season Demounting &amp;amp; Asset Management: Systematically dismantle, inspect O-rings and seals, tag components, and transfer hardware to climate-controlled storage for future seasonal cycles.For commercial facade installations, municipal lighting projects, and custom turnkey deployments, teams like LED Işıklandırma deliver end-to-end engineering services—from initial 3D digital mockup rendering to IP68 hardware specification and certified high-altitude rigging.6. Architectural Implementation FAQsWhat Correlated Color Temperature (CCT) should be specified for modern vs. historic facades?Historic stone, brick, or masonry structures perform best with warm color temperatures (2700K–3000K Warm White) to highlight natural earthy textures. Modern glass and aluminum curtain walls benefit from neutral or cool output (4000K–6000K Cold White) or dynamic RGBW systems that emphasize clean architectural lines.Why do IP68 threaded couplings outperform self-amalgamating tape?Self-amalgamating tape degrades over time due to UV exposure and continuous thermal expansion/contraction. Threaded IP68 couplings feature internal silicone O-rings and threaded compression collars that maintain a permanent hermetic seal even under heavy downpours or temporary submersion.What control protocols (DMX512, SPI, or fixed AC/DC topology) or structural substrates are you designing for in your upcoming outdoor lighting builds?&lt;/li&gt;
&lt;/ol&gt;

</description>
    </item>
  </channel>
</rss>
