<?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: Maegami</title>
    <description>The latest articles on DEV Community by Maegami (@maegamidev).</description>
    <link>https://dev.to/maegamidev</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%2F4008837%2Ff88119ed-7329-47f0-9be3-c8e6666843c4.png</url>
      <title>DEV Community: Maegami</title>
      <link>https://dev.to/maegamidev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maegamidev"/>
    <language>en</language>
    <item>
      <title>I got a real Next.js store to 98 on mobile PageSpeed. Here is what actually moved it, and the trap that fooled me.</title>
      <dc:creator>Maegami</dc:creator>
      <pubDate>Sat, 04 Jul 2026 15:38:34 +0000</pubDate>
      <link>https://dev.to/maegamidev/i-got-a-real-nextjs-store-to-98-on-mobile-pagespeed-here-is-what-actually-moved-it-and-the-trap-4oko</link>
      <guid>https://dev.to/maegamidev/i-got-a-real-nextjs-store-to-98-on-mobile-pagespeed-here-is-what-actually-moved-it-and-the-trap-4oko</guid>
      <description>&lt;p&gt;I run my own Next.js 14 store (link at the end). It was sitting around the low 50s on mobile, LCP over 10 seconds. It is now 98 on mobile PSI, LCP about 3.5s. This is a live store with real traffic, not a demo, so every fix had to survive production.&lt;/p&gt;

&lt;p&gt;What actually moved the number, roughly in order of impact:&lt;/p&gt;

&lt;p&gt;Self-hosted the fonts. 15 woff2 files local, inline @font-face, preload only the critical cyrillic and latin subsets. This killed every external font connection and was the single biggest win.&lt;/p&gt;

&lt;p&gt;PNG to WebP everywhere. Category images went from about 11MB to 0.5MB, blog covers from 37MB to 1.3MB.&lt;/p&gt;

&lt;p&gt;Deferred the iframe embeds and the hero video to window.load. Their runtimes were stealing the main thread from first paint.&lt;/p&gt;

&lt;p&gt;Dropped the reveal animations on the above-fold hero.&lt;/p&gt;

&lt;p&gt;The trap: my local Lighthouse kept flapping between 63 and 99 on the exact same build. I burned an hour chasing a regression that did not exist. It was my laptop under load, not the page. A cold PSI run was the real number. Lab scores lie under local CPU pressure, trust the field.&lt;/p&gt;

&lt;p&gt;Live store: &lt;a href="https://euphoria-parfum.com.ua" rel="noopener noreferrer"&gt;https://euphoria-parfum.com.ua&lt;/a&gt;. Write-up of the build: &lt;a href="https://maegamidev.vercel.app/euphoria.html" rel="noopener noreferrer"&gt;https://maegamidev.vercel.app/euphoria.html&lt;/a&gt;. Happy to answer anything.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webperf</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Adding a third language to a static HTML site: ~60 lines of vanilla JS</title>
      <dc:creator>Maegami</dc:creator>
      <pubDate>Fri, 03 Jul 2026 13:40:46 +0000</pubDate>
      <link>https://dev.to/maegamidev/adding-a-third-language-to-a-static-html-site-60-lines-of-vanilla-js-43p2</link>
      <guid>https://dev.to/maegamidev/adding-a-third-language-to-a-static-html-site-60-lines-of-vanilla-js-43p2</guid>
      <description>&lt;p&gt;Yesterday a lead from Ukraine asked to see my work. My case study was in Russian. Sending a Russian-language portfolio to a Ukrainian client is a good way to lose them before hello.&lt;/p&gt;

&lt;p&gt;The site is static HTML on Vercel. No framework, no build step, and I wanted to keep it that way. An i18n library for two pages felt like overkill, so here's what I did instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Russian stays in the DOM, other languages are dictionaries
&lt;/h2&gt;

&lt;p&gt;The page source is the Russian version. On load I walk all text nodes once and remember their original value, whitespace and a normalized key:&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;function&lt;/span&gt; &lt;span class="nf"&gt;norm&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="k"&gt;return&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;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&lt;/span&gt;&lt;span class="sr"&gt;&amp;nbsp;&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="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="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="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;walker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createTreeWalker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NodeFilter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SHOW_TEXT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;walker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nextNode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;walker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentNode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;norm&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="nx"&gt;nodeValue&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;key&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;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__ru&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeValue&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="nx"&gt;__key&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="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__lead&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&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&lt;/span&gt;&lt;span class="sr"&gt;&amp;nbsp;&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="mi"&gt;0&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="nx"&gt;__trail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&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&lt;/span&gt;&lt;span class="sr"&gt;&amp;nbsp;&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nx"&gt;nodes&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;n&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;EN and UK are plain objects keyed by the normalized Russian string. Switching languages is rewriting nodeValue, nothing else:&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;function&lt;/span&gt; &lt;span class="nf"&gt;setLang&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;EN&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;UK&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;nodes&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="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
    &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dict&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__lead&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;node&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="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__ru&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__trail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__ru&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lang&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;persist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;langManual&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;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lang&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lang&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;If a string is missing from the dictionary it stays Russian, so a half-translated page still works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detection order matters more than detection
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;manual choice from localStorage wins forever&lt;/li&gt;
&lt;li&gt;otherwise country by IP (ipapi.co free tier, 1.5s timeout), UA gets Ukrainian&lt;/li&gt;
&lt;li&gt;if the IP call is slow or blocked, navigator.language already picked a language, so nothing blinks&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first version waited for the IP response and the page sat in Russian for a second. Felt broken. Render from browser language immediately, treat geo as an upgrade.&lt;/p&gt;

&lt;p&gt;Limitations, honestly: this only swaps text nodes. Attributes like placeholder or aria-label need separate handling, and if one sentence is split across several tags the keys get ugly. For a portfolio it's more than enough.&lt;/p&gt;

&lt;p&gt;Live: &lt;a href="https://maegamidev.vercel.app" rel="noopener noreferrer"&gt;https://maegamidev.vercel.app&lt;/a&gt; (dropdown top right). The case study it was built for: &lt;a href="https://maegamidev.vercel.app/euphoria.html" rel="noopener noreferrer"&gt;https://maegamidev.vercel.app/euphoria.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sent the links to the lead the same evening.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>i18n</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built a Next.js 16 store starter for perfume &amp; cosmetics (and learned a few things)</title>
      <dc:creator>Maegami</dc:creator>
      <pubDate>Tue, 30 Jun 2026 00:55:04 +0000</pubDate>
      <link>https://dev.to/maegamidev/i-built-a-nextjs-16-store-starter-for-perfume-cosmetics-and-learned-a-few-things-fgg</link>
      <guid>https://dev.to/maegamidev/i-built-a-nextjs-16-store-starter-for-perfume-cosmetics-and-learned-a-few-things-fgg</guid>
      <description>&lt;p&gt;Honestly a bit scary to post this. I'm a self-taught dev, and Aura is the first thing I've actually finished and put up for sale. It's a Next.js 16 storefront starter, but I didn't want to make "another generic shop", so I built it specifically for perfume and cosmetics.&lt;/p&gt;

&lt;p&gt;Live demo: &lt;a href="https://aura-starter.vercel.app" rel="noopener noreferrer"&gt;https://aura-starter.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's why I niched it, and two things that confused me while building, in case it helps someone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why perfume &amp;amp; cosmetics
&lt;/h2&gt;

&lt;p&gt;I kept noticing fragrance and cosmetics aren't sold like normal products:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;People buy &lt;strong&gt;decants&lt;/strong&gt; first (2, 5 or 10 ml of a scent) before paying for a full bottle. So instead of a product photo, my hero is three little apothecary vials, filled to different heights by size.&lt;/li&gt;
&lt;li&gt;Every fragrance has a &lt;strong&gt;note pyramid&lt;/strong&gt; (top / heart / base). Buyers actually care how it opens and where it lands.&lt;/li&gt;
&lt;li&gt;Cosmetics need real &lt;strong&gt;shade swatches&lt;/strong&gt; and a short &lt;strong&gt;ingredients&lt;/strong&gt; list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most templates ignore all that. I figured building around how people actually buy is what makes it worth something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thing that confused me #1: React 19 and refs
&lt;/h2&gt;

&lt;p&gt;I have a hover panel that slides in with product info, and I wanted it to keep showing the last product while it slides away. I reached for a ref:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastShown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;lastShown&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// React 19 yelled at me here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turns out React 19 has a new lint rule that won't let you read or write refs during render. Took me a while to get it. The fix was just to keep it in state and set it in the handler:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;shownSlug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setShownSlug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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;open&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;setActiveSlug&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="nf"&gt;setShownSlug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cleaner in the end, honestly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thing that confused me #2: making graphics work in dark mode
&lt;/h2&gt;

&lt;p&gt;The decant vials are just divs, not images. I didn't want to draw them twice for light and dark, so instead of fixed colors I used CSS variables for the fill:&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="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;linear-gradient(180deg, var(--accent), var(--accent-strong))&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;Since the variable flips with the theme, the same vials just work in both. No &lt;code&gt;dark:&lt;/code&gt; copies, no second SVG. Small thing, but it made me happy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Also, Next 16 + Tailwind v4
&lt;/h2&gt;

&lt;p&gt;If you're starting fresh: &lt;code&gt;params&lt;/code&gt; and &lt;code&gt;searchParams&lt;/code&gt; are promises now (you &lt;code&gt;await&lt;/code&gt; them), and Tailwind v4 config lives in &lt;code&gt;globals.css&lt;/code&gt;. Took me a minute coming from older versions.&lt;/p&gt;




&lt;p&gt;That's it really. It's typed, has tests, light/dark, and the hover thing.&lt;/p&gt;

&lt;p&gt;Demo: &lt;a href="https://aura-starter.vercel.app" rel="noopener noreferrer"&gt;https://aura-starter.vercel.app&lt;/a&gt;&lt;br&gt;
If it's useful to anyone, it's up here: &lt;a href="https://maegami.gumroad.com/l/aura/LAUNCH" rel="noopener noreferrer"&gt;https://maegami.gumroad.com/l/aura/LAUNCH&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is genuinely my first time selling something I made, so any feedback (design, code, whatever) would mean a lot. Thanks for reading 🙏&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
