<?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: Leon</title>
    <description>The latest articles on DEV Community by Leon (@laconicleon).</description>
    <link>https://dev.to/laconicleon</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%2F4059719%2F41d9b1c3-d318-4507-a4d7-64357dba5687.png</url>
      <title>DEV Community: Leon</title>
      <link>https://dev.to/laconicleon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/laconicleon"/>
    <language>en</language>
    <item>
      <title>Testing React Page Titles Before JavaScript Runs and After Navigation</title>
      <dc:creator>Leon</dc:creator>
      <pubDate>Mon, 02 Nov 2026 03:08:02 +0000</pubDate>
      <link>https://dev.to/laconicleon/what-an-ai-rap-video-generator-taught-me-about-crawlable-react-pages-2026-8b7</link>
      <guid>https://dev.to/laconicleon/what-an-ai-rap-video-generator-taught-me-about-crawlable-react-pages-2026-8b7</guid>
      <description>&lt;p&gt;Disclosure: I build &lt;a href="https://myairap.video/" rel="noopener noreferrer"&gt;MyAIRap.video&lt;/a&gt;. This development case study was written with AI assistance and checked against the project's release records and a production test.&lt;/p&gt;

&lt;p&gt;The MyAIRap.video homepage looked complete in Chrome. It had examples, pricing, and a link to the video creator. A raw HTTP request returned a heading, two paragraphs, and a link to the creator. The pricing link I could see in Chrome was missing from the initial HTML.&lt;/p&gt;

&lt;p&gt;We already had a Python prerender step. It wrote a short summary for each public route, while React rendered the full page in the browser. That meant we maintained two versions of the content for the same URL. We also found a separate problem on &lt;code&gt;/pricing&lt;/code&gt;. The initial HTML had a pricing title, but the browser could show the homepage title after React took over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replace the hand-written summaries with rendered React components
&lt;/h2&gt;

&lt;p&gt;Here is the complete &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt; from the old homepage build artifact, reformatted for readability:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;main&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;AI RAP VIDEO Generator&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Make your photo rap.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Create AI rap videos from your photos with MyAIRap.video. Go solo, rap with a friend, or let your pet take the mic. Pick a style and make it yours.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/create"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Create original Rap&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We changed the build to render the existing React components for the homepage, pricing, tutorial, and articles. Python still assembled the page document around that output. The rendering happened at build time. Account balances and orders stayed behind authentication.&lt;/p&gt;

&lt;p&gt;These are two excerpts from the new homepage artifact, with the surrounding markup omitted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/pricing"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Pricing&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hero-display"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;AI RAP VIDEO Generator&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hero-description"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Your face. Your words. A whole new energy. Turn a photo into an original Rap video.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Previous build artifact&lt;/th&gt;
&lt;th&gt;November 1 build artifact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;1,175 bytes&lt;/td&gt;
&lt;td&gt;29,099 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/pricing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,065 bytes&lt;/td&gt;
&lt;td&gt;13,077 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The new artifacts matched the SHA-256 hashes of the production responses captured during release verification. The byte counts helped spot unexpectedly small responses. The checks that mattered were more specific: the homepage contained a real link to &lt;code&gt;/pricing&lt;/code&gt;, and the pricing HTML contained amounts from the product catalog.&lt;/p&gt;

&lt;h2&gt;
  
  
  A correct &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; in the response can still become stale in the browser
&lt;/h2&gt;

&lt;p&gt;The old pricing artifact already had a route-specific title:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Pricing &lt;span class="ni"&gt;&amp;amp;amp;&lt;/span&gt; credits | MyAIRap.video&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A test that only fetched &lt;code&gt;/pricing&lt;/code&gt; could therefore pass while the browser still showed the wrong title. We put the metadata for each public route in a shared definition used by the build and by client-side navigation. The November 1 pricing artifact contained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;AI Rap Video Pricing: One-Time Credits, No Subscription&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"canonical"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://myairap.video/pricing"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The title copy changed in a later release. On November 3, 2026, I ran the following check against production using &lt;code&gt;playwright-core&lt;/code&gt; and headless Edge. The code below uses a normal package import in place of the machine-specific path used to load &lt;code&gt;playwright-core&lt;/code&gt; in the test environment. The expected title came from the current route configuration.&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;assert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:assert/strict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playwright-core&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;async &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="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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://myairap.video&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;url&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;/pricing&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;href&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;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AI Rap Video Generator Pricing 2026 | MyAIRap.video&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&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;response&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;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;title&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;title&amp;gt;/i&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="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expected&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;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;msedge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headless&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="k"&gt;try&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;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;home&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;home&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expected&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__titleCheck&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a[href="/pricing"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;click&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForURL&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;title&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expected&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;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expected&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;sameDocument&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__titleCheck&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sameDocument&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="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="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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;home&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="nx"&gt;sameDocument&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&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;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&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;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="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exitCode&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The run returned:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"raw"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AI Rap Video Generator Pricing 2026 | MyAIRap.video"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"home"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AI Rap Video Generator 2026 from Photos | MyAIRap.video"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AI Rap Video Generator Pricing 2026 | MyAIRap.video"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://myairap.video/pricing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sameDocument"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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 &lt;code&gt;window&lt;/code&gt; marker was there to make the test discriminate between a client-side route change and a full page load. Without it, the title assertion could pass after a full load simply because the server had already sent the correct title. The marker survived the click, and the title changed from the homepage value to the pricing value in the same document.&lt;/p&gt;

&lt;p&gt;This is the pair of checks I would keep on another React site. Fetch an important route and assert something specific about its raw response. Then reach it through an in-app link and check the browser state. These checks establish what the server sent and what the app did with it. Search indexing needs its own evidence.&lt;/p&gt;

</description>
      <category>react</category>
      <category>seo</category>
      <category>startup</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Browser-Local Moving Checklist Without User Accounts</title>
      <dc:creator>Leon</dc:creator>
      <pubDate>Thu, 10 Sep 2026 03:27:47 +0000</pubDate>
      <link>https://dev.to/laconicleon/building-a-browser-local-moving-checklist-without-user-accounts-3fgk</link>
      <guid>https://dev.to/laconicleon/building-a-browser-local-moving-checklist-without-user-accounts-3fgk</guid>
      <description>&lt;p&gt;Many planning tools begin with sign-up, cloud storage, and collaboration features. For a moving checklist, I wanted to test a narrower idea: can a useful planning tool work without requiring an account?&lt;/p&gt;

&lt;p&gt;MovePlaybook is a browser-based moving checklist. A user starts with a move date, reviews a generated task plan, adjusts tasks when circumstances change, and can print or save the plan as a PDF.&lt;/p&gt;

&lt;p&gt;The technical constraint was privacy: the product should remain useful even when the plan is stored locally in the browser. That shaped several decisions.&lt;/p&gt;

&lt;p&gt;First, task identity needs to be stable. A user may complete, reschedule, hide, restore, or annotate a task, so those changes must attach to a durable task ID rather than a display position.&lt;/p&gt;

&lt;p&gt;Second, temporary hiding must not mean deletion. A hidden task is excluded from the active checklist and PDF, but it remains recoverable through a dedicated Hidden view.&lt;/p&gt;

&lt;p&gt;Third, schedule changes need explanations. When a user adjusts a date, the interface should distinguish between a recommended date, a tight timing window, a conflict, or something the user must confirm locally.&lt;/p&gt;

&lt;p&gt;The product is available at &lt;a href="https://moveplaybook.app/" rel="noopener noreferrer"&gt;https://moveplaybook.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I built it as a practical experiment in local-first planning. I would be interested in how other developers approach versioned browser storage, printable application state, and explainable scheduling rules.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>privacy</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to convert a folder of PNGs to one PDF without uploading the files</title>
      <dc:creator>Leon</dc:creator>
      <pubDate>Sat, 05 Sep 2026 12:48:21 +0000</pubDate>
      <link>https://dev.to/laconicleon/how-to-convert-a-folder-of-pngs-to-one-pdf-without-uploading-the-files-19pe</link>
      <guid>https://dev.to/laconicleon/how-to-convert-a-folder-of-pngs-to-one-pdf-without-uploading-the-files-19pe</guid>
      <description>&lt;h2&gt;
  
  
  A simple browser-local PNG-to-PDF workflow
&lt;/h2&gt;

&lt;p&gt;For this kind of job, the useful workflow is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select the PNG, JPG, or JPEG files.&lt;/li&gt;
&lt;li&gt;Put the pages in the order they should appear.&lt;/li&gt;
&lt;li&gt;Choose a page size and margins if the document needs them.&lt;/li&gt;
&lt;li&gt;Export one PDF.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important detail is where the conversion happens. A browser-local PNG-to-PDF tool processes the images in the browser instead of uploading them to a conversion server. That makes it easier to keep control of source files while still producing one shareable PDF.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this is useful
&lt;/h2&gt;

&lt;p&gt;This workflow is handy for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;combining screenshots into a bug report or handoff document;&lt;/li&gt;
&lt;li&gt;turning scanned pages into one file for email or printing;&lt;/li&gt;
&lt;li&gt;arranging portfolio images or design exports in a deliberate order; and&lt;/li&gt;
&lt;li&gt;collecting receipts or reference images without making a separate document first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before exporting, check the page order and decide whether each page should match the image, A4, or US Letter. A preview is useful here: it catches a stray portrait page, an oversized margin, or a screenshot in the wrong position before the PDF is created.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool I use for this
&lt;/h2&gt;

&lt;p&gt;I maintain &lt;a href="https://pngbinder.com/" rel="noopener noreferrer"&gt;PNG Binder&lt;/a&gt;, a free PNG-to-PDF converter for this specific workflow. It accepts up to 50 PNG, JPG, or JPEG images, lets you arrange them, and creates one PDF locally in the browser. It does not require an account, and the images are not sent to a conversion server.&lt;/p&gt;

&lt;p&gt;It creates an image-based PDF, so it does not perform OCR or rebuild text and tables. If that is the kind of result you need, try it and let me know whether page ordering, page settings, or browser compatibility could be improved.&lt;/p&gt;




&lt;p&gt;Disclosure: I am the maker and operator of PNG Binder.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>privacy</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>A small privacy decision that changed my web calculator</title>
      <dc:creator>Leon</dc:creator>
      <pubDate>Thu, 03 Sep 2026 10:00:20 +0000</pubDate>
      <link>https://dev.to/laconicleon/a-small-privacy-decision-that-changed-my-web-calculator-5c99</link>
      <guid>https://dev.to/laconicleon/a-small-privacy-decision-that-changed-my-web-calculator-5c99</guid>
      <description>&lt;p&gt;I built Calculator Toolkit for a narrow moment: someone has a restaurant bill, wants to check the tip and tax math, then needs to split the total with a few people.&lt;/p&gt;

&lt;p&gt;At first I treated the no-account requirement as a small feature. It turned out to be the decision that shaped most of the interface.&lt;/p&gt;

&lt;p&gt;A saved-history feature would be convenient. It would also mean sign-in, account recovery, data retention, and an extra decision for someone who only wants to settle a dinner bill. I decided that the calculator should run locally in the browser instead. There is no account and no bill-data upload.&lt;/p&gt;

&lt;p&gt;That creates constraints. A person cannot return tomorrow and recover an earlier calculation. There is no dashboard where an unclear result can be explained later. The current screen has to do the work. It needs to make the bill amount, tax, tip basis, total, and each person's share understandable before the group pays.&lt;/p&gt;

&lt;p&gt;I found that this was less about adding a privacy notice and more about removing places where the product could hide complexity. If the calculation is temporary, the result needs to be legible now. If somebody changes the tip basis, the change needs to be obvious. If one diner pays a different share, the total still has to add up.&lt;/p&gt;

&lt;p&gt;I do not think local-only is a universal rule. It is a poor fit for products where history, collaboration, or handoffs are the reason people use the product. For a one-off restaurant calculation, it felt like the more honest default.&lt;/p&gt;

&lt;p&gt;I am Leon, the maker of &lt;a href="https://calculator-toolkit.com/" rel="noopener noreferrer"&gt;Calculator Toolkit&lt;/a&gt;. I would like feedback from other developers: when you remove accounts or saved state from a small tool, which parts of the interface become harder to get right?&lt;/p&gt;

&lt;p&gt;Disclosure: This article was drafted with AI and reviewed by Leon.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>privacy</category>
      <category>ux</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
