<?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: Simran Kaur</title>
    <description>The latest articles on DEV Community by Simran Kaur (@simran_kaur_9eda1e242c31f).</description>
    <link>https://dev.to/simran_kaur_9eda1e242c31f</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%2F3958455%2F4cbc671f-a074-4f57-927e-7fa499cae870.png</url>
      <title>DEV Community: Simran Kaur</title>
      <link>https://dev.to/simran_kaur_9eda1e242c31f</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/simran_kaur_9eda1e242c31f"/>
    <language>en</language>
    <item>
      <title>How to Check a Page's Canonical URL Programmatically (Node.js)</title>
      <dc:creator>Simran Kaur</dc:creator>
      <pubDate>Tue, 08 Sep 2026 11:05:11 +0000</pubDate>
      <link>https://dev.to/simran_kaur_9eda1e242c31f/how-to-check-a-pages-canonical-url-programmatically-nodejs-1a5o</link>
      <guid>https://dev.to/simran_kaur_9eda1e242c31f/how-to-check-a-pages-canonical-url-programmatically-nodejs-1a5o</guid>
      <description>&lt;p&gt;Canonical bugs are the quiet kind. The page looks fine, it ranks for a while, and then a theme change or a new plugin points &lt;code&gt;rel="canonical"&lt;/code&gt; at the wrong URL. Google follows the tag, consolidates the wrong version, and your traffic slides. No error, no 404, nothing in the logs.&lt;/p&gt;

&lt;p&gt;If you build or maintain sites, you can catch this in code. Here is how to read a page's canonical URL in Node, classify what it means, check many pages at once, and even test IP canonicalization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where canonical URLs actually live
&lt;/h2&gt;

&lt;p&gt;Most people check the HTML. There are really two places a canonical can be declared, and a good check reads both:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The HTML &lt;code&gt;&amp;lt;link rel="canonical" href="..."&amp;gt;&lt;/code&gt; in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The HTTP &lt;code&gt;Link&lt;/code&gt; response header: &lt;code&gt;Link: &amp;lt;https://example.com/&amp;gt;; rel="canonical"&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That header version is easy to forget. It is common on PDFs and non-HTML resources, and some CDNs and frameworks set it. If the header and the HTML disagree, search engines can get conflicting signals, so you want to catch that too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the canonical in Node
&lt;/h2&gt;

&lt;p&gt;Node 18+ ships a global &lt;code&gt;fetch&lt;/code&gt;, so no dependencies are needed for a basic check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getCanonical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="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="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;follow&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="s2"&gt;User-Agent&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;canonical-check/1.0&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;let&lt;/span&gt; &lt;span class="nx"&gt;canonical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;source&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="c1"&gt;// 1) HTTP Link header&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;linkHeader&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;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;link&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;linkHeader&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;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;linkHeader&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;&lt;/span&gt;&lt;span class="se"&gt;([^&lt;/span&gt;&lt;span class="sr"&gt;&amp;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;&amp;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;\s&lt;/span&gt;&lt;span class="sr"&gt;*rel=&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;canonical&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;/i&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;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;canonical&lt;/span&gt; &lt;span class="o"&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="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http-header&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="c1"&gt;// 2) HTML &amp;lt;link rel="canonical"&amp;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;canonical&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;html&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;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&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;link&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+rel=&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;canonical&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;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*&amp;gt;/i&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;tag&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;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tag&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;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/href=&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;+&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;/i&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;href&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;canonical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;href&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;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;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="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="na"&gt;requested&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;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For production I would reach for &lt;code&gt;cheerio&lt;/code&gt; to parse the HTML instead of a regex, since regex on HTML breaks on edge cases. But for a quick audit script, the pattern above is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classifying what the canonical means
&lt;/h2&gt;

&lt;p&gt;Finding the tag is half the job. The useful part is what it tells you:&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;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requested&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canonical&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing&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;norm&lt;/span&gt; &lt;span class="o"&gt;=&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;=&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;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="nf"&gt;toLowerCase&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;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;)&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;requested&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;self-referencing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;// the healthy default&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;points-elsewhere&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// intentional for duplicates, a bug otherwise&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;self-referencing&lt;/strong&gt;: the page points to itself. This is what you want on a normal, unique page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;points-elsewhere&lt;/strong&gt;: fine for a duplicate or a paginated page consolidating to page one, a problem when it is accidental.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;missing&lt;/strong&gt;: search engines pick their own preferred version, which is a coin flip you do not want to leave to chance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put together:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;requested&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getCanonical&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://example.com/&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requested&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;canonical&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;source&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bulk-checking a whole site
&lt;/h2&gt;

&lt;p&gt;One page is never the real job. After a migration you want to scan a list:&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;urls&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="s2"&gt;https://example.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="s2"&gt;https://example.com/blog/&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;https://example.com/pricing/&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;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;url&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;requested&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canonical&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getCanonical&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;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;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requested&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&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; -&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;canonical&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MISSING&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;catch &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="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;`error            &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; -&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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;Feed it your sitemap URLs and you have a canonical audit in a few seconds. Add a small concurrency limit if the list is long so you do not hammer the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one people miss: IP canonicalization
&lt;/h2&gt;

&lt;p&gt;Here is a subtle one. If your server answers on its raw IP address as well as your domain, search engines can index both &lt;code&gt;http://203.0.113.10/&lt;/code&gt; and &lt;code&gt;https://example.com/&lt;/code&gt; as separate sites. That splits link equity for content that is literally identical.&lt;/p&gt;

&lt;p&gt;The test: resolve the domain to its IP, request the IP directly, and check whether it redirects back to the domain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dns&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;node:dns/promises&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;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;example.com&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="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lookup&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="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="s2"&gt;`http://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ip&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="na"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual&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;location&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;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;location&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;301&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;302&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;307&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;308&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;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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;location&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;host&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PASS: the IP redirects to your domain&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;else&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;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;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&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;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;300&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FAIL: the IP serves content directly (duplicate content risk)&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;else&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Blocked or no response, which usually means no IP duplicate content&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;Note that on shared hosting the IP often belongs to many sites, so this test is most meaningful on a dedicated IP or behind your own reverse proxy.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you just need an answer, not a script
&lt;/h2&gt;

&lt;p&gt;For a one-off check, or to hand to someone who does not write Node, a browser tool is faster. I built a free one that does all of the above: &lt;a href="https://pixellize.io/canonical-url-checker" rel="noopener noreferrer"&gt;Pixellize Canonical URL Checker&lt;/a&gt;. It reads both the HTML tag and the HTTP &lt;code&gt;Link&lt;/code&gt; header, flags self-referencing vs points-elsewhere vs missing, has a &lt;strong&gt;bulk mode&lt;/strong&gt; for up to 20 URLs with CSV export, and an &lt;strong&gt;IP canonicalization test&lt;/strong&gt;. It runs in the browser, nothing is uploaded.&lt;/p&gt;

&lt;p&gt;If you want the non-code background on canonicals, these two guides go deeper: &lt;a href="https://pixellize.io/blog/how-to-find-canonical-url-of-any-website" rel="noopener noreferrer"&gt;how to find the canonical URL of any website&lt;/a&gt; and &lt;a href="https://pixellize.io/blog/how-to-change-canonical-url" rel="noopener noreferrer"&gt;how to change a canonical URL&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Canonicals live in two places: the HTML &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; and the HTTP &lt;code&gt;Link&lt;/code&gt; header. Check both, and flag conflicts.&lt;/li&gt;
&lt;li&gt;In Node, &lt;code&gt;fetch&lt;/code&gt; plus a small parser is enough to read and classify canonicals.&lt;/li&gt;
&lt;li&gt;Bulk-scan your sitemap after every migration or theme change, since that is when canonicals silently break.&lt;/li&gt;
&lt;li&gt;Do not forget IP canonicalization if you run your own server or reverse proxy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How do you audit canonicals in your stack? Let me know in the comments.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>node</category>
    </item>
    <item>
      <title>Convert CSV to Excel (XLSX) in JavaScript, in the Browser and in Node</title>
      <dc:creator>Simran Kaur</dc:creator>
      <pubDate>Tue, 08 Sep 2026 10:20:35 +0000</pubDate>
      <link>https://dev.to/simran_kaur_9eda1e242c31f/convert-csv-to-excel-xlsx-in-javascript-in-the-browser-and-in-node-29a6</link>
      <guid>https://dev.to/simran_kaur_9eda1e242c31f/convert-csv-to-excel-xlsx-in-javascript-in-the-browser-and-in-node-29a6</guid>
      <description>&lt;p&gt;You export a CSV, someone opens it in Excel, and &lt;code&gt;00123&lt;/code&gt; turns into &lt;code&gt;123&lt;/code&gt;. A phone column becomes scientific notation. Dates flip from day-month to month-day depending on the machine's locale. If you have ever shipped a "data export" feature, you have gotten that bug report.&lt;/p&gt;

&lt;p&gt;The root cause is simple: a CSV is plain text with no type information. When Excel opens it, it &lt;em&gt;guesses&lt;/em&gt; what each column is. A real &lt;code&gt;.xlsx&lt;/code&gt; file stores the type per cell, so once you write XLSX yourself you get to decide, and the guessing stops.&lt;/p&gt;

&lt;p&gt;Here is how to convert CSV to Excel in JavaScript, both server-side in Node and fully in the browser, without losing your data along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-liner that works for clean data
&lt;/h2&gt;

&lt;p&gt;If your data is genuinely simple (no IDs with leading zeros, no long numbers, no ambiguous dates), the fastest path uses the &lt;code&gt;xlsx&lt;/code&gt; package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;xlsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;XLSX&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="s2"&gt;xlsx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// read the CSV straight into a workbook&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workbook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;XLSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;employees.csv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// write it back out as a real .xlsx&lt;/span&gt;
&lt;span class="nx"&gt;XLSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workbook&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;employees.xlsx&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;Two lines of real work. &lt;code&gt;readFile&lt;/code&gt; parses the CSV into a sheet, &lt;code&gt;writeFile&lt;/code&gt; serializes it to XLSX. Done.&lt;/p&gt;

&lt;p&gt;The catch: &lt;code&gt;readFile&lt;/code&gt; still lets the parser coerce values. &lt;code&gt;00123&lt;/code&gt; can still come out as the number &lt;code&gt;123&lt;/code&gt;. So for anything that matters, keep reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping leading zeros, long numbers, and dates intact
&lt;/h2&gt;

&lt;p&gt;The fix is to read the CSV as raw strings, then force the fragile columns to the text type before you write. Here is a version that treats an &lt;code&gt;Employee ID&lt;/code&gt; column as text so zeros survive:&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;XLSX&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="s2"&gt;xlsx&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;fs&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="s2"&gt;fs&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;csv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;employees.csv&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;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// parse as strings, do not let the parser guess types&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workbook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;XLSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;raw&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sheet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;workbook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Sheets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;workbook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SheetNames&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="c1"&gt;// walk the cells and pin column A (Employee ID) to text&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;XLSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;utils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode_range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;!ref&lt;/span&gt;&lt;span class="dl"&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;let&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;range&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="nx"&gt;r&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="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;range&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;r&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;row&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;addr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;XLSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;utils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode_cell&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;c&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="c1"&gt;// column A&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;addr&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;cell&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;s&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                 &lt;span class="c1"&gt;// s = string&lt;/span&gt;
    &lt;span class="nx"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cell&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="c1"&gt;// keep the original text, zeros and all&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;XLSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workbook&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;employees.xlsx&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;&lt;code&gt;cell.t = "s"&lt;/code&gt; is the whole trick. It tells the XLSX writer "this is text, not a number," so Excel shows &lt;code&gt;00123&lt;/code&gt; exactly as written instead of helpfully stripping the zeros.&lt;/p&gt;

&lt;p&gt;The same idea covers the other classic breakages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Long IDs (16+ digits):&lt;/strong&gt; set them to text, or Excel renders &lt;code&gt;1.2E+15&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dates:&lt;/strong&gt; either keep them as text in a known format like &lt;code&gt;YYYY-MM-DD&lt;/code&gt;, or write real date cells with &lt;code&gt;cell.t = "d"&lt;/code&gt; and a &lt;code&gt;cell.z&lt;/code&gt; number format so every machine reads them the same way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encoding:&lt;/strong&gt; read the file as UTF-8 (as above) so accented names like &lt;code&gt;Muller&lt;/code&gt; do not turn into symbols.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Doing it entirely in the browser
&lt;/h2&gt;

&lt;p&gt;You do not need a server for this. The same library runs client-side, so the file never leaves the user's machine, which is the right call for anything private like a customer list.&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;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt; &lt;span class="na"&gt;accept=&lt;/span&gt;&lt;span class="s"&gt;".csv"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/xlsx/dist/xlsx.full.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;change&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&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;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&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;file&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="c1"&gt;// parse the CSV as strings so nothing gets coerced&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;XLSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// hand the user a real .xlsx download&lt;/span&gt;
  &lt;span class="nx"&gt;XLSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&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="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;csv$/i&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="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.xlsx&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="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;file.text()&lt;/code&gt; reads the CSV, &lt;code&gt;XLSX.read&lt;/code&gt; parses it, and &lt;code&gt;writeFile&lt;/code&gt; in the browser triggers a download of the generated workbook. No backend, no upload, no temp files to clean up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Batch converting a folder in Node
&lt;/h2&gt;

&lt;p&gt;When you have a directory of CSVs to convert on a schedule, loop over them:&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;XLSX&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="s2"&gt;xlsx&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;fs&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="s2"&gt;fs&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;path&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="s2"&gt;path&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;dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./exports&lt;/span&gt;&lt;span class="dl"&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;name&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&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;f&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;f&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="s2"&gt;.csv&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;wb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;XLSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&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="nx"&gt;dir&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;XLSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&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="nx"&gt;dir&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="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;csv$/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.xlsx&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;Drop this in a cron job or a CI step and your CSV drops become XLSX automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you just need it once, without writing code
&lt;/h2&gt;

&lt;p&gt;Not every CSV-to-Excel job deserves a script. If you are handing the task to a non-developer, or you just need a clean workbook once, a browser tool is faster than wiring up a project.&lt;/p&gt;

&lt;p&gt;I built a free one for exactly this: &lt;a href="https://pixellize.io/csv-to-xlsx" rel="noopener noreferrer"&gt;Pixellize CSV to XLSX converter&lt;/a&gt;. Drop a &lt;code&gt;.csv&lt;/code&gt;, review the parsed grid (it keeps leading zeros), edit any cell, and download a real &lt;code&gt;.xlsx&lt;/code&gt;. It runs fully in the browser, so nothing is uploaded. There is also an &lt;a href="https://pixellize.io/xlsx-to-csv" rel="noopener noreferrer"&gt;XLSX to CSV converter&lt;/a&gt; and an &lt;a href="https://pixellize.io/excel-to-json" rel="noopener noreferrer"&gt;Excel to JSON tool&lt;/a&gt; for the reverse trips.&lt;/p&gt;

&lt;p&gt;If you want the non-code walkthrough with screenshots and the Excel-specific gotchas, I wrote that up here: &lt;a href="https://pixellize.io/blog/how-to-convert-csv-to-excel" rel="noopener noreferrer"&gt;How to convert CSV to Excel without breaking your data&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A CSV has no type information, so opening it in Excel means letting Excel guess. Writing XLSX yourself removes the guessing.&lt;/li&gt;
&lt;li&gt;In JavaScript, &lt;code&gt;XLSX.read&lt;/code&gt; plus &lt;code&gt;XLSX.writeFile&lt;/code&gt; does the conversion in two steps, in Node or in the browser.&lt;/li&gt;
&lt;li&gt;Read values as strings and set &lt;code&gt;cell.t = "s"&lt;/code&gt; on fragile columns to keep leading zeros and long numbers.&lt;/li&gt;
&lt;li&gt;Handle dates explicitly and read as UTF-8 so nothing silently changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Got a favorite approach for typed CSV imports? Drop it in the comments.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Detect What WordPress Theme (and Stack) a Website Uses</title>
      <dc:creator>Simran Kaur</dc:creator>
      <pubDate>Thu, 03 Sep 2026 07:23:48 +0000</pubDate>
      <link>https://dev.to/simran_kaur_9eda1e242c31f/how-to-detect-what-wordpress-theme-and-stack-a-website-uses-5751</link>
      <guid>https://dev.to/simran_kaur_9eda1e242c31f/how-to-detect-what-wordpress-theme-and-stack-a-website-uses-5751</guid>
      <description>&lt;p&gt;You land on a slick website and wonder: is this WordPress, and if so, what theme is it running? You can usually answer both in under a minute by reading a few signals the site leaves in its HTML and a couple of predictable endpoints. Here is how to do it by hand, and how to script it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five signals that give WordPress away
&lt;/h2&gt;

&lt;p&gt;WordPress leaves fingerprints. Any one of these is a strong hint; two or more is near certainty.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Asset paths&lt;/strong&gt; contain &lt;code&gt;/wp-content/&lt;/code&gt; or &lt;code&gt;/wp-includes/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A login page&lt;/strong&gt; exists at &lt;code&gt;/wp-login.php&lt;/code&gt; (and &lt;code&gt;/wp-admin/&lt;/code&gt; redirects to it)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The generator meta tag&lt;/strong&gt;: &lt;code&gt;&amp;lt;meta name="generator" content="WordPress 6.x"&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The REST API&lt;/strong&gt; responds at &lt;code&gt;/wp-json/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The theme path&lt;/strong&gt;: styles load from &lt;code&gt;/wp-content/themes/&amp;lt;theme-name&amp;gt;/&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last one is the good part. The folder name after &lt;code&gt;/themes/&lt;/code&gt; is usually the theme slug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking by hand
&lt;/h2&gt;

&lt;p&gt;Open the page source (Ctrl+U) and search for &lt;code&gt;wp-content&lt;/code&gt;. If it is there, it is almost certainly WordPress. Then search for &lt;code&gt;/themes/&lt;/code&gt; to read the theme slug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/wp-content/themes/astra/style.css
                                    ^^^^^ theme slug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hit &lt;code&gt;/wp-json/&lt;/code&gt; in the address bar. A JSON response with &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, and &lt;code&gt;routes&lt;/code&gt; confirms WordPress and often leaks the site title and active plugins via the REST routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scripting it
&lt;/h2&gt;

&lt;p&gt;Here is a minimal Node check that fetches the HTML and reports the signals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;detectWordPress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&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="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isWP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="sr"&gt;/wp-content|wp-includes/&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;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="sr"&gt;/&amp;lt;meta name="generator" content="WordPress/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;html&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;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&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;\/&lt;/span&gt;&lt;span class="sr"&gt;wp-content&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;themes&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;/&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="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;generator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&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;meta name="generator" content="&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;WordPress&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;"/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="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="k"&gt;return&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;isWP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;generator&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;detectWordPress&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://example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&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="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// { url: "...", isWP: true, theme: "astra", generator: "WordPress 6.6" }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For plugins, scan asset URLs for &lt;code&gt;/wp-content/plugins/&amp;lt;plugin-slug&amp;gt;/&lt;/code&gt;. Each unique slug is a plugin loading a script or stylesheet on the front end.&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;plugins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;html&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;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;wp-content&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;plugins&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;/g&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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="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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why detection sometimes fails
&lt;/h2&gt;

&lt;p&gt;It is not foolproof, and it is worth knowing the blind spots:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Headless WordPress&lt;/strong&gt;: the front end is React or Next.js, so &lt;code&gt;wp-content&lt;/code&gt; never appears. The &lt;code&gt;/wp-json/&lt;/code&gt; endpoint may still respond.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security plugins&lt;/strong&gt; rename the login URL and strip the generator tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggressive caching/optimization&lt;/strong&gt; can rewrite or inline asset paths, hiding theme folders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDNs&lt;/strong&gt; sometimes serve assets from a different host, so match the path, not the domain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So treat a negative result as "probably not, but check &lt;code&gt;/wp-json/&lt;/code&gt; before you are sure."&lt;/p&gt;

&lt;h2&gt;
  
  
  When you just want the answer
&lt;/h2&gt;

&lt;p&gt;If you do not want to open a console, paste the URL into the &lt;a href="https://pixellize.io/wordpress-theme-detector" rel="noopener noreferrer"&gt;WordPress Theme Detector&lt;/a&gt; on Pixellize. It runs these same checks and returns the theme name, version, author, and detectable plugins in one view. It is the scripted logic above with a UI, and it is free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;WordPress is one of the easiest platforms to fingerprint: &lt;code&gt;wp-content&lt;/code&gt; in the source, a &lt;code&gt;/wp-login.php&lt;/code&gt; page, the generator tag, and a live &lt;code&gt;/wp-json/&lt;/code&gt; endpoint. Read the &lt;code&gt;/themes/&amp;lt;slug&amp;gt;/&lt;/code&gt; path for the theme and &lt;code&gt;/plugins/&amp;lt;slug&amp;gt;/&lt;/code&gt; for plugins. Script it for bulk checks, or run a single URL through the &lt;a href="https://pixellize.io/wordpress-theme-detector" rel="noopener noreferrer"&gt;detector&lt;/a&gt; when you just need a fast answer.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Meta Title and Description Length: What Google Actually Cuts Off</title>
      <dc:creator>Simran Kaur</dc:creator>
      <pubDate>Tue, 01 Sep 2026 07:15:27 +0000</pubDate>
      <link>https://dev.to/simran_kaur_9eda1e242c31f/meta-title-and-description-length-what-google-actually-cuts-off-22j8</link>
      <guid>https://dev.to/simran_kaur_9eda1e242c31f/meta-title-and-description-length-what-google-actually-cuts-off-22j8</guid>
      <description>&lt;p&gt;You ship a page, Google indexes it, and then your title shows up cut off with an ellipsis in the search results. Or worse, your carefully written meta description gets replaced entirely. Most of the time the cause is boring: the title or description was the wrong length.&lt;/p&gt;

&lt;p&gt;Here is what actually gets truncated, the numbers that matter, and how to check any page in a few seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why length matters at all
&lt;/h2&gt;

&lt;p&gt;Google gives each search result a fixed amount of room. Go over it and the end of your title or description gets chopped with an ellipsis, which usually means your call to action or keyword never gets seen. Under-use it and you waste space that could have earned the click.&lt;/p&gt;

&lt;p&gt;Two elements you control:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Title tag&lt;/strong&gt; (&lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;): the blue clickable headline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meta description&lt;/strong&gt; (&lt;code&gt;&amp;lt;meta name="description"&amp;gt;&lt;/code&gt;): the gray text under it&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The numbers worth remembering
&lt;/h2&gt;

&lt;p&gt;Google measures width in pixels, not characters, but characters are a reliable proxy for a quick check:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Safe range&lt;/th&gt;
&lt;th&gt;Gets cut around&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Title tag&lt;/td&gt;
&lt;td&gt;50 to 60 characters&lt;/td&gt;
&lt;td&gt;~60 characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meta description&lt;/td&gt;
&lt;td&gt;120 to 155 characters&lt;/td&gt;
&lt;td&gt;~155 to 160 characters&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Keep your primary keyword near the front of the title, because a truncated title still shows its beginning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading them from the DOM
&lt;/h2&gt;

&lt;p&gt;If you want to check the current page in the console:&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;title&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="nx"&gt;title&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;desc&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;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;meta[name="description"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;content&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="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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Title:&lt;/span&gt;&lt;span class="dl"&gt;"&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;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chars&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Description:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;desc&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chars&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;warn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;max&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;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;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;len&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;too short&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;len&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;too long&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;good&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Title&lt;/span&gt;&lt;span class="dl"&gt;"&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;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;desc&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="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;155&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works for a page you are on. Checking a page you have already deployed is the more common need, and for that you have to fetch the HTML and parse the two tags out. Doing that from the browser hits CORS walls, so it usually needs a small server-side fetch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part people skip: the SERP preview
&lt;/h2&gt;

&lt;p&gt;Length numbers tell you if something fits, but they do not show you what the result will actually look like. That matters because the same 58-character title reads very differently depending on the words. The only way to judge it is to see it rendered like a real Google result, on both desktop and mobile, since mobile truncates sooner.&lt;/p&gt;

&lt;h2&gt;
  
  
  A faster way to check
&lt;/h2&gt;

&lt;p&gt;Rather than rebuild the fetch-and-parse step every time, I put it behind a UI: the &lt;a href="https://pixellize.io/title-description-checker" rel="noopener noreferrer"&gt;Title and Meta Description Checker&lt;/a&gt; on Pixellize.&lt;/p&gt;

&lt;p&gt;Paste any public URL and it pulls the live title and meta description, shows the character count against Google's ranges with color-coded bars, gives a quick 0 to 100 score, and renders a live &lt;a href="https://pixellize.io/title-description-checker" rel="noopener noreferrer"&gt;SERP snippet preview&lt;/a&gt; for desktop and mobile. You can edit the title and description inline and watch the preview update, which is handy for testing rewrites before you push them. It runs free in the browser, and you can even copy a shareable link that reopens the exact check for a teammate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Titles cut off near 60 characters, descriptions near 155, and Google measures pixels so treat those as guidelines, not hard rules. Check the length, then look at the actual snippet preview before you decide, because fitting the box and earning the click are two different things. Run one of your live pages through the &lt;a href="https://pixellize.io/title-description-checker" rel="noopener noreferrer"&gt;checker&lt;/a&gt; and see how it looks.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Convert Text to Binary (and Back) in JavaScript</title>
      <dc:creator>Simran Kaur</dc:creator>
      <pubDate>Sun, 30 Aug 2026 06:53:59 +0000</pubDate>
      <link>https://dev.to/simran_kaur_9eda1e242c31f/how-to-convert-text-to-binary-and-back-in-javascript-4e8i</link>
      <guid>https://dev.to/simran_kaur_9eda1e242c31f/how-to-convert-text-to-binary-and-back-in-javascript-4e8i</guid>
      <description>&lt;p&gt;You type "Hi" and the computer stores &lt;code&gt;01001000 01101001&lt;/code&gt;. Text is just numbers wearing a costume. Here is exactly how a string turns into binary, why UTF-8 matters, and how to do the conversion both ways in a few lines of JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "binary" actually means here
&lt;/h2&gt;

&lt;p&gt;Computers do not store letters. They store numbers, and every number is a run of ones and zeros. Each character maps to a code point, that number becomes a byte, and each byte is written as &lt;strong&gt;eight bits&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The letter &lt;code&gt;A&lt;/code&gt; has the ASCII code 65. In binary that is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;65 = 01000001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lowercase &lt;code&gt;a&lt;/code&gt; is 97, which is &lt;code&gt;01100001&lt;/code&gt;. So the whole word "Hi" (&lt;code&gt;H&lt;/code&gt; = 72, &lt;code&gt;i&lt;/code&gt; = 105) becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;01001000 01101001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Group the bits into bytes of 8 and you can read any binary string back into text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Text to binary in JavaScript
&lt;/h2&gt;

&lt;p&gt;The reliable way is &lt;code&gt;TextEncoder&lt;/code&gt;. It hands you the raw UTF-8 bytes, so you do not have to worry about character codes above 127.&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;textToBinary&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&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;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&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="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&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="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&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;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;span class="nf"&gt;textToBinary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "01001000 01101001"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;toString(2)&lt;/code&gt; gives the binary digits, and &lt;code&gt;padStart(8, "0")&lt;/code&gt; keeps every byte a full 8 bits. Without the pad, &lt;code&gt;H&lt;/code&gt; would come out as &lt;code&gt;1001000&lt;/code&gt; (7 bits) and the string would be impossible to split back cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Binary back to text
&lt;/h2&gt;

&lt;p&gt;Reverse the process: strip spaces, cut the string into 8-bit chunks, parse each chunk as a base-2 number, then decode the bytes with &lt;code&gt;TextDecoder&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;binaryToText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bin&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;bits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bin&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;+/g&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&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;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bits&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;8&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;bytes&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bytes&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;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bits&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="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="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;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;binaryToText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;01001000 01101001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "Hi"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two checks worth adding in real code: reject anything that is not &lt;code&gt;0&lt;/code&gt; or &lt;code&gt;1&lt;/code&gt;, and reject a bit count that is not a multiple of 8. Those two guards catch almost every bad paste.&lt;/p&gt;

&lt;h2&gt;
  
  
  The UTF-8 catch that trips people up
&lt;/h2&gt;

&lt;p&gt;ASCII fits in one byte, so &lt;code&gt;A&lt;/code&gt; is always 8 bits. But the moment you leave plain English, a character can take more than one byte.&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="nf"&gt;textToBinary&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;// "11000011 10101001"  (2 bytes)&lt;/span&gt;
&lt;span class="nf"&gt;textToBinary&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;// 4 bytes, 32 bits&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is UTF-8 doing its job. If you assume one character equals one byte, accented letters and emoji will break your decoder. Using &lt;code&gt;TextEncoder&lt;/code&gt; and &lt;code&gt;TextDecoder&lt;/code&gt; handles this for you, which is why the manual &lt;code&gt;charCodeAt&lt;/code&gt; approach you see in a lot of old tutorials quietly corrupts non-ASCII text.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you just need the answer, not the code
&lt;/h2&gt;

&lt;p&gt;If you are debugging, teaching, or solving a puzzle and you do not want to open a console, I built a free tool for this on Pixellize: the &lt;a href="https://pixellize.io/text-to-binary" rel="noopener noreferrer"&gt;Text to Binary Translator&lt;/a&gt;. Paste text or binary, it converts both ways as you type, handles full Unicode, and runs entirely in your browser so nothing you paste is uploaded. It is the same logic above with a UI on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;Text to binary is not magic, it is three steps: character to code point, code point to byte, byte to 8 bits. &lt;code&gt;TextEncoder&lt;/code&gt; and &lt;code&gt;TextDecoder&lt;/code&gt; give you the correct answer including emoji, and padding each byte to 8 bits is the detail that makes the round trip reversible. Try it in the console, or paste something into the &lt;a href="https://pixellize.io/text-to-binary" rel="noopener noreferrer"&gt;Text to Binary Translator&lt;/a&gt; and watch it happen live.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Extract a .7z File Online Without Installing Anything</title>
      <dc:creator>Simran Kaur</dc:creator>
      <pubDate>Wed, 26 Aug 2026 17:48:35 +0000</pubDate>
      <link>https://dev.to/simran_kaur_9eda1e242c31f/how-to-extract-a-7z-file-online-without-installing-anything-123</link>
      <guid>https://dev.to/simran_kaur_9eda1e242c31f/how-to-extract-a-7z-file-online-without-installing-anything-123</guid>
      <description>&lt;p&gt;You download a file, it ends in &lt;code&gt;.7z&lt;/code&gt;, and your computer just shrugs. Double-clicking does nothing useful. Windows has no built-in way to open it, and neither does macOS. So you go hunting for yet another app to install for a one-time job.&lt;/p&gt;

&lt;p&gt;There is a faster way. You can open and extract a &lt;code&gt;.7z&lt;/code&gt; archive straight in your browser, with no install and no upload.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a .7z file actually is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;.7z&lt;/code&gt; is a compressed archive format, like &lt;code&gt;.zip&lt;/code&gt;, but it usually squeezes files smaller. That is why a lot of software, game mods, and large downloads ship as &lt;code&gt;.7z&lt;/code&gt;. The tradeoff is support: &lt;code&gt;.zip&lt;/code&gt; opens everywhere, &lt;code&gt;.7z&lt;/code&gt; often does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The usual options, and why they are annoying
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Install a desktop app.&lt;/strong&gt; It works, but it is a full download and setup for something you might need once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use an online tool that uploads your file.&lt;/strong&gt; Fast to start, but now your archive sits on someone else's server. For anything private, that is a real concern.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Extract a 7z file online, in your browser
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://pixellize.io/7z-extractor" rel="noopener noreferrer"&gt;7z Extractor on Pixellize&lt;/a&gt; runs entirely in your browser. Your file never leaves your device. Here is the whole flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;a href="https://pixellize.io/7z-extractor" rel="noopener noreferrer"&gt;7z Extractor&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Drop your &lt;code&gt;.7z&lt;/code&gt; file onto the page, or click to pick it.&lt;/li&gt;
&lt;li&gt;Browse the contents right there. You see every file and folder inside.&lt;/li&gt;
&lt;li&gt;Pull out one file, or extract everything as a single &lt;code&gt;.zip&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is it. No account, no email, no install.&lt;/p&gt;

&lt;h2&gt;
  
  
  It handles the tricky cases too
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Password-protected archives.&lt;/strong&gt; If the &lt;code&gt;.7z&lt;/code&gt; was made with a password, enter it and extract normally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nested folders.&lt;/strong&gt; The file tree shows the full structure, so you grab exactly what you need instead of dumping everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Big archives.&lt;/strong&gt; Since it runs locally, you are limited by your own machine, not an upload cap.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When a desktop app still makes sense
&lt;/h2&gt;

&lt;p&gt;If you extract &lt;code&gt;.7z&lt;/code&gt; files every day, or you script extraction as part of a build, a desktop tool like 7-Zip on Windows or The Unarchiver on Mac is the better fit. The online route wins for the common case: a one-off archive you just need to open right now, on any machine, without adding another installer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can I open a .7z file without 7-Zip?&lt;/strong&gt;&lt;br&gt;
Yes. A browser-based extractor opens &lt;code&gt;.7z&lt;/code&gt; files with nothing installed. You browse the contents and pull files out directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it safe to extract 7z files online?&lt;/strong&gt;&lt;br&gt;
It depends on the tool. If the file uploads to a server, treat it like any upload. A tool that runs in your browser keeps the file on your device, which is safer for private archives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between .7z and .zip?&lt;/strong&gt;&lt;br&gt;
Both are compressed archives. &lt;code&gt;.7z&lt;/code&gt; usually compresses smaller, but &lt;code&gt;.zip&lt;/code&gt; opens on more devices without extra software.&lt;/p&gt;

&lt;p&gt;Next time a &lt;code&gt;.7z&lt;/code&gt; lands in your downloads, skip the installer. Open it in the browser, grab your files, and move on: &lt;a href="https://pixellize.io/7z-extractor" rel="noopener noreferrer"&gt;try the 7z Extractor&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>7z</category>
      <category>archive</category>
      <category>tools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Saving a tweet as a PDF is harder than it looks (CORS, tokens, and Unicode)</title>
      <dc:creator>Simran Kaur</dc:creator>
      <pubDate>Thu, 06 Aug 2026 04:50:15 +0000</pubDate>
      <link>https://dev.to/simran_kaur_9eda1e242c31f/saving-a-tweet-as-a-pdf-is-harder-than-it-looks-cors-tokens-and-unicode-10hf</link>
      <guid>https://dev.to/simran_kaur_9eda1e242c31f/saving-a-tweet-as-a-pdf-is-harder-than-it-looks-cors-tokens-and-unicode-10hf</guid>
      <description>&lt;p&gt;I wanted a simple thing: paste a Twitter/X post link, get a clean PDF of it. Text, author, images, the whole card. No screenshots stitched together.&lt;/p&gt;

&lt;p&gt;It sounded like a one-afternoon project. It was not. Here are the four walls I hit, and how I got past each one. If you ever try to read tweet data from the browser, this will save you a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wall 1: you cannot read a tweet from the browser
&lt;/h2&gt;

&lt;p&gt;The obvious first attempt: fetch the tweet from the syndication endpoint that X's own embed widgets use.&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;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1719049883789729890&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="s2"&gt;`https://cdn.syndication.twimg.com/tweet-result?id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;lang=en&amp;amp;token=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;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="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// TypeError: Failed to fetch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dead on arrival. The response carries this header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Access-Control-Allow-Origin: https://platform.twitter.com
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the endpoint only allows requests from X's own embed origin. No other website can call it from the browser, CORS blocks it. The old conversation endpoints (&lt;code&gt;/timeline/conversation/&amp;lt;id&amp;gt;.json&lt;/code&gt;) now return &lt;code&gt;200&lt;/code&gt; with an empty body, so those are gone too. And the official API needs a paid key.&lt;/p&gt;

&lt;p&gt;There is no pure client-side path. If you want tweet data on your own site, something on your server has to fetch it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: a tiny server-side proxy
&lt;/h2&gt;

&lt;p&gt;CORS is a browser rule. Server to server, it does not apply. So the browser asks my server, and my server asks X. My stack is WordPress, so this is a small REST endpoint (any backend works the same way).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;register_rest_route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'myplugin/v1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/tweet'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s1"&gt;'methods'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'GET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'permission_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'__return_true'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$id&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/[^0-9]/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$req&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/[^A-Za-z0-9]/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$req&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'token'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://cdn.syndication.twimg.com/tweet-result?id=&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;lang=en&amp;amp;token=&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_remote_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s1"&gt;'timeout'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'headers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'User-Agent'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Mozilla/5.0 ... Chrome/124 Safari/537.36'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="c1"&gt;// ...normalize and return JSON&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One caveat worth knowing: send a real browser &lt;code&gt;User-Agent&lt;/code&gt;. Without it the endpoint gets moody.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wall 2: the mysterious &lt;code&gt;token&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;That endpoint needs a &lt;code&gt;token&lt;/code&gt; query param. It is not an API key. It is a value derived from the tweet ID, and X's own embed code computes it client-side. The formula floating around the web (used by Vercel's &lt;code&gt;react-tweet&lt;/code&gt;) is:&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;token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e15&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;PI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&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;0+|&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="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I compute this in the browser (it is trivial and exact there) and pass it to my endpoint, so the PHP side never has to reimplement JavaScript's float-to-base36. Clean division of labor.&lt;/p&gt;

&lt;p&gt;Now the browser flow is:&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;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseIdFromUrl&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&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;`/wp-json/myplugin/v1/tweet?id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;token=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&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;r&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="c1"&gt;// { name, handle, avatar, text, images, likes, replies, quoted }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wall 3: images taint the canvas
&lt;/h2&gt;

&lt;p&gt;To put images into a PDF, most tools rasterize through a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;. But drawing a cross-origin image onto a canvas taints it, and &lt;code&gt;toDataURL()&lt;/code&gt; throws a security error. Twitter's &lt;code&gt;pbs.twimg.com&lt;/code&gt; sometimes cooperates with &lt;code&gt;crossOrigin&lt;/code&gt;, sometimes not.&lt;/p&gt;

&lt;p&gt;Simplest reliable fix: let the server fetch the image bytes too and hand them back as base64 data URLs. Data URLs are same-origin by definition, so no taint, ever.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$bin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_remote_retrieve_body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;wp_remote_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$imgUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
&lt;span class="nv"&gt;$dataUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'data:'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$mime&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;';base64,'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;base64_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$bin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, base64 is about 33% bigger than the binary. For a handful of tweet images, who cares. It just works, on every browser, every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wall 4: jsPDF breaks on Hindi and emoji
&lt;/h2&gt;

&lt;p&gt;This one bit hardest. I drew the whole tweet card with &lt;a href="https://github.com/parallax/jsPDF" rel="noopener noreferrer"&gt;jsPDF&lt;/a&gt;, which is great, until you feed it anything outside Latin-1. The built-in fonts (Helvetica and friends) have no glyphs for Devanagari, Arabic, CJK, or emoji. Hindi text came out as boxes and broken characters.&lt;/p&gt;

&lt;p&gt;You can embed a full Unicode TTF, but a single font that covers every script plus color emoji is enormous and still incomplete.&lt;/p&gt;

&lt;p&gt;The trick that actually works: render the text with the browser's canvas (which already has the system fonts for every script and emoji), then drop that as a crisp image into the PDF.&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;textImage&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="nx"&gt;maxWpt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fontPt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;color&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;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                    &lt;span class="c1"&gt;// render at 3x for sharp text&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cv&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;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canvas&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;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fontPt&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px Inter, "Noto Sans", "Noto Sans Devanagari", "Apple Color Emoji", sans-serif`&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="nf"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&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="nx"&gt;maxWpt&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// your own word-wrap&lt;/span&gt;
  &lt;span class="nx"&gt;cv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;maxWpt&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;cv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lines&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;fontPt&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textBaseline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;top&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="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillText&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="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="nx"&gt;fontPt&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.4&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;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDataURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;wPt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;maxWpt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hPt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// then in jsPDF:&lt;/span&gt;
&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PNG&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wPt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hPt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tradeoff: that text is now an image, so it is not selectable. But it renders correctly in every language, which was the whole point. I keep the ASCII bits (handles, dates, counts) as real jsPDF text so those stay selectable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting the card together
&lt;/h2&gt;

&lt;p&gt;With data in hand, the layout is ordinary jsPDF drawing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A centered card on a white page (no heavy border, just the content).&lt;/li&gt;
&lt;li&gt;Circular avatar via a clip path (&lt;code&gt;saveGraphicsState&lt;/code&gt; + &lt;code&gt;circle&lt;/code&gt; + &lt;code&gt;clip&lt;/code&gt; + &lt;code&gt;addImage&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The verified badge is a blue filled circle with a white check drawn from two lines.&lt;/li&gt;
&lt;li&gt;Images in a 2x2 grid, capped at 4 per page (that is Twitter's own max per post), with pagination when a thread runs long.&lt;/li&gt;
&lt;li&gt;Quote tweets render as a nested bordered box, same building blocks, smaller.&lt;/li&gt;
&lt;li&gt;A footer link using &lt;code&gt;doc.textWithLink(...)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Engagement counts are worth a note: the public endpoint returns real &lt;code&gt;favorite_count&lt;/code&gt; (likes) and &lt;code&gt;conversation_count&lt;/code&gt; (replies), but not retweets or views. Rather than show a fake number or an empty dash, I only render the metrics that actually exist. Honest and cleaner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: PNG export for free
&lt;/h2&gt;

&lt;p&gt;Once the card renders nicely in the DOM (the on-page preview), exporting a shareable image is one library away. &lt;a href="https://github.com/niklasvh/html2canvas" rel="noopener noreferrer"&gt;html2canvas&lt;/a&gt; snapshots the preview element, and because the images are already base64 data URLs, there is no CORS drama.&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;canvas&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;html2canvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cardEl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#fff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;scale&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;useCORS&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;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBlob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;post.png&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;image/png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lessons
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;If a third party locks CORS to their own origin, no client-side trick beats it. Proxy through your server or stop.&lt;/li&gt;
&lt;li&gt;Compute fiddly values (like that base36 token) on whichever side has the right primitives, then pass them along. Do not port float math across languages for no reason.&lt;/li&gt;
&lt;li&gt;Base64 data URLs are the boring, reliable answer to canvas tainting.&lt;/li&gt;
&lt;li&gt;jsPDF is Latin-only out of the box. For real i18n, render text on a canvas and embed it as an image.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The finished thing does what I wanted: paste a link, get a clean PDF or image, threads and quote tweets included, every language intact, all in the browser with a thin server helper.&lt;/p&gt;

&lt;p&gt;If you want to see it in action: &lt;a href="https://pixellize.io/twitter-threads-to-pdf" rel="noopener noreferrer"&gt;Twitter/X to PDF&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Have you fought the syndication endpoint or jsPDF's font limits before? I would love to hear how you handled it.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Convert Images to PDF in the Browser (No Upload, No Server)</title>
      <dc:creator>Simran Kaur</dc:creator>
      <pubDate>Fri, 03 Jul 2026 07:13:07 +0000</pubDate>
      <link>https://dev.to/simran_kaur_9eda1e242c31f/convert-images-to-pdf-in-the-browser-no-upload-no-server-3aap</link>
      <guid>https://dev.to/simran_kaur_9eda1e242c31f/convert-images-to-pdf-in-the-browser-no-upload-no-server-3aap</guid>
      <description>&lt;p&gt;You have five scanned pages sitting on your desktop and a form that only accepts a single PDF. So you search "images to PDF", pick the first result, and upload your documents to a server you know nothing about.&lt;/p&gt;

&lt;p&gt;That last part should bother you more than it does.&lt;/p&gt;

&lt;p&gt;Most free image-to-PDF converters send your files to their backend, run the conversion there, and hand you a download. For a meme, fine. For an ID, a payslip, or a signed contract, you just handed a stranger a copy. The good news: converting images to a PDF is a job the browser can do entirely on its own, no upload required. Here is how, both in code and with a tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do it client-side?
&lt;/h2&gt;

&lt;p&gt;Bundling images into a PDF is pure client work. The browser can already decode a JPG or PNG, draw it to a canvas, and write the bytes of a PDF file. Nothing about that needs a server.&lt;/p&gt;

&lt;p&gt;Doing it in the browser buys you three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy.&lt;/strong&gt; The file never leaves the device. No upload, no server logs, no retention policy to trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed.&lt;/strong&gt; No round trip. A handful of images becomes a PDF in the time it takes to click.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline.&lt;/strong&gt; Once the page is loaded, it works with the network off.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only real cost is memory. Very large images held in a canvas use RAM, so a phone will tap out sooner than a laptop. For everyday scans and photos, that ceiling is high enough that you will not hit it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DIY way with jsPDF
&lt;/h2&gt;

&lt;p&gt;If you want to build this yourself, &lt;a href="https://github.com/parallax/jsPDF" rel="noopener noreferrer"&gt;jsPDF&lt;/a&gt; does the heavy lifting. It creates a PDF in memory and lets you drop an image onto each page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;jsPDF&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;jspdf&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;loadImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&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;img&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;Image&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;imagesToPdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&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;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;jsPDF&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a4&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;pageW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getWidth&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;pageH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHeight&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;files&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="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;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;img&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;loadImage&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;if &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;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;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JPEG&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageH&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="nf"&gt;revokeObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;images.pdf&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;Wire that to a file input and you have a working converter in about 25 lines.&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;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt; &lt;span class="na"&gt;accept=&lt;/span&gt;&lt;span class="s"&gt;"image/*"&lt;/span&gt; &lt;span class="na"&gt;multiple&lt;/span&gt;
       &lt;span class="na"&gt;onchange=&lt;/span&gt;&lt;span class="s"&gt;"imagesToPdf(this.files)"&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;Keep in mind what this simple version skips: it stretches every image to fill an A4 page, ignores aspect ratio, and offers no margins, ordering, or compression. Handling all of that well is where a few lines turns into a few hundred.&lt;/p&gt;

&lt;h2&gt;
  
  
  The no-code way
&lt;/h2&gt;

&lt;p&gt;When you just need the PDF and not a side project, a browser tool that already handles the edge cases is faster. &lt;a href="https://pixellize.io/images-to-pdf" rel="noopener noreferrer"&gt;Pixellize Images to PDF&lt;/a&gt; runs the whole conversion in your browser, so the files stay on your device, and it covers the parts the snippet above skips.&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%2Fb6xh9nua3qrxad45gwu6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb6xh9nua3qrxad45gwu6.png" alt="The Pixellize Images to PDF tool with three images ordered, page size, orientation, and export options" width="800" height="623"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Drop in your JPG, PNG, or HEIC images and you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ordering.&lt;/strong&gt; Drag the thumbnails into the page order you want before you export.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page setup.&lt;/strong&gt; Pick A4, Letter, or fit-to-image, plus portrait or landscape and a margin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compress images.&lt;/strong&gt; Shrink the output so the PDF is small enough for an upload cap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page numbers and grayscale.&lt;/strong&gt; Handy for printed handouts and scanned documents.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then hit Generate PDF and the file downloads. No account, no watermark, nothing uploaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few tips that save reprints
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Order before you export.&lt;/strong&gt; Reordering pages after the PDF exists means redoing it. Sort the thumbnails first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match the page size to the destination.&lt;/strong&gt; A4 for most of the world, Letter for the US. Fit-to-image keeps photos edge to edge with no white border.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compress for upload limits.&lt;/strong&gt; Portals that cap PDFs at a few MB are the usual reason a clean export gets rejected. Turn compression on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grayscale scanned text.&lt;/strong&gt; A scanned document in grayscale is smaller and prints cleaner than a full-color one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch orientation.&lt;/strong&gt; Portrait photos on a landscape page get letterboxed. Set the orientation to match the images.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Converting images to a PDF is one of those tasks that quietly does not need a server, and doing it in the browser keeps your files private and the whole thing instant. Build it with jsPDF when you want it inside your own app, or use a ready tool like &lt;a href="https://pixellize.io/images-to-pdf" rel="noopener noreferrer"&gt;Pixellize Images to PDF&lt;/a&gt; when you just need the file. Either way, your documents never have to leave your machine.&lt;/p&gt;

&lt;p&gt;If you build the DIY version, drop a link in the comments. I am curious how people handle aspect ratio and ordering.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>10 Free Developer Tools That Run 100% in Your Browser (No Signup, No Upload)</title>
      <dc:creator>Simran Kaur</dc:creator>
      <pubDate>Tue, 09 Jun 2026 05:37:37 +0000</pubDate>
      <link>https://dev.to/simran_kaur_9eda1e242c31f/10-free-developer-tools-that-run-100-in-your-browser-no-signup-no-upload-3fc3</link>
      <guid>https://dev.to/simran_kaur_9eda1e242c31f/10-free-developer-tools-that-run-100-in-your-browser-no-signup-no-upload-3fc3</guid>
      <description>&lt;p&gt;We have all done it. You are mid-debug, you need to pretty-print a chunk of JSON, so you paste it into the first "JSON formatter online" result you find. Then you remember that JSON had an auth token in it. Now it lives on someone else's server.&lt;/p&gt;

&lt;p&gt;That nagging feeling is why I keep coming back to tools that run &lt;strong&gt;entirely in the browser&lt;/strong&gt;. No upload, no account, no "we value your privacy" banner hiding a tracker. The page loads, JavaScript does the work on your machine, and nothing gets sent anywhere.&lt;/p&gt;

&lt;p&gt;Below are 10 of these I actually use, all from &lt;a href="https://pixellize.io" rel="noopener noreferrer"&gt;Pixellize&lt;/a&gt;. They are free, there is no signup, and you can open your network tab and watch them make zero requests while they work. Open one in a tab and pin it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. JSON Formatter, Validator, and Tree View
&lt;/h2&gt;

&lt;p&gt;The everyday one. Paste minified JSON, get it back indented, validated, and as a collapsible tree so you can actually navigate a deep payload.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// in
{"user":{"id":7,"roles":["admin","editor"],"active":true}}

// out
{
  "user": {
    "id": 7,
    "roles": ["admin", "editor"],
    "active": true
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The validator catches the trailing comma you cannot see at 2am. Tool: &lt;a href="https://pixellize.io/json-formatter/" rel="noopener noreferrer"&gt;JSON Formatter&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. CSS Compressor
&lt;/h2&gt;

&lt;p&gt;Strips whitespace, comments, and the last newline from a stylesheet before you ship it. Handy when a build step is overkill and you just want a quick minified blob.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* before */&lt;/span&gt;
&lt;span class="nc"&gt;.btn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#754ffe&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt; &lt;span class="m"&gt;20px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* after */&lt;/span&gt;
&lt;span class="nc"&gt;.btn&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="m"&gt;#754ffe&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="m"&gt;12px&lt;/span&gt; &lt;span class="m"&gt;20px&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tool: &lt;a href="https://pixellize.io/css-compressor/" rel="noopener noreferrer"&gt;CSS Compressor&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Hash Generator
&lt;/h2&gt;

&lt;p&gt;MD5, SHA-1, SHA-256, and friends from any string, computed in the browser with the Web Crypto API. Good for checksums, cache keys, or comparing a file hash without installing anything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sha256("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because it runs locally, you can hash something sensitive without it touching a server. Tool: &lt;a href="https://pixellize.io/hash-generator/" rel="noopener noreferrer"&gt;Hash Generator&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Unix Timestamp Converter
&lt;/h2&gt;

&lt;p&gt;The one I open three times a day. Paste a &lt;code&gt;1717900000&lt;/code&gt;, get a human date in your timezone and UTC. Go the other way too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1717900000  -&amp;gt;  Sat, 08 Jun 2024 23:46:40 UTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No more &lt;code&gt;new Date(ts * 1000)&lt;/code&gt; in the console just to read a log line. Tool: &lt;a href="https://pixellize.io/unix-timestamp-converter/" rel="noopener noreferrer"&gt;Unix Timestamp Converter&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. CSS Grid Generator
&lt;/h2&gt;

&lt;p&gt;A visual way to build a &lt;code&gt;grid-template&lt;/code&gt; and copy the CSS out. You drag the columns and rows, set gaps, and it hands you working code instead of you counting &lt;code&gt;fr&lt;/code&gt; units by hand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.grid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;grid-template-rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&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;Tool: &lt;a href="https://pixellize.io/css-grid-generator/" rel="noopener noreferrer"&gt;CSS Grid Generator&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Case Converter
&lt;/h2&gt;

&lt;p&gt;Rename-variable energy. Flip text between camelCase, snake_case, kebab-case, PascalCase, and UPPER/lower in one click. Saves you from hand-editing a list of 30 field names.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user profile id  -&amp;gt;  userProfileId  /  user_profile_id  /  user-profile-id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tool: &lt;a href="https://pixellize.io/case-converter/" rel="noopener noreferrer"&gt;Case Converter&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Password Generator
&lt;/h2&gt;

&lt;p&gt;Generates strong random passwords (and decent API-key-style strings) using the browser's crypto randomness, not &lt;code&gt;Math.random()&lt;/code&gt;. Set the length, toggle symbols, copy. Since it never phones home, the value you generate is only ever in your clipboard.&lt;/p&gt;

&lt;p&gt;Tool: &lt;a href="https://pixellize.io/password-generator/" rel="noopener noreferrer"&gt;Password Generator&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. QR Code Generator
&lt;/h2&gt;

&lt;p&gt;Drop in a URL, get a QR code you can download. Useful for a quick "open this on your phone" during testing, linking a staging build, or a deploy that needs a device check. Generated client-side, so a private staging URL does not leak to a QR service.&lt;/p&gt;

&lt;p&gt;Tool: &lt;a href="https://pixellize.io/qrcode-generator/" rel="noopener noreferrer"&gt;QR Code Generator&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Color Palette Generator
&lt;/h2&gt;

&lt;p&gt;Build a coherent palette when you do not have a designer on call. Pick a base color, get tints, shades, and matching accents with their hex values ready to paste into your tokens.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#754ffe&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--brand-600&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#5f3fe0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--brand-100&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#efe9ff&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;Tool: &lt;a href="https://pixellize.io/color-palette-generator/" rel="noopener noreferrer"&gt;Color Palette Generator&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Lorem Ipsum Generator
&lt;/h2&gt;

&lt;p&gt;The unglamorous workhorse. Generate placeholder paragraphs, sentences, or words to fill a layout before the real copy shows up. Pick the count, copy, move on.&lt;/p&gt;

&lt;p&gt;Tool: &lt;a href="https://pixellize.io/lorem-ipsum-generator/" rel="noopener noreferrer"&gt;Lorem Ipsum Generator&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "in the browser" actually matters
&lt;/h2&gt;

&lt;p&gt;It is not just a privacy nicety. Client-side tools are also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Faster.&lt;/strong&gt; No round trip to a server, so the result is instant even on a flaky connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline-friendly.&lt;/strong&gt; Once the page is loaded, most of these keep working with the network off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest about your data.&lt;/strong&gt; You can verify the claim yourself: open DevTools, watch the Network tab, run the tool, and see nothing leave the page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is the one I care about most. "We do not store your files" is a promise. Zero network requests is a fact you can check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rest of the kit
&lt;/h2&gt;

&lt;p&gt;Pixellize has around 130 of these in total, including PDF tools, image utilities, and SEO checkers, all the same way: free, no signup, in your browser. If you live in the browser dev tools all day, the &lt;a href="https://pixellize.io/tools/" rel="noopener noreferrer"&gt;full toolbox&lt;/a&gt; is worth a bookmark.&lt;/p&gt;

&lt;p&gt;What is the one small utility you open every single day? Drop it in the comments. I am always looking for the next tab to pin.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>tools</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Every YouTube video has 5 thumbnails sitting at predictable URLs (and how to grab them in 10 lines)</title>
      <dc:creator>Simran Kaur</dc:creator>
      <pubDate>Mon, 01 Jun 2026 09:35:51 +0000</pubDate>
      <link>https://dev.to/simran_kaur_9eda1e242c31f/every-youtube-video-has-5-thumbnails-sitting-at-predictable-urls-and-how-to-grab-them-in-10-lines-fmc</link>
      <guid>https://dev.to/simran_kaur_9eda1e242c31f/every-youtube-video-has-5-thumbnails-sitting-at-predictable-urls-and-how-to-grab-them-in-10-lines-fmc</guid>
      <description>&lt;p&gt;You've probably searched for a YouTube thumbnail downloader at some point. Maybe you wanted to study a competitor's design, build a research collection, or just save a thumbnail of your own video before re-uploading. The first 10 results in Google are all the same: a site with a paste-URL input, a "Generate" button, and 14 third-party scripts that load before the download link appears.&lt;/p&gt;

&lt;p&gt;Here's the thing: &lt;strong&gt;YouTube serves the same five thumbnail files at predictable URLs for every public video.&lt;/strong&gt; No auth, no API key, no rate limit. The URL pattern hasn't changed since 2014.&lt;/p&gt;

&lt;p&gt;The downloader sites are wrappers around &lt;code&gt;img.youtube.com&lt;/code&gt;. You can skip them entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The URL pattern
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://img.youtube.com/vi/&amp;lt;VIDEO_ID&amp;gt;/&amp;lt;QUALITY&amp;gt;.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;VIDEO_ID&amp;gt;&lt;/code&gt; is the 11-character ID that appears in every YouTube URL. &lt;code&gt;&amp;lt;QUALITY&amp;gt;&lt;/code&gt; is one of these:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Filename&lt;/th&gt;
&lt;th&gt;Dimensions&lt;/th&gt;
&lt;th&gt;Always available?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxresdefault.jpg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1280×720&lt;/td&gt;
&lt;td&gt;Only if creator uploaded a custom HD thumbnail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sddefault.jpg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;640×480&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hqdefault.jpg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;480×360&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mqdefault.jpg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;320×180&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;default.jpg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;120×90&lt;/td&gt;
&lt;td&gt;Yes (used in playlist UI)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Try it now. Paste this in your browser bar (it's PSY's "Gangnam Style" — definitely public):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://img.youtube.com/vi/9bZkp7q19f0/maxresdefault.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's the thumbnail. Right-click, Save Image As, done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extracting the video ID from any URL
&lt;/h2&gt;

&lt;p&gt;YouTube has at least 7 URL formats — standard watch, share link, Shorts, embed, music, mobile, old player. The 11-character ID lives in a different position depending on format. One regex catches them all:&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;extractYouTubeId&lt;/span&gt; &lt;span class="o"&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="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;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&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;(?:&lt;/span&gt;&lt;span class="sr"&gt;v=|youtu&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;be&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;|shorts&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;|embed&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;v&lt;/span&gt;&lt;span class="se"&gt;\/)([\w&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;]{11})&lt;/span&gt;&lt;span class="sr"&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;match&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="c1"&gt;// Test cases&lt;/span&gt;
&lt;span class="nf"&gt;extractYouTubeId&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://youtube.com/watch?v=9bZkp7q19f0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;         &lt;span class="c1"&gt;// → '9bZkp7q19f0'&lt;/span&gt;
&lt;span class="nf"&gt;extractYouTubeId&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://youtu.be/9bZkp7q19f0?t=42&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                &lt;span class="c1"&gt;// → '9bZkp7q19f0'&lt;/span&gt;
&lt;span class="nf"&gt;extractYouTubeId&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://youtube.com/shorts/abc123xyz4_&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// → 'abc123xyz4_'&lt;/span&gt;
&lt;span class="nf"&gt;extractYouTubeId&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://youtube.com/embed/9bZkp7q19f0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// → '9bZkp7q19f0'&lt;/span&gt;
&lt;span class="nf"&gt;extractYouTubeId&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://music.youtube.com/watch?v=9bZkp7q19f0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// → '9bZkp7q19f0'&lt;/span&gt;
&lt;span class="nf"&gt;extractYouTubeId&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://m.youtube.com/watch?v=9bZkp7q19f0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// → '9bZkp7q19f0'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The character class &lt;code&gt;[\w-]{11}&lt;/code&gt; matches exactly 11 characters of word-chars or hyphens (YouTube IDs use letters, numbers, hyphen, underscore — and are always 11 chars long).&lt;/p&gt;

&lt;h2&gt;
  
  
  The 10-line bookmarklet
&lt;/h2&gt;

&lt;p&gt;Drop this in your browser bookmarks bar. Click it on any YouTube watch page and the maxresdefault thumbnail opens in a new tab:&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="nx"&gt;javascript&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="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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;location&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="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;(?:&lt;/span&gt;&lt;span class="sr"&gt;v=|youtu&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;be&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;|shorts&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;|embed&lt;/span&gt;&lt;span class="se"&gt;\/)([\w&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;]{11})&lt;/span&gt;&lt;span class="sr"&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;m&lt;/span&gt;&lt;span class="p"&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="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://img.youtube.com/vi/&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="s2"&gt;/maxresdefault.jpg`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_blank&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;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Not a YouTube video URL&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;To install: create a new bookmark, name it "YT Thumb", paste the entire &lt;code&gt;javascript:...&lt;/code&gt; string above as the URL. Click it on any YouTube page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 404 gotcha nobody mentions
&lt;/h2&gt;

&lt;p&gt;About 40% of YouTube videos return a 404 at &lt;code&gt;/maxresdefault.jpg&lt;/code&gt;. This trips up every naive downloader.&lt;/p&gt;

&lt;p&gt;The HD thumbnail file only exists when the creator uploaded a custom thumbnail at 1280×720 or larger. Auto-generated thumbnails (the three frame previews YouTube picks when you don't upload one) and pre-2014 uploads don't have a maxres file.&lt;/p&gt;

&lt;p&gt;The fix: check sizes in priority order and use the first one that returns 200.&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;sizes&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;maxresdefault&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;sddefault&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;hqdefault&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;mqdefault&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;default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getBestThumbnail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;videoId&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;size&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;sizes&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="s2"&gt;`https://img.youtube.com/vi/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;videoId&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;size&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.jpg`&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="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="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;HEAD&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;size&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="kc"&gt;null&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;Note the &lt;code&gt;method: 'HEAD'&lt;/code&gt; — you only care about the status code, not the file body. Saves bandwidth and latency.&lt;/p&gt;

&lt;h2&gt;
  
  
  CORS, or why you can't fetch the image client-side
&lt;/h2&gt;

&lt;p&gt;If you try this in a browser script:&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://img.youtube.com/vi/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/maxresdefault.jpg`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="cm"&gt;/* download blob */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…you'll get a CORS error. &lt;code&gt;img.youtube.com&lt;/code&gt; doesn't set &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;, so the response body is hidden from JavaScript. The HEAD check above works because you're only checking status; the image itself can't be read into a Blob.&lt;/p&gt;

&lt;p&gt;Three workarounds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;window.open(url)&lt;/code&gt;&lt;/strong&gt; — opens the image in a new tab, user right-clicks to save. No CORS issue because the browser is fetching for display, not for JS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;a download&amp;gt;&lt;/code&gt; link&lt;/strong&gt; — set &lt;code&gt;href&lt;/code&gt; to the image URL, click programmatically. Same idea: browser handles the download, JS doesn't touch the bytes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server proxy&lt;/strong&gt; — your backend fetches the image (no CORS at server-to-server), returns it to your frontend with proper CORS headers. Required if you want to repackage as ZIP or apply edits.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  YouTube Shorts
&lt;/h2&gt;

&lt;p&gt;Shorts use the same backend as regular videos. The thumbnail file pattern is identical — only the URL format changes (&lt;code&gt;youtube.com/shorts/&amp;lt;ID&amp;gt;&lt;/code&gt; instead of &lt;code&gt;?v=&amp;lt;ID&amp;gt;&lt;/code&gt;). The regex above already handles this case.&lt;/p&gt;

&lt;p&gt;One thing to know: even though Shorts display in portrait (9:16), the file YouTube serves is still landscape 16:9 with letterboxing or padding. If you want the actual portrait frame, you need the YouTube Data API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Show DEV: my hosted version
&lt;/h2&gt;

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

&lt;p&gt;I built &lt;a href="https://pixellize.io/youtube-thumbnail-downloader/" rel="noopener noreferrer"&gt;Pixellize YouTube Thumbnail Downloader&lt;/a&gt; for the times when "paste a URL, click download" is faster than touching a bookmark. It runs the fallback chain automatically, parses every YouTube URL format the regex above catches, shows you what's available before you commit to a download, and has zero analytics scripts in the page. No signup, no daily limit, no watermark.&lt;/p&gt;

&lt;p&gt;I built it because the existing free downloaders all violate one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple ads above the download link&lt;/li&gt;
&lt;li&gt;Aggressive newsletter popups&lt;/li&gt;
&lt;li&gt;Account required for "premium" sizes&lt;/li&gt;
&lt;li&gt;Mobile redirects to apps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three rules I wrote it under: don't load third-party scripts (zero tracking pixels), don't gate any quality behind a signup, render the result faster than the ads on competitor sites would have loaded.&lt;/p&gt;

&lt;p&gt;Source code for the bookmarklet above + the fallback function are MIT licensed; you can wire them into your own tool. The hosted version handles the edge cases (CORS workaround via server fetch, age-restricted video detection, deleted-video graceful failure) so users get a working result without troubleshooting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The simplest URL pattern beats every "tool" abstraction.&lt;/strong&gt; Knowing &lt;code&gt;img.youtube.com/vi/&amp;lt;ID&amp;gt;/&amp;lt;QUALITY&amp;gt;.jpg&lt;/code&gt; makes 90% of "free YouTube thumbnail downloader" sites obsolete for developers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CORS is the only meaningful ceiling.&lt;/strong&gt; Once you accept that the browser can't read the image bytes directly, you stop fighting it and use &lt;code&gt;window.open&lt;/code&gt; or &lt;code&gt;&amp;lt;a download&amp;gt;&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The 40% maxresdefault gap is real.&lt;/strong&gt; If you're building a tool, the fallback chain is the difference between "works for most videos" and "works for every public video".&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A 10-line bookmarklet replaces a 200-script download site.&lt;/strong&gt; Sometimes the right answer is a one-liner pasted in your bookmark bar, not a SaaS subscription.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want the hosted version, here it is: &lt;strong&gt;&lt;a href="https://pixellize.io/youtube-thumbnail-downloader/" rel="noopener noreferrer"&gt;pixellize.io/youtube-thumbnail-downloader&lt;/a&gt;&lt;/strong&gt;. If you'd rather build your own, the code above is everything you need.&lt;/p&gt;

&lt;p&gt;Happy to answer questions about the implementation in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>youtube</category>
    </item>
    <item>
      <title>Auto-Convert Every WordPress Upload to WebP (Free, No Cloud Service)</title>
      <dc:creator>Simran Kaur</dc:creator>
      <pubDate>Fri, 29 May 2026 12:45:56 +0000</pubDate>
      <link>https://dev.to/simran_kaur_9eda1e242c31f/auto-convert-every-wordpress-upload-to-webp-free-no-cloud-service-oab</link>
      <guid>https://dev.to/simran_kaur_9eda1e242c31f/auto-convert-every-wordpress-upload-to-webp-free-no-cloud-service-oab</guid>
      <description>&lt;p&gt;If you have ever run a Lighthouse audit on a content-heavy WordPress site, you know the usual verdict: "Serve images in next-gen formats." Translation: your JPEGs and PNGs are too heavy, and WebP would cut them down a lot.&lt;/p&gt;

&lt;p&gt;The problem is that nobody wants to convert images by hand, and most automated options either run as a bulk job you have to remember to trigger, or push your images to a cloud service with a monthly quota.&lt;/p&gt;

&lt;p&gt;I wanted something simpler: upload an image, get a WebP, done. So I built a free plugin called &lt;strong&gt;&lt;a href="https://wordpress.org/plugins/pixellize-image-optimizer" rel="noopener noreferrer"&gt;Pixellize Image Optimizer&lt;/a&gt;&lt;/strong&gt;, and this post walks through the problem, how the plugin solves it, and how it works under the hood.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Why WebP is worth it
&lt;/h2&gt;

&lt;p&gt;WebP files are usually 25 to 35 percent smaller than the equivalent JPG or PNG at the same visual quality.&lt;br&gt;
On an image-heavy page that is a real difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Faster Largest Contentful Paint, which is one of the Core Web Vitals Google uses for ranking.&lt;/li&gt;
&lt;li&gt;Less bandwidth, which matters if you pay for CDN traffic.&lt;/li&gt;
&lt;li&gt;No visible quality loss at sensible quality settings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Browser support is no longer a concern. Every current browser handles WebP.&lt;/p&gt;

&lt;h2&gt;
  
  
  The approach: convert on your own server, on upload
&lt;/h2&gt;

&lt;p&gt;Instead of a cloud API, the plugin converts images locally with the PHP GD or Imagick extension (almost every host has one). There is no account, no API key, and no paid tier.&lt;/p&gt;

&lt;p&gt;The core flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You upload an image to the Media Library as usual.&lt;/li&gt;
&lt;li&gt;The plugin generates a WebP version of the full image and every thumbnail size.&lt;/li&gt;
&lt;li&gt;Your front end serves the WebP automatically.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How it works under the hood
&lt;/h2&gt;

&lt;p&gt;For WordPress developers, here is the interesting part. The plugin hooks into the standard upload and rendering pipeline rather than fighting it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On upload, it taps into the attachment metadata generation so it can convert the full image &lt;strong&gt;and&lt;/strong&gt; every registered sub-size, not just the original. That matters because responsive images use &lt;code&gt;srcset&lt;/code&gt;, and a half-converted set defeats the purpose.&lt;/li&gt;
&lt;li&gt;On the front end, it rewrites image URLs to the WebP version through the same filters WordPress already uses: attachment image source, the responsive &lt;code&gt;srcset&lt;/code&gt;, and post content. If a WebP does not exist for a given file, the original is served, so nothing breaks.&lt;/li&gt;
&lt;li&gt;File naming mirrors the original: &lt;code&gt;image.jpg&lt;/code&gt; becomes &lt;code&gt;image.webp&lt;/code&gt;, and a duplicate upload that WordPress renames to &lt;code&gt;image-1.jpg&lt;/code&gt; gets &lt;code&gt;image-1.webp&lt;/code&gt;. No surprises.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Keep your originals, or save the space
&lt;/h2&gt;

&lt;p&gt;This is the part I am happiest with in version 0.4. You get to choose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Replace mode (default):&lt;/strong&gt; the upload becomes a single WebP and the original is removed to save disk space.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep mode:&lt;/strong&gt; the original stays in your Media Library, a WebP is built alongside it, visitors are served the WebP, and you can put any image back to its original whenever you want.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In keep mode there is a one-click &lt;strong&gt;Restore&lt;/strong&gt; action in the Media Library that points an attachment back at its source file and rebuilds its sizes. And when you delete an image, the plugin cleans up the WebP copies with it, so you do not end up with orphaned files piling up on disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Install "Pixellize Image Optimizer" from the WordPress plugin directory and activate it. It checks for GD or Imagick before activating.&lt;/li&gt;
&lt;li&gt;Optionally visit Tools then Pixellize Image Optimizer to set the WebP quality. The default of 85 is a good balance for most sites.&lt;/li&gt;
&lt;li&gt;Upload images the way you always do.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the whole workflow. The settings page also shows a savings panel: how many images were converted and how much storage you have saved.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on quality
&lt;/h2&gt;

&lt;p&gt;Quality is adjustable from 0 to 100. A value between 80 and 90 keeps images looking clean while still cutting file size meaningfully. Going below 60 starts to show visible artifacts on detailed photos, so I would not push it lower without checking the results.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is next
&lt;/h2&gt;

&lt;p&gt;Bulk conversion of images you uploaded before installing the plugin is the next big feature on my list, followed by an option to resize oversized images on upload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The plugin is free and open source (GPL):&lt;br&gt;
&lt;a href="https://wordpress.org/plugins/pixellize-image-optimizer" rel="noopener noreferrer"&gt;https://wordpress.org/plugins/pixellize-image-optimizer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you try it, I would genuinely like to hear what works, what breaks, and what you would want next. Drop a comment or open a thread on the plugin support page.&lt;/p&gt;

&lt;p&gt;Set published: false so you can preview on dev.to first, then flip to true. Want me to add a cover image line, a code snippet showing the WordPress filter usage, or trim it shorter for a quicker read?&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
